Skip to content

Stack CLI#1227

Merged
BilalG1 merged 23 commits intodevfrom
stack-cli2
Mar 9, 2026
Merged

Stack CLI#1227
BilalG1 merged 23 commits intodevfrom
stack-cli2

Conversation

@BilalG1
Copy link
Copy Markdown
Collaborator

@BilalG1 BilalG1 commented Feb 27, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Stack CLI with authentication (login/logout) commands.
    • Added project management commands to list and create projects.
    • Added configuration management to pull and push project settings.
    • Added code execution capability to run JavaScript expressions.
    • Added initialization command for Stack Auth setup.
  • Tests

    • Added comprehensive end-to-end test suite for CLI functionality.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-backend Ready Ready Preview, Comment Mar 9, 2026 7:27pm
stack-dashboard Ready Ready Preview, Comment Mar 9, 2026 7:27pm
stack-demo Ready Ready Preview, Comment Mar 9, 2026 7:27pm
stack-docs Ready Ready Preview, Comment Mar 9, 2026 7:27pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces a complete Stack CLI implementation with authentication, project management, code execution, and configuration management capabilities, plus comprehensive end-to-end tests and build configuration.

Changes

Cohort / File(s) Summary
Project Setup
packages/stack-cli/package.json, packages/stack-cli/tsconfig.json, packages/stack-cli/tsdown.config.ts, packages/stack-cli/.eslintrc.cjs
Build and package configuration for the Stack CLI, including ESM module setup, TypeScript compilation, tsdown bundler config, and ESLint defaults.
Authentication & Configuration
packages/stack-cli/src/lib/auth.ts, packages/stack-cli/src/lib/config.ts
Authentication resolution from environment variables and JSON-based config files (~/.config/stack-auth/credentials.json), including URL/token/project ID determination and error handling.
Error & Utility Classes
packages/stack-cli/src/lib/errors.ts, packages/stack-cli/src/lib/interactive.ts, packages/stack-cli/src/lib/app.ts
Custom error types (CliError, AuthError), non-interactive environment detection, and internal app/user/project access utilities.
Login & Session Management
packages/stack-cli/src/commands/login.ts, packages/stack-cli/src/commands/logout.ts
Browser-based login flow with refresh token persistence and logout command to clear stored credentials.
Code Execution
packages/stack-cli/src/commands/exec.ts
JavaScript code execution against a StackServerApp context with error handling and JSON output formatting.
Configuration Management
packages/stack-cli/src/commands/config-file.ts
Config pull (export branch override to .js/.ts) and push (apply .js/.ts config file) subcommands.
Project Management & Init
packages/stack-cli/src/commands/project.ts, packages/stack-cli/src/commands/init.ts
Project listing/creation with JSON output support and delegation to @stackframe/init-stack external tool.
CLI Entry Point
packages/stack-cli/src/index.ts
Commander-based CLI program setup with global options, command registration, and error handling for AuthError and CliError.
End-to-End Tests
apps/e2e/tests/general/cli.test.ts
Comprehensive test suite covering help/version output, authentication states, project lifecycle, JavaScript execution, config flows, user management, error scenarios, and async handling (~348 lines).

Possibly related PRs

  • Config override CRUD #803: Introduces config pull/push commands that directly utilize project.getConfigOverride() and project.replaceConfigOverride() methods for branch config management.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hops through the CLI with glee,
Auth tokens tucked in config's pouch so snug,
Projects spin up, configs dance to and fro,
exec jumps high with code to sow,
A burrow of commands, built to go!

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author; the PR is missing required context about changes, objectives, and testing. Add a pull request description that outlines the main changes, motivation, and testing performed. Reference the CONTRIBUTING.md guidelines as specified in the template.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Stack CLI' is vague and generic, describing only the general category of changes without indicating the specific functionality or scope added. Provide a more specific title that clarifies the main change, such as 'Add Stack CLI with login, project, config, and exec commands' or similar.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch stack-cli2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 27, 2026

Greptile Summary

This PR adds a new CLI package (@stackframe/stack-cli) to the Stack Auth monorepo, providing developers with command-line tools to interact with their Stack Auth projects.

