feat(AGE-48): Multi-step deployment wizard#39
Conversation
- Add /dashboard/new with 5-step wizard form - Step 1: Stack name + cloud provider selection - Step 2: Region/location + instance type (dynamic per provider) - Step 3: Owner info (name, timezone, working hours) - Step 4: Agent selection from presets with descriptions - Step 5: Review summary with cost estimates - Import constants and types directly from @agent-army/cli workspace package - Add exports to cli/package.json for cross-workspace imports - Progress indicator, back/forward navigation, field validation - Produces valid ArmyManifest on submit Closes AGE-48
📝 WalkthroughWalkthroughImplements a comprehensive authentication system for a Next.js web application, including Prisma database schema with User, Account, Session, and VerificationToken models, NextAuth configuration with credentials provider, login and signup pages with forms, protected dashboard page, middleware-based route protection, and a multi-step deployment wizard component. Also exposes cli package exports for constants and types. Changes
Sequence DiagramsequenceDiagram
participant Client as Client (Browser)
participant Server as Server (Next.js)
participant Auth as NextAuth Handler
participant DB as Prisma / Database
participant Session as Session Context
Client->>Server: POST /api/auth/signup (email, password)
Server->>DB: Check for existing user
DB-->>Server: User exists or not
alt User exists
Server-->>Client: 409 / Error response
else User not found
Server->>Server: Hash password with bcryptjs
Server->>DB: Create new User record
DB-->>Server: User created with id, email, name
Server-->>Client: 201 Created response
end
Client->>Server: POST /api/auth/callback/credentials (email, password)
Server->>Auth: NextAuth credentials provider
Auth->>DB: Query User by email
DB-->>Auth: User record with hashedPassword
Auth->>Auth: Verify password with bcryptjs
alt Password valid
Auth->>Auth: Create JWT token with user id
Auth-->>Server: JWT token
Server-->>Client: Set session cookie
Client->>Session: Access session context
Session-->>Client: User id, email, name available
else Password invalid
Auth-->>Server: Unauthorized
Server-->>Client: 401 Unauthorized
end
Client->>Server: GET /dashboard (with session)
Server->>Auth: Verify NextAuth token
Auth-->>Server: Token valid
Server->>Server: auth() retrieves session
Server-->>Client: Render dashboard with greeting
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@cli/package.json`:
- Around line 21-25: The package.json exports currently point to source .ts
files (exports like "./lib/constants": "./lib/constants.ts" and "./types":
"./types.ts"), which will break consumers when only dist is published; update
the "exports" mapping to reference the compiled outputs under dist (e.g. map
"./lib/constants" to the compiled JS at dist/lib/constants.js and its types at
dist/lib/constants.d.ts, and map "./types" to dist/types.js with
dist/types.d.ts) so module resolution and type declarations work for consumers
after tsc-outDir compilation.
In `@web/package.json`:
- Around line 13-18: Your package.json currently pins next-auth to
"5.0.0-beta.30" (next-auth) which is unstable; either replace that dependency
with the stable v4 ("4.24.13") or add an explicit rationale in project docs and
add CI tests for auth flows to cover App Router behaviors; also add a
max-password-length validation wherever passwords are handled (e.g.,
registerUser / validatePassword or auth controller) because bcryptjs truncates
at 72 bytes, and confirm `@prisma/client` ("5.22.0") is intentionally chosen or
update if needed. Ensure changes include updating package.json dependency,
adding a short README note about the v5-beta choice if kept, and adding
unit/integration tests for authentication paths and a password-length check in
the password-handling functions.
In `@web/src/app/api/auth/signup/route.ts`:
- Around line 7-57: Validate that email and password are strings (e.g., typeof
email === "string" && typeof password === "string"), trim and normalize the
email to lowercase before use, and only proceed to bcrypt.hash after those
checks; keep the prisma.user.findUnique existence check but wrap
prisma.user.create in a try/catch that specifically detects a
Prisma.PrismaClientKnownRequestError with code "P2002" (unique constraint) and
return NextResponse.json({ error: "User with this email already exists" }, {
status: 409 }) for that case, while rethrowing or returning a 500 for other
errors—update the final catch block to discriminate P2002 vs generic errors and
return 409 when encountered.
In `@web/src/app/dashboard/new/page.tsx`:
- Around line 68-72: The agent selection UI uses a clickable <div> with a
readOnly checkbox which blocks keyboard users; update the selection markup so it
is keyboard-accessible—either wrap the checkbox and content in a <label> and
wire the checkbox's onChange to the existing selection handler (remove
readOnly), or make the clickable container interactive by adding role="button",
tabIndex={0}, and an onKeyDown handler that toggles selection on Enter/Space and
calls the same selection function; ensure the visual style referenced by
agentCard remains unchanged and that the checkbox input and the container call
the same selection method so both mouse and keyboard trigger selection.
- Around line 98-115: In validate(), trim state.stackName once and use that
trimmed value for both checks to avoid false pattern errors and duplicates; e.g.
const name = state.stackName.trim(); then if (step === 0) { if (!name)
errs.push("Stack name is required"); else if (!/^[a-z0-9-]+$/.test(name))
errs.push("Stack name: lowercase letters, numbers, hyphens only"); } — update
references to state.stackName accordingly so validate() uses the trimmed name
for validation.
In `@web/src/app/dashboard/page.tsx`:
- Around line 17-28: Replace the GET anchor logout link with a client-side
logout button component that calls signOut from next-auth/react: create a "use
client" component named LogoutButton (e.g., export function LogoutButton) which
imports signOut and invokes signOut({ redirectTo: "/login" }) in an onClick
handler, apply the existing styles to a <button> (remove href/anchor usage), and
then import and render LogoutButton in page.tsx instead of the anchor to ensure
proper POST logout behavior in NextAuth v5.
🧹 Nitpick comments (3)
web/src/app/login/page.tsx (1)
34-35: Unused variable in catch block.The
errorvariable is caught but never used. Consider logging it for debugging or using an underscore prefix to indicate it's intentionally ignored.🔧 Suggested fix
- } catch (error) { + } catch (_error) { setError("An error occurred. Please try again.");web/prisma/schema.prisma (2)
12-21: Consider makinghashedPasswordoptional for future OAuth support.Currently
hashedPasswordis required (String), but if OAuth providers (Google, GitHub, etc.) are added later, those users won't have passwords. Making it optional (String?) would future-proof the schema.🔧 Suggested change
model User { id String `@id` `@default`(cuid()) email String `@unique` name String? - hashedPassword String + hashedPassword String? createdAt DateTime `@default`(now()) updatedAt DateTime `@updatedAt` sessions Session[] accounts Account[] }Note: The check in
auth.ts(!user.hashedPassword) already handles this case defensively.
7-10: SQLite is suitable for development; consider production database strategy.SQLite works well for local development and small deployments. Ensure there's a plan for migrating to a production-ready database (PostgreSQL, MySQL) when scaling, as SQLite has concurrency limitations.
| "license": "MIT", | ||
| "exports": { | ||
| "./lib/constants": "./lib/constants.ts", | ||
| "./types": "./types.ts" | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n cli/package.jsonRepository: stepandel/agent-army
Length of output: 841
🏁 Script executed:
find dist -type f \( -name "*.js" -o -name "*.d.ts" \) 2>/dev/null | head -20 || echo "dist directory not found or empty"Repository: stepandel/agent-army
Length of output: 46
🏁 Script executed:
# Check if there's a build script and any TypeScript config
cat -n cli/tsconfig.json 2>/dev/null | head -30 || echo "No tsconfig.json found"Repository: stepandel/agent-army
Length of output: 602
🏁 Script executed:
# Check for the source files referenced in exports
ls -la cli/lib/constants.ts 2>/dev/null || echo "cli/lib/constants.ts not found"
ls -la cli/types.ts 2>/dev/null || echo "cli/types.ts not found"Repository: stepandel/agent-army
Length of output: 195
🏁 Script executed:
# Check dist directory contents
ls -la cli/dist/ 2>/dev/null || echo "cli/dist directory not found or empty"Repository: stepandel/agent-army
Length of output: 101
🏁 Script executed:
# Check if dist directory even exists
test -d cli/dist && echo "dist directory exists" || echo "dist directory does not exist"Repository: stepandel/agent-army
Length of output: 93
Exporting TS sources will break consumers when only dist is published.
The "exports" field maps to ./lib/constants.ts and ./types.ts, but "files" includes only dist. When the package is built with tsc (configured with "outDir": "./dist"), these files will be compiled to ./dist/lib/constants.js, ./dist/lib/constants.d.ts, etc., which won't match the export paths. Consumers will encounter module resolution failures. Update exports to reference the compiled outputs in dist with corresponding type declarations.
🔧 Suggested fix (map exports to compiled outputs)
"exports": {
- "./lib/constants": "./lib/constants.ts",
- "./types": "./types.ts"
+ "./lib/constants": {
+ "types": "./dist/lib/constants.d.ts",
+ "default": "./dist/lib/constants.js"
+ },
+ "./types": {
+ "types": "./dist/types.d.ts",
+ "default": "./dist/types.js"
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "license": "MIT", | |
| "exports": { | |
| "./lib/constants": "./lib/constants.ts", | |
| "./types": "./types.ts" | |
| } | |
| "license": "MIT", | |
| "exports": { | |
| "./lib/constants": { | |
| "types": "./dist/lib/constants.d.ts", | |
| "default": "./dist/lib/constants.js" | |
| }, | |
| "./types": { | |
| "types": "./dist/types.d.ts", | |
| "default": "./dist/types.js" | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@cli/package.json` around lines 21 - 25, The package.json exports currently
point to source .ts files (exports like "./lib/constants": "./lib/constants.ts"
and "./types": "./types.ts"), which will break consumers when only dist is
published; update the "exports" mapping to reference the compiled outputs under
dist (e.g. map "./lib/constants" to the compiled JS at dist/lib/constants.js and
its types at dist/lib/constants.d.ts, and map "./types" to dist/types.js with
dist/types.d.ts) so module resolution and type declarations work for consumers
after tsc-outDir compilation.
| "@auth/prisma-adapter": "^2.11.1", | ||
| "@prisma/client": "5.22.0", | ||
| "bcryptjs": "^3.0.3", | ||
| "next": "^14.2.0", | ||
| "next-auth": "5.0.0-beta.30", | ||
| "react": "^18.3.0", |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
next-auth latest stable version 2025 production ready
💡 Result:
As of Feb 10, 2026, the latest stable (production) release of next-auth (NextAuth.js/Auth.js) on npm is:
next-auth@4.24.13(stable) — released Oct 29, 2025. [1][2]- The v5 line is still beta; the latest beta shown is
5.0.0-beta.30(released Oct 27, 2025). [1][3]
Production-ready recommendation (2025/2026):
- If you want “stable” specifically, use
4.24.13. [1] - Note: the maintainers state Auth.js (formerly NextAuth.js) is now maintained by the Better Auth team and recommend new projects start with Better Auth unless you have specific gaps. [2]
- Security note: GitHub’s advisory shows fixes landed in
4.24.12and5.0.0-beta.30for an email sign-in misdelivery issue, so you should be on at least those versions if affected. [3]
Sources: [1] [2] [3]
🌐 Web query:
next-auth 5.0.0-beta.30 vs next-auth 5.0.0 stable release date
💡 Result:
next-auth@5.0.0-beta.30release date: October 27, 2025 (published as a beta) [1][2]next-auth@5.0.0(stable) release date: Not released / no stable5.0.0published as of February 10, 2026—the latest stable line shown is still4.24.13(October 29, 2025) [1]
Sources:
[1] next-auth release history (NPM release tracker) https://releasealert.dev/npm/_/next-auth
[2] GitHub Advisory noting patched version 5.0.0-beta.30 published Oct 27, 2025 GHSA-5jpx-9hw9-2fx4
🌐 Web query:
bcryptjs 3.0.3 security advisories vulnerabilities
💡 Result:
bcryptjs@3.0.3 — known security advisories / vulnerabilities
-
Direct vulnerabilities in
bcryptjs@3.0.3: none currently listed in Snyk’s vulnerability database (Snyk shows “No direct vulnerabilities” and lists 3.0.3 as the latest non‑vulnerable version). [1] -
Important security footgun (design property): 72-byte password truncation.
bcryptjs(like bcrypt generally) ignores bytes beyond 72, so applications that don’t enforce a max password length can create surprising authentication behavior for very long passwords. This has shown up as a vulnerability in downstream applications (example: CVE-2025-25298 in Strapi, fixed by adding max-length validation rather than changingbcryptjs). [2][3]
Practical guidance
- If you use
bcryptjs@3.0.3, enforce a maximum password length of 72 bytes (or fewer) (and handle Unicode/UTF-8 bytes explicitly) to avoid truncation surprises described above. [2] - Also run your usual dependency scanning (
npm audit, Dependabot, Snyk) for indirect vulnerabilities (even thoughbcryptjsitself is “zero dependencies”). [1]
Sources: Snyk package advisory status for bcryptjs@3.0.3 [1]; NVD record for CVE-2025-25298 (downstream misuse of bcryptjs length behavior) [2]; Tenable summary referencing the associated advisory [3].
[1] https://security.snyk.io/package/npm/bcryptjs/3.0.3
[2] https://nvd.nist.gov/vuln/detail/CVE-2025-25298
[3] https://www.tenable.com/cve/CVE-2025-25298
🌐 Web query:
Prisma 5.22.0 security advisories vulnerabilities
💡 Result:
As of Feb 10, 2026, there are no known direct security advisories / CVEs specifically affecting prisma@5.22.0 or @prisma/client@5.22.0 in Snyk’s vulnerability database. [1][2]
The Prisma GitHub repo’s published security advisory is GHSA-pxcc-hj8w-fmm7 (CVE-2021-21414), a command-injection issue in @prisma/sdk affecting versions < 2.20.0 (patched in 2.20.0), which is far older than Prisma 5.22.0. [3]
If you’re auditing a real project pinned to Prisma 5.22.0, you should still check transitive dependency advisories via npm audit/pnpm audit (Snyk’s “no direct vulnerabilities” does not rule out indirect ones). [1][2]
Sources: [1] prisma@5.22.0 (Snyk) • [2] @prisma/client@5.22.0 (Snyk) • [3] GHSA-pxcc-hj8w-fmm7 / CVE-2021-21414 (Prisma GitHub Security Advisory)
🌐 Web query:
next-auth beta Next.js 14 production use recommended
💡 Result:
Using next-auth@beta (Auth.js / NextAuth v5) with Next.js 14 can work in production, but it’s not the “recommended” low-risk choice because it’s still a beta line.
- What’s officially clear: v5 is installed via
npm install next-auth@beta, is “App Router-first”, and requires Next.js ≥ 14.0. [1] - Stability reality check: the stable line is still v4 (4.24.x), while v5 continues to ship as 5.0.0-beta.* (no stable 5.0.0 in the public release stream shown). [2]
- Practical guidance from community usage: people do run v5 beta in production successfully, but commonly advise v4 for maximum stability and v5 only if you need v5/App Router-first features, with caution. [3]
- Why caution: there are ongoing v5-specific issues/edge-cases reported (example: middleware/react/Next version interactions). [4]
Recommendation (production):
- If you want the safest/most conventional production setup on Next.js 14: use
next-auth@4.24.x. [2] - If you specifically need v5 features (notably the new
auth()-centric APIs and App Router-first approach): you can usenext-auth@beta, but treat it as higher risk (pin an exact beta version, add strong test coverage for auth flows, and watch upstream issues). [1][3]
Sources:
[1] Auth.js “Migrating to v5” (shows next-auth@beta, App Router-first, Next.js ≥14) (authjs.cn)
[2] next-auth release stream showing v4 stable vs v5 beta versions (releasealert.dev)
[3] Next.js Discord forum thread discussing v5 beta production suitability (nextjs-forum.com)
[4] next-auth GitHub issue example (v5 beta + Next version/middleware issues) (github.com)
Citations:
- 1: https://www.authjs.cn/getting-started/migrating-to-v5?utm_source=openai
- 2: https://releasealert.dev/npm/_/next-auth?utm_source=openai
- 3: https://nextjs-forum.com/post/1350340142421905470?utm_source=openai
- 4: v5 Beta does not work with Next 15 RC (React 19 RC) - Middleware issue nextauthjs/next-auth#11006
Consider upgrading to stable next-auth@4.24.13 or confirm v5-beta is intentional.
Line 17 pins next-auth@5.0.0-beta.30, which has no stable v5 release as of February 2026; the stable line remains v4 (4.24.13). If you need v5-specific features (App Router-first APIs), the beta is functional but carries higher risk and fewer production examples. Recommend either upgrading to v4 for maximum stability, or explicitly documenting the v5 choice with test coverage for auth flows.
No security advisories found for bcryptjs@3.0.3 or Prisma@5.22.0 directly; note that bcryptjs truncates passwords at 72 bytes (inherent to bcrypt), so enforce a max-length validation in your password handling.
Also applies to: 22-26
🤖 Prompt for AI Agents
In `@web/package.json` around lines 13 - 18, Your package.json currently pins
next-auth to "5.0.0-beta.30" (next-auth) which is unstable; either replace that
dependency with the stable v4 ("4.24.13") or add an explicit rationale in
project docs and add CI tests for auth flows to cover App Router behaviors; also
add a max-password-length validation wherever passwords are handled (e.g.,
registerUser / validatePassword or auth controller) because bcryptjs truncates
at 72 bytes, and confirm `@prisma/client` ("5.22.0") is intentionally chosen or
update if needed. Ensure changes include updating package.json dependency,
adding a short README note about the v5-beta choice if kept, and adding
unit/integration tests for authentication paths and a password-length check in
the password-handling functions.
| const body = await req.json(); | ||
| const { email, password, name } = body; | ||
|
|
||
| if (!email || !password) { | ||
| return NextResponse.json( | ||
| { error: "Email and password are required" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Check if user already exists | ||
| const existingUser = await prisma.user.findUnique({ | ||
| where: { email }, | ||
| }); | ||
|
|
||
| if (existingUser) { | ||
| return NextResponse.json( | ||
| { error: "User with this email already exists" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Hash the password | ||
| const hashedPassword = await bcrypt.hash(password, 10); | ||
|
|
||
| // Create the user | ||
| const user = await prisma.user.create({ | ||
| data: { | ||
| email, | ||
| name, | ||
| hashedPassword, | ||
| }, | ||
| }); | ||
|
|
||
| // Return user without password | ||
| return NextResponse.json( | ||
| { | ||
| user: { | ||
| id: user.id, | ||
| email: user.email, | ||
| name: user.name, | ||
| }, | ||
| }, | ||
| { status: 201 } | ||
| ); | ||
| } catch (error) { | ||
| console.error("Signup error:", error); | ||
| return NextResponse.json( | ||
| { error: "Internal server error" }, | ||
| { status: 500 } | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "route.ts" -path "*/auth/signup/*" | head -20Repository: stepandel/agent-army
Length of output: 102
🏁 Script executed:
git ls-files | grep -E "signup.*route\.ts|route\.ts.*signup"Repository: stepandel/agent-army
Length of output: 100
🏁 Script executed:
find . -type f -name "route.ts" | xargs grep -l "signup\|POST.*email.*password" 2>/dev/null | head -10Repository: stepandel/agent-army
Length of output: 46
🏁 Script executed:
cat -n web/src/app/api/auth/signup/route.tsRepository: stepandel/agent-army
Length of output: 1801
Harden input validation and handle concurrent signup race (P2002).
Line 8 lacks type safety—non-string values like email: 123 or password: {} pass the truthy check but will crash downstream. Lines 18–27 expose a TOCTOU race: if two requests attempt signup with the same email, both may pass the existence check before the first completes, causing Prisma to throw a P2002 unique-constraint error. The catch block (lines 52–57) doesn't distinguish P2002 from other errors, returning 500 instead of 409. Add explicit type checks, email normalization, and catch P2002 with a 409 response.
🛠️ Suggested fix
-import { prisma } from "@/lib/prisma";
+import { prisma } from "@/lib/prisma";
+import { Prisma } from "@prisma/client";
export async function POST(req: NextRequest) {
try {
const body = await req.json();
- const { email, password, name } = body;
+ const { email, password, name } = body ?? {};
+
+ if (typeof email !== "string" || typeof password !== "string") {
+ return NextResponse.json(
+ { error: "Email and password are required" },
+ { status: 400 }
+ );
+ }
+
+ const normalizedEmail = email.trim().toLowerCase();
+ const normalizedName = typeof name === "string" ? name.trim() : undefined;
+ if (!normalizedEmail || password.length < 6) {
+ return NextResponse.json(
+ { error: "Email and password are required" },
+ { status: 400 }
+ );
+ }
// Check if user already exists
const existingUser = await prisma.user.findUnique({
- where: { email },
+ where: { email: normalizedEmail },
});
if (existingUser) {
return NextResponse.json(
- { error: "User with this email already exists" },
- { status: 400 }
+ { error: "Unable to create account" },
+ { status: 409 }
);
}
// Hash the password
const hashedPassword = await bcrypt.hash(password, 10);
// Create the user
const user = await prisma.user.create({
data: {
- email,
- name,
+ email: normalizedEmail,
+ name: normalizedName,
hashedPassword,
},
});
@@
- } catch (error) {
+ } catch (error) {
+ if (
+ error instanceof Prisma.PrismaClientKnownRequestError &&
+ error.code === "P2002"
+ ) {
+ return NextResponse.json(
+ { error: "Unable to create account" },
+ { status: 409 }
+ );
+ }
console.error("Signup error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}🤖 Prompt for AI Agents
In `@web/src/app/api/auth/signup/route.ts` around lines 7 - 57, Validate that
email and password are strings (e.g., typeof email === "string" && typeof
password === "string"), trim and normalize the email to lowercase before use,
and only proceed to bcrypt.hash after those checks; keep the
prisma.user.findUnique existence check but wrap prisma.user.create in a
try/catch that specifically detects a Prisma.PrismaClientKnownRequestError with
code "P2002" (unique constraint) and return NextResponse.json({ error: "User
with this email already exists" }, { status: 409 }) for that case, while
rethrowing or returning a 500 for other errors—update the final catch block to
discriminate P2002 vs generic errors and return 409 when encountered.
| agentCard: (selected: boolean) => ({ | ||
| padding: 16, borderRadius: 8, marginBottom: 8, cursor: "pointer", | ||
| border: `2px solid ${selected ? "#3b82f6" : "#e5e7eb"}`, | ||
| background: selected ? "#eff6ff" : "#fff", | ||
| }), |
There was a problem hiding this comment.
Make agent selection keyboard-accessible.
The clickable <div> plus readOnly checkbox prevents keyboard toggling. Use a <label> with an onChange handler (or add role, tabIndex, and key handlers) so keyboard users can select agents.
♿ Suggested fix
- agentCard: (selected: boolean) => ({
+ agentCard: (selected: boolean) => ({
padding: 16, borderRadius: 8, marginBottom: 8, cursor: "pointer",
border: `2px solid ${selected ? "#3b82f6" : "#e5e7eb"}`,
background: selected ? "#eff6ff" : "#fff",
+ display: "block",
}),- <div
- key={key}
- style={s.agentCard(selected)}
- onClick={() => toggleAgent(key)}
- >
+ <label
+ key={key}
+ style={s.agentCard(selected)}
+ >
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<input
type="checkbox"
checked={selected}
- readOnly
+ onChange={() => toggleAgent(key)}
style={{ width: 18, height: 18 }}
/>
<div>
<div style={{ fontWeight: 600, fontSize: 15 }}>
{preset.displayName}{" "}
<span style={{ fontWeight: 400, color: "#6b7280" }}>
({preset.role})
</span>
</div>
<div style={{ fontSize: 13, color: "#6b7280", marginTop: 2 }}>
{preset.description}
</div>
</div>
</div>
- </div>
+ </label>Also applies to: 305-329
🤖 Prompt for AI Agents
In `@web/src/app/dashboard/new/page.tsx` around lines 68 - 72, The agent selection
UI uses a clickable <div> with a readOnly checkbox which blocks keyboard users;
update the selection markup so it is keyboard-accessible—either wrap the
checkbox and content in a <label> and wire the checkbox's onChange to the
existing selection handler (remove readOnly), or make the clickable container
interactive by adding role="button", tabIndex={0}, and an onKeyDown handler that
toggles selection on Enter/Space and calls the same selection function; ensure
the visual style referenced by agentCard remains unchanged and that the checkbox
input and the container call the same selection method so both mouse and
keyboard trigger selection.
| function validate(): string[] { | ||
| const errs: string[] = []; | ||
| if (step === 0) { | ||
| if (!state.stackName.trim()) errs.push("Stack name is required"); | ||
| if (!/^[a-z0-9-]+$/.test(state.stackName)) errs.push("Stack name: lowercase letters, numbers, hyphens only"); | ||
| } | ||
| if (step === 1) { | ||
| if (!state.region) errs.push("Region is required"); | ||
| if (!state.instanceType) errs.push("Instance type is required"); | ||
| } | ||
| if (step === 2) { | ||
| if (!state.ownerName.trim()) errs.push("Owner name is required"); | ||
| } | ||
| if (step === 3) { | ||
| if (state.selectedAgents.length === 0) errs.push("Select at least one agent"); | ||
| } | ||
| return errs; | ||
| } |
There was a problem hiding this comment.
Validate the trimmed stack name to avoid false errors.
Line 101–103 validates the raw string; leading/trailing spaces can produce a pattern error even when the trimmed value is valid, and you can also end up with duplicate errors.
✅ Suggested fix
function validate(): string[] {
const errs: string[] = [];
if (step === 0) {
- if (!state.stackName.trim()) errs.push("Stack name is required");
- if (!/^[a-z0-9-]+$/.test(state.stackName)) errs.push("Stack name: lowercase letters, numbers, hyphens only");
+ const stack = state.stackName.trim();
+ if (!stack) {
+ errs.push("Stack name is required");
+ } else if (!/^[a-z0-9-]+$/.test(stack)) {
+ errs.push("Stack name: lowercase letters, numbers, hyphens only");
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function validate(): string[] { | |
| const errs: string[] = []; | |
| if (step === 0) { | |
| if (!state.stackName.trim()) errs.push("Stack name is required"); | |
| if (!/^[a-z0-9-]+$/.test(state.stackName)) errs.push("Stack name: lowercase letters, numbers, hyphens only"); | |
| } | |
| if (step === 1) { | |
| if (!state.region) errs.push("Region is required"); | |
| if (!state.instanceType) errs.push("Instance type is required"); | |
| } | |
| if (step === 2) { | |
| if (!state.ownerName.trim()) errs.push("Owner name is required"); | |
| } | |
| if (step === 3) { | |
| if (state.selectedAgents.length === 0) errs.push("Select at least one agent"); | |
| } | |
| return errs; | |
| } | |
| function validate(): string[] { | |
| const errs: string[] = []; | |
| if (step === 0) { | |
| const stack = state.stackName.trim(); | |
| if (!stack) { | |
| errs.push("Stack name is required"); | |
| } else if (!/^[a-z0-9-]+$/.test(stack)) { | |
| errs.push("Stack name: lowercase letters, numbers, hyphens only"); | |
| } | |
| } | |
| if (step === 1) { | |
| if (!state.region) errs.push("Region is required"); | |
| if (!state.instanceType) errs.push("Instance type is required"); | |
| } | |
| if (step === 2) { | |
| if (!state.ownerName.trim()) errs.push("Owner name is required"); | |
| } | |
| if (step === 3) { | |
| if (state.selectedAgents.length === 0) errs.push("Select at least one agent"); | |
| } | |
| return errs; | |
| } |
🤖 Prompt for AI Agents
In `@web/src/app/dashboard/new/page.tsx` around lines 98 - 115, In validate(),
trim state.stackName once and use that trimmed value for both checks to avoid
false pattern errors and duplicates; e.g. const name = state.stackName.trim();
then if (step === 0) { if (!name) errs.push("Stack name is required"); else if
(!/^[a-z0-9-]+$/.test(name)) errs.push("Stack name: lowercase letters, numbers,
hyphens only"); } — update references to state.stackName accordingly so
validate() uses the trimmed name for validation.
| <a | ||
| href="/api/auth/signout" | ||
| style={{ | ||
| padding: "10px 20px", | ||
| backgroundColor: "#f44336", | ||
| color: "white", | ||
| textDecoration: "none", | ||
| borderRadius: "4px", | ||
| display: "inline-block", | ||
| }} | ||
| > | ||
| Logout |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "page.tsx" | grep dashboardRepository: stepandel/agent-army
Length of output: 133
🏁 Script executed:
find . -type f -name "package.json" | head -5Repository: stepandel/agent-army
Length of output: 116
🏁 Script executed:
find . -type f -name "*.config.*" | grep -E "(auth|next)" | head -10Repository: stepandel/agent-army
Length of output: 84
🏁 Script executed:
cat ./web/src/app/dashboard/page.tsxRepository: stepandel/agent-army
Length of output: 893
🏁 Script executed:
cat ./web/package.json | head -50Repository: stepandel/agent-army
Length of output: 732
🏁 Script executed:
find . -type f -name "auth.ts" -o -name "auth.js" -o -name "[auth]" | grep -v node_modulesRepository: stepandel/agent-army
Length of output: 85
🏁 Script executed:
cat ./web/src/lib/auth.tsRepository: stepandel/agent-army
Length of output: 1734
🏁 Script executed:
cat -n ./web/src/app/dashboard/page.tsx | sed -n '17,28p'Repository: stepandel/agent-army
Length of output: 460
🏁 Script executed:
cat ./web/src/app/dashboard/page.tsx | wc -lRepository: stepandel/agent-army
Length of output: 66
🏁 Script executed:
find . -type f -name "*.tsx" -o -name "*.ts" | xargs grep -l "signOut" | grep -v node_modulesRepository: stepandel/agent-army
Length of output: 85
🏁 Script executed:
find . -type f -path "*/app/api/auth/*" | head -20Repository: stepandel/agent-army
Length of output: 148
🏁 Script executed:
cat ./web/src/app/api/auth/[...nextauth]/route.tsRepository: stepandel/agent-army
Length of output: 142
🏁 Script executed:
web_search
NextAuth v5 beta /api/auth/signout GET vs POST behaviorRepository: stepandel/agent-army
Length of output: 161
🌐 Web query:
NextAuth v5 /api/auth/signout endpoint GET vs POST CSRF protection
💡 Result:
In NextAuth/Auth.js v5, /api/auth/signout has two different behaviors:
- GET
/api/auth/signout: renders the sign-out page (a confirmation UI). It is not the state-changing logout action. [1] - POST
/api/auth/signout: actually signs the user out (clears the session cookie; deletes DB session if used). Because it changes auth state, it is CSRF-protected. [1]
Why POST needs CSRF (and why GET doesn’t log you out)
Auth.js uses the double-submit cookie approach: you must first obtain a CSRF token (which also sets a CSRF cookie), then send that token back in the POST. The CSRF token must be submitted as csrfToken with all POST submissions to auth endpoints. [2]
What to do in practice
- Preferred: call the client helper
signOut()(it handles CSRF for you). - If you must call the endpoint yourself:
GET /api/auth/csrf(store cookies)POST /api/auth/signoutincludingcsrfToken(and include cookies viacredentials: "include"). If you don’t, you’ll typically get redirected to a CSRF flow / signout page and the session may remain. [3]
Sources: Auth.js endpoint semantics for signout GET vs POST [1], CSRF token/cookie requirement (“double submit cookie method”) [2], example of manual POST signout requiring CSRF token and avoiding unwanted redirects [3].
[1] (authjs.cn)
[2] (github.com)
[3] (stackoverflow.com)
Citations:
- 1: https://www.authjs.cn/reference/core/types?utm_source=openai
- 2: Is it possible to sign in a user with a REST API call when using Credentials provider? nextauthjs/next-auth#6011
- 3: https://stackoverflow.com/questions/73392534/how-to-sign-out-using-the-post-api-auth-signout-endpoint-with-next-auth?utm_source=openai
🌐 Web query:
NextAuth 5.0.0-beta.30 signOut function next-auth/react logout
💡 Result:
In NextAuth/Auth.js v5 (e.g. next-auth@5.0.0-beta.30), “logout” on the client is done with signOut from next-auth/react.
"use client"
import { signOut } from "next-auth/react"
export function LogoutButton() {
return (
<button onClick={() => signOut({ redirectTo: "/login" })}>
Logout
</button>
)
}Notes:
- Make sure you call it:
signOut()(notsignOut). [1] - In v5,
callbackUrlis deprecated; useredirectToinstead. [2] - To sign out without a navigation:
(then do your own
await signOut({ redirect: false })
router.push(...)if you want). [2]
Sources: [1] [2]
Use signOut() client function instead of GET link to properly log out.
Line 18 uses a GET link to /api/auth/signout. In NextAuth v5, GET only renders a confirmation page and doesn't actually sign out; only POST (with CSRF protection) performs the logout. The correct approach is a client-side button that calls signOut() from next-auth/react.
Create a logout button component:
Suggested approach
// web/src/components/logout-button.tsx
"use client";
import { signOut } from "next-auth/react";
export function LogoutButton() {
return (
<button
onClick={() => signOut({ redirectTo: "/login" })}
style={{
padding: "10px 20px",
backgroundColor: "#f44336",
color: "white",
border: "none",
borderRadius: "4px",
}}
>
Logout
</button>
);
}- <a
- href="/api/auth/signout"
- style={{
- padding: "10px 20px",
- backgroundColor: "#f44336",
- color: "white",
- textDecoration: "none",
- borderRadius: "4px",
- display: "inline-block",
- }}
- >
- Logout
- </a>
+ <LogoutButton />🤖 Prompt for AI Agents
In `@web/src/app/dashboard/page.tsx` around lines 17 - 28, Replace the GET anchor
logout link with a client-side logout button component that calls signOut from
next-auth/react: create a "use client" component named LogoutButton (e.g.,
export function LogoutButton) which imports signOut and invokes signOut({
redirectTo: "/login" }) in an onClick handler, apply the existing styles to a
<button> (remove href/anchor usage), and then import and render LogoutButton in
page.tsx instead of the anchor to ensure proper POST logout behavior in NextAuth
v5.
Summary
Adds a multi-step deployment wizard at
/dashboard/newthat replicates the CLI init wizard as a web form.Wizard Steps
Key Design Decisions
@agent-army/cliworkspace packageexportsfield tocli/package.jsonfor cross-workspace importsArmyManifestmatchingcli/types.tsAlso includes
Uncommitted auth files from AGE-47 that were part of the merged PR #38 but not tracked on the branch.
Ticket
Closes AGE-48
Summary by CodeRabbit
Release Notes
New Features
Chores