Flatten the repo: move everything out of app/ to the root

Now that the CLI and the Next.js app are one package, nesting it inside
app/ served no purpose - the repo root itself becomes the published
npm package. Merges app/.gitignore and app/README.md into the root
versions, drops the now-duplicate app/LICENSE, and updates path
references (README, docs/ARCHITECTURE.md, docs/CONFIG_REFERENCE.md,
package.json's repository.directory) that assumed the app/ nesting.

Also fixes a real bug this surfaced: the in-app docs viewer resolved
docs/ relative to process.cwd(), which only worked by accident when the
CLI happened to be invoked from app/'s parent directory. A first attempt
at fixing it with import.meta.dirname broke instead, for the same
cross-module-graph reason config-path resolution already documented -
Next compiles Route Handlers through a separate module graph that
doesn't preserve source-relative import.meta paths. Fixed by exposing
the app root via TRIGGERSHELL_APP_ROOT (set once in server.ts, where
import.meta *does* resolve correctly), the same pattern already used
for TRIGGERSHELL_CONFIG_PATH.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 11:14:01 +02:00
co-authored by Claude Sonnet 5
parent 30350d80f4
commit 3f379ca2ac
123 changed files with 65 additions and 104 deletions
+62
View File
@@ -0,0 +1,62 @@
export const dynamic = "force-dynamic";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { getDb } from "@/lib/db/client";
import { users } from "@/lib/db/schema";
import { verifyPassword } from "@/lib/auth/password";
import { getSession } from "@/lib/auth/session";
import {
isRateLimited,
recordFailedAttempt,
clearAttempts,
} from "@/lib/auth/rate-limit";
const loginSchema = z.object({
username: z.string().min(1),
password: z.string().min(1),
});
export async function POST(request: Request) {
const rateLimitKey = request.headers.get("x-forwarded-for") ?? "local";
if (isRateLimited(rateLimitKey)) {
return Response.json(
{ error: "Too many attempts, try again later." },
{ status: 429 },
);
}
const body = await request.json().catch(() => null);
const parsed = loginSchema.safeParse(body);
if (!parsed.success) {
return Response.json({ error: "Invalid request body" }, { status: 400 });
}
const db = getDb();
const user = db
.select()
.from(users)
.where(eq(users.username, parsed.data.username))
.get();
if (
!user ||
!(await verifyPassword(user.passwordHash, parsed.data.password))
) {
recordFailedAttempt(rateLimitKey);
return Response.json({ error: "Invalid credentials" }, { status: 401 });
}
clearAttempts(rateLimitKey);
db.update(users)
.set({ lastLoginAt: new Date() })
.where(eq(users.id, user.id))
.run();
const session = await getSession();
session.userId = user.id;
session.username = user.username;
await session.save();
return Response.json({ user: { username: user.username } });
}