Major Changes:

  • Authentication commands: stack login and stack logout for browser-based authentication flow with secure token storage (0o600 permissions)
  • Project management: stack project list and stack project create for managing Stack Auth projects
  • JavaScript execution: stack exec command allowing developers to run arbitrary JavaScript with a pre-configured stackServerApp instance
  • Config management: stack config pull and stack config push for syncing branch configurations to/from local TypeScript/JavaScript files
  • Init delegation: stack init delegates to @stackframe/init-stack package

Security practices followed:

  • Uses JSON.stringify() when generating code in config-file.ts (lines 38-41) to prevent injection attacks
  • Uses execFileSync with array arguments in init.ts for safe command execution without shell injection
  • Stores refresh tokens with restrictive file permissions (0o600)
  • The exec command intentionally allows arbitrary JavaScript execution, which is expected behavior for a developer CLI tool

Test coverage:

  • Comprehensive E2E test suite covering all commands, error cases, and edge cases (358 lines of tests)

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • Well-structured implementation with proper security practices, comprehensive test coverage, and clear separation of concerns. All security guidelines are followed, including proper escaping when generating code and safe command execution patterns.
  • No files require special attention

Important Files Changed

Filename Overview
packages/stack-cli/src/commands/exec.ts Implements JavaScript execution with StackServerApp context using AsyncFunction constructor
packages/stack-cli/src/commands/config-file.ts Adds config pull/push commands with proper JSON.stringify() escaping and validation
packages/stack-cli/src/commands/init.ts Delegates to @stackframe/init-stack using execFileSync with array arguments (safe from injection)
packages/stack-cli/src/commands/login.ts Implements browser-based login flow and stores refresh token securely with 0o600 permissions
packages/stack-cli/src/commands/project.ts Implements project list and create commands with interactive prompts and JSON output support
packages/stack-cli/src/lib/config.ts Implements config file operations with proper file permissions (0o600) for sensitive data
apps/e2e/tests/general/cli.test.ts Comprehensive E2E tests covering all CLI commands and error cases

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    Start[User runs stack command] --> Parse[Commander.js parses args]
    Parse --> CheckAuth{Requires auth?}
    
    CheckAuth -->|No| Init[stack init]
    CheckAuth -->|Session auth| Session[stack login/logout/project]
    CheckAuth -->|Project auth| Project[stack exec/config]
    
    Init --> Delegate[Delegate to @stackframe/init-stack via execFileSync]
    
    Session --> ResolveSession[resolveSessionAuth]
    ResolveSession --> CheckToken{Has refresh token?}
    CheckToken -->|No| LoginFlow[stack login: Browser auth flow]
    CheckToken -->|Yes| GetUser[Get internal user]
    
    LoginFlow --> SaveToken[Save to ~/.stackrc with 0o600]
    SaveToken --> Success1[Command executes]
    GetUser --> Success1
    
    Project --> ResolveProject[resolveAuth]
    ResolveProject --> CheckPID{Has project ID?}
    CheckPID -->|No| Error1[Error: No project ID]
    CheckPID -->|Yes| ValidateProject[Validate project ownership]
    ValidateProject --> ExecOrConfig{Which command?}
    
    ExecOrConfig -->|exec| RunJS[Create AsyncFunction & execute JS]
    ExecOrConfig -->|config pull| PullConfig[Get config & generate .ts/.js file]
    ExecOrConfig -->|config push| PushConfig[Load config via jiti & push to API]
    
    PullConfig --> JSONStringify[Use JSON.stringify for safe code generation]
    RunJS --> Output[Output result as JSON]
    JSONStringify --> Output
    PushConfig --> Output
Loading

Last reviewed commit: 35770d7

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

21 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (2)
apps/e2e/tests/general/cli.test.ts (2)

163-171: Consider typing the parsed JSON or adding a comment for any.

Line 168 uses any for the project type in the callback. Per coding guidelines, any usage should be explained or avoided where possible.

Proposed fix
    const projects = JSON.parse(stdout);
-    const found = projects.find((p: any) => p.id === createdProjectId);
+    // JSON.parse returns unknown structure; we only need to check the id property
+    const found = projects.find((p: { id: string }) => p.id === createdProjectId);
    expect(found).toBeDefined();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/general/cli.test.ts` around lines 163 - 171, The test parses
stdout into projects and currently uses an untyped callback with (p: any);
update the code to avoid raw any by either typing the parsed JSON (e.g. declare
projects as Project[] or a local type/interface matching expected fields) or add
an inline explanatory comment justifying any usage; locate the test that calls
runCli and assigns createdProjectId, then change the line const projects =
JSON.parse(stdout); and the find callback (p: any) => ... to use the proper
Project type (or a TypeScript cast with a short comment) so the find uses (p:
Project) => p.id === createdProjectId and found.displayName is correctly typed.

20-26: Add type annotation or comment for any usage.

The as any cast on line 24 bypasses type checking. Per coding guidelines, any usage should include a comment explaining why it's necessary. The ExecException type from child_process already has a code property, so this could be typed more precisely.

Proposed fix
+import { execFile, ExecException } from "child_process";
-import { execFile } from "child_process";
...
    }, (error, stdout, stderr) => {
      resolve({
        stdout: stdout.toString(),
        stderr: stderr.toString(),
-        exitCode: error ? (error as any).code ?? 1 : 0,
+        exitCode: error ? (error as ExecException).code ?? 1 : 0,
      });
    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/general/cli.test.ts` around lines 20 - 26, The callback passed
to exec uses an `any` cast for `error`—replace this with the proper
`ExecException | null` type from `child_process` (or type the callback
parameters explicitly) and then use `error?.code ?? 1` when computing exitCode;
update the callback signature where resolve is called so TypeScript knows
`error` has a `code` property instead of using `(error as any)`. Ensure you
import `ExecException` if needed and remove the `as any` cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/stack-cli/src/commands/config-file.ts`:
- Line 73: The dynamic import using import(filePath) fails on Windows because
Node requires a file:// URL for absolute filesystem paths; update the code
around the import (where configModule is set from filePath) to convert filePath
to a file URL using pathToFileURL from the 'url' module (e.g., const fileUrl =
pathToFileURL(filePath).href) and then await import(fileUrl) so configModule
imports reliably across platforms.

In `@packages/stack-cli/src/commands/exec.ts`:
- Around line 32-37: The try-catch-all around formatExecApiHelp(), the
AsyncFunction creation, and user code execution must be narrowed: remove the
try/catch around formatExecApiHelp() since it is synchronous and shouldn’t be
wrapped, change the catch around the new AsyncFunction(...) creation to only
handle SyntaxError (and wrap that into CliError with the existing help message),
and for the block that executes user code (the function invocation where
user-provided code runs) only handle documented expected error types and rethrow
any unknown/other errors rather than blanket-wrapping; update the catch clauses
in exec.ts to reference formatExecApiHelp(), the AsyncFunction constructor, and
the user-code execution site by name so only specific errors are caught and
others are rethrown.

In `@packages/stack-cli/src/commands/init.ts`:
- Around line 10-14: Remove the unnecessary type cast on cmd.args inside the
.action callback: change the declaration using "const args = cmd.args as
string[]" to just use cmd.args (e.g., const args = cmd.args) so you rely on
Commander v13's built-in typing; ensure the rest of the call to
execFileSync("@stackframe/init-stack@latest", ...args) remains the same and
references execFileSync and the .action handler.

In `@packages/stack-cli/src/commands/project.ts`:
- Around line 57-66: The code only trims and validates displayName for the
interactive prompt path, letting opts.displayName = "   " pass through; fix by
normalizing and validating displayName in one place: after assigning let
displayName = opts.displayName; immediately do displayName = typeof displayName
=== "string" ? displayName.trim() : displayName and then explicitly check if
displayName == null or displayName.length === 0 and throw new CliError("Display
name cannot be empty."); also apply the same explicit trim-and-validate logic to
the other occurrence that sets/uses displayName before calling createProject and
keep references to isNonInteractiveEnv(), prompt(), CliError, and createProject
to locate the relevant code paths.

In `@packages/stack-cli/src/lib/app.ts`:
- Line 23: Remove the redundant type assertion on the returned user in the
function that returns the current user: the app is typed as StackClientApp<true,
"internal"> and its overloaded getUser() already returns
ProjectCurrentUser<"internal"> (alias CurrentInternalUser), so replace the
forced cast `as CurrentInternalUser` with a plain `return user;` (i.e., remove
the cast) in the return statement to let TypeScript infer the correct type;
update the return in the function that calls getUser() accordingly.

In `@packages/stack-cli/src/lib/config.ts`:
- Around line 9-14: The readConfigFileRaw function currently swallows all errors
from fs.readFileSync; change the catch to inspect the thrown error (from
fs.readFileSync(CONFIG_PATH, "utf-8")) and only return [] when err.code ===
'ENOENT' (missing file); for any other error (permission denied, corruption,
etc.) rethrow the error so callers fail fast. Ensure the thrown error is
properly typed/guarded when checking err.code and reference the existing
readConfigFileRaw function and CONFIG_PATH in your change.
- Line 57: After writing the config/token file with fs.writeFileSync (e.g., the
call that writes CONFIG_PATH using result.join("\n") and the other write at the
second occurrence), call fs.chmodSync(CONFIG_PATH, 0o600) immediately after each
fs.writeFileSync so the file permissions are enforced on both newly created and
existing files; update the code paths that write the token/config (referencing
CONFIG_PATH and the functions or blocks that perform the writes) to always
invoke fs.chmodSync(CONFIG_PATH, 0o600) after the write.

---

Nitpick comments:
In `@apps/e2e/tests/general/cli.test.ts`:
- Around line 163-171: The test parses stdout into projects and currently uses
an untyped callback with (p: any); update the code to avoid raw any by either
typing the parsed JSON (e.g. declare projects as Project[] or a local
type/interface matching expected fields) or add an inline explanatory comment
justifying any usage; locate the test that calls runCli and assigns
createdProjectId, then change the line const projects = JSON.parse(stdout); and
the find callback (p: any) => ... to use the proper Project type (or a
TypeScript cast with a short comment) so the find uses (p: Project) => p.id ===
createdProjectId and found.displayName is correctly typed.
- Around line 20-26: The callback passed to exec uses an `any` cast for
`error`—replace this with the proper `ExecException | null` type from
`child_process` (or type the callback parameters explicitly) and then use
`error?.code ?? 1` when computing exitCode; update the callback signature where
resolve is called so TypeScript knows `error` has a `code` property instead of
using `(error as any)`. Ensure you import `ExecException` if needed and remove
the `as any` cast.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d86512 and 35770d7.

⛔ Files ignored due to path filters (2)
  • packages/stack-cli/src/generated/exec-api-metadata.json is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • apps/e2e/tests/general/cli.test.ts
  • packages/stack-cli/.eslintrc.cjs
  • packages/stack-cli/package.json
  • packages/stack-cli/scripts/generate-exec-api-metadata.ts
  • packages/stack-cli/src/commands/config-file.ts
  • packages/stack-cli/src/commands/exec.ts
  • packages/stack-cli/src/commands/init.ts
  • packages/stack-cli/src/commands/login.ts
  • packages/stack-cli/src/commands/logout.ts
  • packages/stack-cli/src/commands/project.ts
  • packages/stack-cli/src/index.ts
  • packages/stack-cli/src/lib/app.ts
  • packages/stack-cli/src/lib/auth.ts
  • packages/stack-cli/src/lib/config.ts
  • packages/stack-cli/src/lib/errors.ts
  • packages/stack-cli/src/lib/exec-api.ts
  • packages/stack-cli/src/lib/interactive.ts
  • packages/stack-cli/tsconfig.json
  • packages/stack-cli/tsup.config.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/stack-cli/src/lib/exec-api.ts`:
- Around line 33-36: formatExecApiHelp currently only lists
metadata.stackServerApp, omitting metadata.stackClientApp from the help output;
update formatExecApiHelp to include both API groups by adding a section for
metadata.stackClientApp (e.g. "StackClientApp methods") in addition to the
existing "StackServerApp methods" so the returned lines include both
metadata.stackClientApp and metadata.stackServerApp before joining and returning
the string.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35770d7 and 17ff4ed.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • packages/stack-cli/src/lib/exec-api.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/e2e/tests/general/cli.test.ts (2)

66-66: Use structured URL construction for the session endpoint.

Avoid raw interpolation for URLs here; use URL/urlString construction for consistency and safer endpoint composition.

Proposed fix
-    const sessionRes = await niceFetch(`${STACK_BACKEND_BASE_URL}/api/v1/auth/sessions`, {
+    const sessionsUrl = new URL("/api/v1/auth/sessions", STACK_BACKEND_BASE_URL);
+    const sessionRes = await niceFetch(sessionsUrl, {

As per coding guidelines, "Use urlString`` or encodeURIComponent()` instead of normal string interpolation for URLs for consistency."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/general/cli.test.ts` at line 66, The call that builds the
sessions endpoint using string interpolation (the niceFetch invocation that
assigns sessionRes with `${STACK_BACKEND_BASE_URL}/api/v1/auth/sessions`) should
be changed to construct the URL with the URL API (or the project's urlString
helper) instead of raw interpolation; replace the interpolated string with new
URL('/api/v1/auth/sessions', STACK_BACKEND_BASE_URL).toString() (or equivalent
urlString call) so the STACK_BACKEND_BASE_URL and the path are safely composed
and encoded before passing to niceFetch.

167-170: Remove any from project lookup assertions.

(p: any) bypasses type checks in a key verification path. Prefer a typed parsed value (or runtime guard) so regressions are caught by TS.

Proposed fix
-    const projects = JSON.parse(stdout);
-    const found = projects.find((p: any) => p.id === createdProjectId);
+    const projects: Array<{ id: string; displayName: string }> = JSON.parse(stdout);
+    const found = projects.find((p) => p.id === createdProjectId);

As per coding guidelines, "Try to avoid the any type" and "Do NOT use as/any/type casts..."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/general/cli.test.ts` around lines 167 - 170, The test
currently uses an untyped callback `(p: any)` when searching parsed JSON
(variables: stdout, projects, createdProjectId), which bypasses TypeScript
checks; change to parse stdout into a typed array (e.g., const projects:
Project[] = JSON.parse(stdout) or use a runtime guard) and use a properly typed
predicate in the find call (e.g., (p: Project) => p.id === createdProjectId), or
implement a small type-guard function isProject(obj): obj is Project and use it
before asserting found.displayName; ensure you reference the existing Project
interface/type (or declare one in the test) instead of using any.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/e2e/tests/general/cli.test.ts`:
- Around line 82-86: Validate the session response body shape before storing
refreshToken: check sessionRes.body.refresh_token exists and is a non-empty
string (and optionally matches expected format) and throw a descriptive error
including the full body if validation fails; replace the direct assignment
refreshToken = sessionRes.body.refresh_token with an explicit assertion/guard
that fails setup immediately when the token is missing or invalid, referencing
sessionRes and refreshToken to locate the change.
- Around line 12-25: The runCli helper currently casts the execFile callback
error to any to produce exitCode; remove that cast and explicitly normalize
exitCode to a number: inside the execFile callback (in runCli) check if error is
null => exitCode = 0; otherwise inspect (error as NodeJS.ErrnoException).code
but without using any casts — if typeof error.code === "number" use that, if
it's a string (e.g. "ENOENT") map it to a numeric fallback (e.g. 1) or handle
known strings specially, and default to 1; return exitCode as a number (never a
string) and keep stderr/stdout as before.

---

Nitpick comments:
In `@apps/e2e/tests/general/cli.test.ts`:
- Line 66: The call that builds the sessions endpoint using string interpolation
(the niceFetch invocation that assigns sessionRes with
`${STACK_BACKEND_BASE_URL}/api/v1/auth/sessions`) should be changed to construct
the URL with the URL API (or the project's urlString helper) instead of raw
interpolation; replace the interpolated string with new
URL('/api/v1/auth/sessions', STACK_BACKEND_BASE_URL).toString() (or equivalent
urlString call) so the STACK_BACKEND_BASE_URL and the path are safely composed
and encoded before passing to niceFetch.
- Around line 167-170: The test currently uses an untyped callback `(p: any)`
when searching parsed JSON (variables: stdout, projects, createdProjectId),
which bypasses TypeScript checks; change to parse stdout into a typed array
(e.g., const projects: Project[] = JSON.parse(stdout) or use a runtime guard)
and use a properly typed predicate in the find call (e.g., (p: Project) => p.id
=== createdProjectId), or implement a small type-guard function isProject(obj):
obj is Project and use it before asserting found.displayName; ensure you
reference the existing Project interface/type (or declare one in the test)
instead of using any.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17ff4ed and ac45ada.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • apps/e2e/tests/general/cli.test.ts

@BilalG1 BilalG1 assigned N2D4 and unassigned BilalG1 Feb 27, 2026
@BilalG1 BilalG1 requested a review from N2D4 February 27, 2026 19:48
@github-actions github-actions bot assigned BilalG1 and unassigned N2D4 Mar 2, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
apps/e2e/tests/general/cli.test.ts (2)

82-86: ⚠️ Potential issue | 🟠 Major

Assert session response body shape before assigning refreshToken.

A 200 status alone does not guarantee refresh_token exists and is valid.

Proposed fix
     if (sessionRes.status !== 200) {
       throw new Error(`Failed to create session: ${sessionRes.status} ${JSON.stringify(sessionRes.body)}`);
     }
-    refreshToken = sessionRes.body.refresh_token;
+    if (
+      sessionRes.body == null ||
+      typeof sessionRes.body !== "object" ||
+      !("refresh_token" in sessionRes.body) ||
+      typeof sessionRes.body.refresh_token !== "string" ||
+      sessionRes.body.refresh_token.length === 0
+    ) {
+      throw new Error(`Session response missing refresh_token: ${JSON.stringify(sessionRes.body)}`);
+    }
+    refreshToken = sessionRes.body.refresh_token;

As per coding guidelines, "Any assumption you make should either be validated..." and "Fail early, fail loud."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/general/cli.test.ts` around lines 82 - 86, The test assumes
sessionRes.body.refresh_token exists after a 200 but doesn't validate the
response shape; before assigning refreshToken, explicitly assert that
sessionRes.body is an object and contains a non-empty string refresh_token
(e.g., check typeof sessionRes.body.refresh_token === 'string' &&
sessionRes.body.refresh_token.length > 0) and throw a clear Error including
sessionRes.status and JSON.stringify(sessionRes.body) if the check fails; update
the assignment site where refreshToken is set from sessionRes.body.refresh_token
so it only occurs after this validation.

20-25: ⚠️ Potential issue | 🟠 Major

Remove any cast in runCli and normalize exitCode explicitly.

The cast bypasses type safety in a core test helper and can hide malformed process-error shapes.

Proposed fix
-    }, (error, stdout, stderr) => {
+    }, (error: NodeJS.ErrnoException | null, stdout, stderr) => {
+      const exitCode =
+        error == null
+          ? 0
+          : typeof error.code === "number"
+            ? error.code
+            : 1;
       resolve({
         stdout: stdout.toString(),
         stderr: stderr.toString(),
-        exitCode: error ? (error as any).code ?? 1 : 0,
+        exitCode,
       });
     });

As per coding guidelines, "Try to avoid the any type...".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/general/cli.test.ts` around lines 20 - 25, The test helper
that resolves spawn results currently uses an unsafe any cast to extract
exitCode; update the resolution in runCli (the callback that receives error,
stdout, stderr) to remove the any cast and normalize exitCode via a proper
type-guard: inspect error (typed as unknown or Error | null), check for presence
of a numeric code (e.g., (error as NodeJS.ErrnoException).code) with typeof
checks, coerce string codes to numbers when appropriate (parseInt) and fall back
to 1 when absent, then return the normalized number along with stdout/stderr.
packages/stack-cli/src/commands/exec.ts (1)

36-47: ⚠️ Potential issue | 🟠 Major

Narrow broad catches in exec compile/execute flow and avoid implicit any-typed control flow.

Both catch blocks are catch-all wrappers; the compile path should only wrap syntax errors, and non-expected runtime throwables should be rethrown.

Proposed fix
-      let fn;
+      let fn: (stackServerApp: unknown) => Promise<unknown>;
       try {
         fn = new AsyncFunction("stackServerApp", javascript);
       } catch (err: unknown) {
-        throw new CliError(`Syntax error in exec code: ${getErrorMessage(err)}`);
+        if (err instanceof SyntaxError) {
+          throw new CliError(`Syntax error in exec code: ${getErrorMessage(err)}`);
+        }
+        throw err;
       }
-      let result;
+      let result: unknown;
       try {
         result = await fn(project.app);
       } catch (err: unknown) {
-        throw new CliError(`Exec error: ${getErrorMessage(err)}`);
+        if (err instanceof Error || typeof err === "string" || (typeof err === "object" && err !== null)) {
+          throw new CliError(`Exec error: ${getErrorMessage(err)}`);
+        }
+        throw err;
       }

Based on learnings, for **/*.{ts,tsx} you should "NEVER try-catch-all" and "Code defensively. Prefer ?? throwErr(...) over non-null assertions with good error messages explicitly stating the assumption."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stack-cli/src/commands/exec.ts` around lines 36 - 47, The
compile/execute try-catches are too broad: when constructing
AsyncFunction("stackServerApp", javascript) only catch and wrap actual syntax
errors (use err: unknown and check instanceof SyntaxError, otherwise rethrow),
and when invoking fn(project.app) only catch expected runtime errors you intend
to wrap (use err: unknown, narrow via instanceof or custom type guards,
otherwise rethrow); replace any implicit assumptions with explicit checks or a
small helper like throwErr(...) so you never swallow unexpected
throwables—reference the AsyncFunction creation block, the two catch clauses
around fn = new AsyncFunction(...) and result = await fn(project.app),
getErrorMessage, and CliError when implementing these narrower catches.
packages/stack-cli/src/lib/config.ts (1)

9-14: ⚠️ Potential issue | 🟠 Major

Do not swallow all config read/parse failures; only treat missing file as empty and validate JSON shape.

Current behavior hides permission/corruption issues and silently continues with {}.

Proposed fix
+function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
+  return typeof error === "object" && error !== null && "code" in error;
+}
+
 function readConfigJson(): Record<string, string> {
+  let raw: string;
   try {
-    return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
-  } catch {
-    return {};
+    raw = fs.readFileSync(CONFIG_PATH, "utf-8");
+  } catch (error: unknown) {
+    if (isErrnoException(error) && error.code === "ENOENT") {
+      return {};
+    }
+    throw error;
+  }
+
+  const parsed: unknown = JSON.parse(raw);
+  if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
+    throw new Error(`Invalid config JSON in ${CONFIG_PATH}: expected an object`);
+  }
+
+  const normalized: Record<string, string> = {};
+  for (const [key, value] of Object.entries(parsed)) {
+    if (typeof value !== "string") {
+      throw new Error(`Invalid config value for '${key}' in ${CONFIG_PATH}: expected string`);
+    }
+    normalized[key] = value;
   }
+  return normalized;
 }

As per coding guidelines, "NEVER try-catch-all", "Fail early, fail loud", and "NEVER silently use fallback values when type errors occur."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stack-cli/src/lib/config.ts` around lines 9 - 14, The readConfigJson
function currently swallows all errors; change it to only treat a missing file
(fs.readFileSync error with code === 'ENOENT') as returning {} and rethrow any
other fs errors; parse JSON separately and rethrow JSON.parse errors; finally
validate the parsed value is an object mapping string->string (throw a TypeError
if not) so the function still returns Record<string,string> when valid.
Reference function: readConfigJson, constants: CONFIG_PATH, calls:
fs.readFileSync and JSON.parse.
🧹 Nitpick comments (1)
apps/e2e/tests/general/cli.test.ts (1)

167-170: Avoid any in project lookup; validate parsed output shape first.

This keeps failures explicit when CLI output changes unexpectedly.

Proposed fix
     const projects = JSON.parse(stdout);
-    const found = projects.find((p: any) => p.id === createdProjectId);
+    if (!Array.isArray(projects)) {
+      throw new Error(`Expected projects array, got: ${JSON.stringify(projects)}`);
+    }
+    const found = projects.find(
+      (p): p is { id: string; displayName: string } =>
+        p != null &&
+        typeof p === "object" &&
+        "id" in p &&
+        "displayName" in p &&
+        typeof p.id === "string" &&
+        typeof p.displayName === "string" &&
+        p.id === createdProjectId,
+    );

As per coding guidelines, "Try to avoid the any type..." and "Fail early, fail loud."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/general/cli.test.ts` around lines 167 - 170, The test
currently parses CLI JSON into an untyped value and uses (p: any) in the lookup;
change it to parse into unknown, validate the shape (ensure parsed value is an
array and each item has id and displayName) before casting to a typed Project[],
then use that Project[] in the find (referencing projects, createdProjectId, and
found) so the test fails early if the CLI output shape changes; implement a
runtime check (Array.isArray and typeof checks for id/displayName) or a small
type guard to avoid using any and then assert found.displayName === "CLI Test".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/stack-cli/package.json`:
- Around line 13-17: The package.json for the CLI (which provides the "bin"
entry and uses the "build": "tsdown" script) lacks an engines.node field; add an
"engines": { "node": ">=20.0.0" } entry to this package.json to enforce the
workspace Node minimum and ensure tsdown targets an appropriate runtime;
additionally, configure tsdown's target (either via a "tsdown" field in this
package.json or the project's tsdown config) to downlevel output for Node 20
(e.g., "target": "node20") so the built CLI emits compatible syntax.

In `@packages/stack-cli/src/commands/exec.ts`:
- Around line 13-17: The current getErrorMessage implementation swallows all
throwables when calling JSON.stringify(err); change the try/catch so it only
catches the expected stringify failure (e.g., TypeError from circular
structures) and falls back to String(err), but rethrow any other unexpected
errors — i.e., replace the broad catch with a conditional catch that handles
TypeError (or checks the error message contains "circular" if you prefer) and
throws the caught error for any other error types; update references in
getErrorMessage to ensure only expected stringify errors are handled.

---

Duplicate comments:
In `@apps/e2e/tests/general/cli.test.ts`:
- Around line 82-86: The test assumes sessionRes.body.refresh_token exists after
a 200 but doesn't validate the response shape; before assigning refreshToken,
explicitly assert that sessionRes.body is an object and contains a non-empty
string refresh_token (e.g., check typeof sessionRes.body.refresh_token ===
'string' && sessionRes.body.refresh_token.length > 0) and throw a clear Error
including sessionRes.status and JSON.stringify(sessionRes.body) if the check
fails; update the assignment site where refreshToken is set from
sessionRes.body.refresh_token so it only occurs after this validation.
- Around line 20-25: The test helper that resolves spawn results currently uses
an unsafe any cast to extract exitCode; update the resolution in runCli (the
callback that receives error, stdout, stderr) to remove the any cast and
normalize exitCode via a proper type-guard: inspect error (typed as unknown or
Error | null), check for presence of a numeric code (e.g., (error as
NodeJS.ErrnoException).code) with typeof checks, coerce string codes to numbers
when appropriate (parseInt) and fall back to 1 when absent, then return the
normalized number along with stdout/stderr.

In `@packages/stack-cli/src/commands/exec.ts`:
- Around line 36-47: The compile/execute try-catches are too broad: when
constructing AsyncFunction("stackServerApp", javascript) only catch and wrap
actual syntax errors (use err: unknown and check instanceof SyntaxError,
otherwise rethrow), and when invoking fn(project.app) only catch expected
runtime errors you intend to wrap (use err: unknown, narrow via instanceof or
custom type guards, otherwise rethrow); replace any implicit assumptions with
explicit checks or a small helper like throwErr(...) so you never swallow
unexpected throwables—reference the AsyncFunction creation block, the two catch
clauses around fn = new AsyncFunction(...) and result = await fn(project.app),
getErrorMessage, and CliError when implementing these narrower catches.

In `@packages/stack-cli/src/lib/config.ts`:
- Around line 9-14: The readConfigJson function currently swallows all errors;
change it to only treat a missing file (fs.readFileSync error with code ===
'ENOENT') as returning {} and rethrow any other fs errors; parse JSON separately
and rethrow JSON.parse errors; finally validate the parsed value is an object
mapping string->string (throw a TypeError if not) so the function still returns
Record<string,string> when valid. Reference function: readConfigJson, constants:
CONFIG_PATH, calls: fs.readFileSync and JSON.parse.

---

Nitpick comments:
In `@apps/e2e/tests/general/cli.test.ts`:
- Around line 167-170: The test currently parses CLI JSON into an untyped value
and uses (p: any) in the lookup; change it to parse into unknown, validate the
shape (ensure parsed value is an array and each item has id and displayName)
before casting to a typed Project[], then use that Project[] in the find
(referencing projects, createdProjectId, and found) so the test fails early if
the CLI output shape changes; implement a runtime check (Array.isArray and
typeof checks for id/displayName) or a small type guard to avoid using any and
then assert found.displayName === "CLI Test".

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd0db20 and 319f7ac.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • apps/e2e/tests/general/cli.test.ts
  • packages/stack-cli/package.json
  • packages/stack-cli/src/commands/exec.ts
  • packages/stack-cli/src/lib/config.ts

@BilalG1 BilalG1 merged commit 57149bd into dev Mar 9, 2026
26 of 28 checks passed
@BilalG1 BilalG1 deleted the stack-cli2 branch March 9, 2026 20:24
mantrakp04 pushed a commit that referenced this pull request Mar 10, 2026
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

* **New Features**
  * Added Stack CLI with authentication (login/logout) commands.
  * Added project management commands to list and create projects.
  * Added configuration management to pull and push project settings.
  * Added code execution capability to run JavaScript expressions.
  * Added initialization command for Stack Auth setup.

* **Tests**
  * Added comprehensive end-to-end test suite for CLI functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This was referenced Mar 11, 2026
@coderabbitai coderabbitai bot mentioned this pull request Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants