Skip to content

Add CLI logout, status, ctx commands and update docs#34

Merged
hackerwins merged 22 commits into
mainfrom
cli-login
Mar 15, 2026
Merged

Add CLI logout, status, ctx commands and update docs#34
hackerwins merged 22 commits into
mainfrom
cli-login

Conversation

@hackerwins

@hackerwins hackerwins commented Mar 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add logout, status, and ctx list/ctx switch commands for workspace management
  • Replace old auth login command with top-level login/logout/status/ctx commands in bin.ts and schema registry
  • Update CLI and VitePress docs with OAuth login flow, context switching, and new config path (~/.wafflebase/)

Test plan

  • pnpm verify:fast passes (103 backend, 1018 sheet, 59 CLI, 51 frontend tests)
  • New ctx tests (9 tests) cover workspace formatting and matching
  • Manual e2e: wafflebase login → browser OAuth → session stored
  • Manual e2e: wafflebase ctx list / wafflebase ctx switch
  • Manual e2e: wafflebase logout clears session

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Browser-based GitHub OAuth CLI login with persistent JWT sessions, auto-refresh, login/logout/status, and workspace context commands (ctx list/switch)
    • Import and export commands for CSV/JSON data (file or stdin/stdout), with dry-run support
  • Documentation

    • New design/task docs and updated CLI/API docs for auth, config location, and import/export usage
  • Tests

    • New unit tests for session, CSV parsing, ctx helpers, and CLI auth store
  • Chores

    • Local git hooks and CI verify pipeline updates

hackerwins and others added 14 commits March 15, 2026 09:47
Implement deferred CLI commands: interactive auth login setup,
CSV/JSON import into spreadsheet tabs, and tab data export.
Update schema registry, VitePress docs, and skill files.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Design browser-based OAuth login for CLI with JWT session storage
in ~/.wafflebase/, workspace context switching, and auth code
exchange pattern for secure token handling.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add Bearer token extraction as a fallback in JwtStrategy so CLI clients
can authenticate via Authorization: Bearer <token> header in addition to
the wafflebase_session cookie. Cookie still takes priority.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
In-memory store for two short-lived token types used in the CLI login
flow: OAuth state tokens (CSRF protection, 5-min TTL) and authorization
codes (60-sec TTL). Both are single-use and cleaned up on each create.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Support CLI OAuth login by passing a state token through the GitHub
OAuth flow. When mode=cli&port=<port> query params are present,
GitHubAuthGuard stores state in CliAuthStore and injects it into
Passport's authenticate options. On callback, if the state maps to a
CLI session, the backend creates a short-lived auth code and redirects
to http://127.0.0.1:<port>/callback?code=<code>. The new POST
/auth/cli/exchange endpoint lets the CLI exchange that code for JWT
tokens.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
When no wafflebase_refresh cookie is present, the endpoint now reads
refreshToken from the request body. If the token came from the body,
it returns JSON { accessToken, refreshToken } instead of setting
cookies, allowing CLI clients to rotate tokens without browser cookies.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Changes default config location from ~/.config/wafflebase/config.yaml
to ~/.wafflebase/config.yaml to consolidate all wafflebase state under
a single home directory. Adds best-effort migration that copies the old
file on first use. Exports getConfigPath() and migrateConfigIfNeeded()
for use by the upcoming session store module.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Session store reads/writes ~/.wafflebase/session.json with 0600
permissions. Supports load, save, clear, expiry check, and JWT
exp claim decoding.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add JWT session as a fallback auth method in the CLI. The auth
resolution order is: flags/env API key → session JWT → config profile
API key → none. The HTTP client now auto-refreshes expired JWT tokens
on 401 responses, persisting new tokens to the session file.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Starts local HTTP server, opens browser for GitHub OAuth, exchanges
auth code for JWT tokens, fetches user info and workspaces, saves
session to ~/.wafflebase/session.json.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Implements Tasks 9 and 10: logout clears the session file, status
displays the current auth state (user, server, workspace, expiry),
and ctx list/switch lets users view and change the active workspace
with exact-ID, exact-name, and ID-prefix matching.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Remove old interactive auth login command. Add login, logout,
status, ctx.list, and ctx.switch entries to schema registry.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Replace auth login with login/logout/status/ctx commands in VitePress
docs and design doc. Update config path to ~/.wafflebase/.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Backend: JWT Bearer, CLI auth store, GitHub guard
- CLI: config, session, auth, login/logout/status/ctx
- Docs and schema registry updated

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@coderabbitai

coderabbitai Bot commented Mar 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds browser-based GitHub OAuth for the CLI with a local callback server and one-time codes, persistent JWT sessions at ~/.wafflebase/session.json, CLI commands (login, logout, status, ctx, import, export), backend CLI OAuth support (CliAuthStore, GitHubAuthGuard, /auth/cli/exchange), and automatic token refresh in the HTTP client.

Changes

Cohort / File(s) Summary
Design & Docs
docs/design/README.md, docs/design/cli-oauth-login.md, docs/design/rest-api-and-cli.md, docs/tasks/active/20260312-cli-todo.md, docs/tasks/active/20260315-cli-oauth-login-todo.md, packages/cli/skills/SKILL.md, packages/cli/skills/import-export.md, packages/docs/api/cli.md
Added CLI OAuth design and task plans, updated CLI docs and API docs, added import/export docs and README entries.
Backend: CLI OAuth Core
packages/backend/src/auth/cli-auth.store.ts, packages/backend/src/auth/cli-auth.store.spec.ts, packages/backend/src/auth/github-auth.guard.ts, packages/backend/src/auth/auth.module.ts
New CliAuthStore (state/code lifecycle + expiry), GitHubAuthGuard that injects CLI state, unit tests, and module provider wiring.
Backend: Auth Controller & Strategy
packages/backend/src/auth/auth.controller.ts, packages/backend/src/auth/auth.controller.spec.ts, packages/backend/src/auth/github.strategy.ts, packages/backend/src/auth/jwt.strategy.ts
Extended GitHub flow to support CLI mode/state, added POST /auth/cli/exchange, refresh accepts token in request body, JwtStrategy now extracts Bearer tokens, and tests expanded.
CLI Session & Config
packages/cli/src/config/session.ts, packages/cli/src/config/config.ts, packages/cli/test/session.test.ts, packages/cli/test/config.test.ts
New session model and persistence (~/.wafflebase/session.json), config migration to ~/.wafflebase/, getConfigPath/migrateConfigIfNeeded, auth resolution order changed (flag/env > session JWT > config API key), and tests.
CLI Commands
packages/cli/src/commands/login.ts, packages/cli/src/commands/logout.ts, packages/cli/src/commands/status.ts, packages/cli/src/commands/ctx.ts, packages/cli/src/commands/import.ts, packages/cli/src/commands/export.ts
New login flow (local server + browser OAuth + exchange), logout/status, ctx list/switch, and import/export commands (CSV/JSON) with CLI wiring and prompts.
CLI Infra & Client
packages/cli/src/bin.ts, packages/cli/src/client/http-client.ts, packages/cli/src/util/csv-parse.ts, packages/cli/src/schema/registry.ts, packages/cli/package.json, packages/cli/test/csv-parse.test.ts, packages/cli/test/ctx.test.ts
Registered commands in bin, HTTP client auto-refresh and session persistence, CSV parsing utilities, registry entries, added dependency open, and tests.
CI / Hooks / Scripts
.githooks/pre-push, package.json, scripts/verify-ci.mjs, scripts/verify-self.mjs, docs/design/harness-engineering.md
Added pre-push hook, verify:ci runner and reports, adjusted verify:self lanes and docs.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI
    participant LocalServer as Local Callback Server
    participant GitHubOAuth as GitHub OAuth
    participant Backend

    User->>CLI: wafflebase login
    CLI->>LocalServer: start server (random port)
    CLI->>Backend: GET /auth/github?mode=cli&port=<port>
    Backend->>Backend: GitHubAuthGuard -> CliAuthStore.createState
    Backend-->>User: redirect to GitHub (includes state)
    User->>GitHubOAuth: authorize
    GitHubOAuth->>Backend: callback /auth/github/callback?state=<state>
    Backend->>Backend: CliAuthStore.consumeState -> create one-time code
    Backend-->>LocalServer: redirect to http://localhost:<port>/callback?code=<code>
    LocalServer-->>CLI: return code
    CLI->>Backend: POST /auth/cli/exchange { code }
    Backend->>Backend: CliAuthStore.consumeCode -> issue JWTs
    Backend-->>CLI: { accessToken, refreshToken }
    CLI->>Backend: GET /auth/me (Bearer)
    Backend-->>CLI: user info
    CLI->>Backend: GET /workspaces
    Backend-->>CLI: workspace list
    CLI->>CLI: save session (~/.wafflebase/session.json)
Loading
sequenceDiagram
    participant CLI
    participant HttpClient as HTTP Client (auto-refresh)
    participant Backend

    CLI->>HttpClient: API request (uses session.accessToken)
    HttpClient->>Backend: request with Authorization: Bearer accessToken
    Backend-->>HttpClient: 401 Unauthorized (expired)
    HttpClient->>Backend: POST /auth/refresh { refreshToken }
    Backend-->>HttpClient: { accessToken, refreshToken }
    HttpClient->>HttpClient: update session + saveSession
    HttpClient->>Backend: retry original request (new token)
    Backend-->>HttpClient: 200 OK
    HttpClient-->>CLI: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped to open browser gates,
A state, a code, and tiny fates.
Tokens tucked in burrowed file,
Workspaces gathered with a smile.
CLI logged in — I nibble, dance, and write! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add CLI logout, status, ctx commands and update docs' accurately summarizes the main changes: it clearly identifies the new CLI commands (logout, status, ctx) being added and mentions documentation updates, which are central to the PR.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cli-login
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@github-actions

github-actions Bot commented Mar 15, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 100.0s

Lane Status Duration
sheet:build ✅ pass 13.8s
verify:fast ✅ pass 49.6s
frontend:build ✅ pass 13.8s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.5s
cli:build ✅ pass 1.8s
verify:entropy ✅ pass 16.2s

Verification: verify:integration

Result: ✅ PASS

Production API is more useful as the default than localhost
for most users. Extracted DEFAULT_SERVER constant to keep
all references in sync.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (12)
packages/cli/test/csv-parse.test.ts (1)

4-33: Good test coverage for parseCsv.

The tests cover key scenarios including quoted fields, escaped quotes, and CRLF handling.

Consider adding a test for quoted fields containing newlines, as this is a common CSV edge case:

it('handles quoted fields with embedded newlines', () => {
  expect(parseCsv('"line1\nline2",b')).toEqual([['line1\nline2', 'b']]);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/test/csv-parse.test.ts` around lines 4 - 33, Add a test to cover
quoted fields that contain embedded newlines for parseCsv: create an it block
that calls parseCsv with a CSV string where a field is quoted and includes a
newline (e.g. "\"line1\nline2\",b") and assert the parsed output contains that
field with the newline preserved (['line1\nline2','b']). Place this new test
alongside the other parseCsv tests so parseCsv is exercised for embedded-newline
quoted fields.
packages/cli/src/util/csv-parse.ts (1)

68-77: Consider logging a warning for invalid cell references.

parseStartRef silently falls back to {row: 1, col: 1} when the input is invalid. While this provides a safe default, users may not realize their --start argument was malformed. Consider returning the fallback with a console warning, or documenting this behavior clearly.

💡 Optional: Add warning for invalid input
 export function parseStartRef(ref: string): { row: number; col: number } {
   const match = ref.match(/^([A-Z]+)(\d+)$/i);
-  if (!match) return { row: 1, col: 1 };
+  if (!match) {
+    console.warn(`Invalid cell reference "${ref}", defaulting to A1`);
+    return { row: 1, col: 1 };
+  }
   const letters = match[1].toUpperCase();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/util/csv-parse.ts` around lines 68 - 77, The parseStartRef
function currently silently returns the default {row: 1, col: 1} for invalid
refs; update parseStartRef to emit a clear warning (e.g., console.warn) when the
input ref fails the regex so users know their --start argument was malformed,
include the original ref value in the message for debugging, and then continue
returning the existing fallback; keep the function name parseStartRef and its
return shape unchanged.
packages/cli/test/config.test.ts (1)

81-125: Consider cleaning up temp directories after tests.

The migration tests create temporary directories in tmpdir() but don't clean them up. While this is typically fine for CI environments that reset between runs, accumulated test artifacts could clutter local development environments over time.

♻️ Proposed cleanup using afterEach
+import { rmSync } from 'node:fs';
+
 describe('migrateConfigIfNeeded', () => {
+  const createdDirs: string[] = [];
+
+  afterEach(() => {
+    for (const dir of createdDirs) {
+      rmSync(dir, { recursive: true, force: true });
+    }
+    createdDirs.length = 0;
+  });
+
   it('copies config from old path to new path when new path does not exist', () => {
     const base = join(tmpdir(), `wfb-migrate-test-${Date.now()}`);
+    createdDirs.push(base);
     // ... rest of test
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/test/config.test.ts` around lines 81 - 125, Tests under the
migrateConfigIfNeeded describe block leave temporary directories in the OS
tmpdir; add cleanup by storing each test's base temp directory in a scoped
variable and adding an afterEach hook that recursively removes that base (using
fs.rmSync or fs.rmdirSync with { recursive: true } or try/catch around unlink)
so tests do not leave artifacts; reference the existing migrateConfigIfNeeded
tests and the tmpdir-based base variables (e.g., base, oldDir, newDir) and
ensure the afterEach is tolerant if the directory was never created.
packages/cli/src/commands/ctx.ts (1)

10-14: Truncated IDs may cause visual ambiguity.

The workspace ID is truncated to 8 characters for display (ws.id.slice(0, 8)). If workspace IDs are UUIDs, this is typically sufficient for uniqueness, but the switch command accepts the full ID, exact name, or prefix match. Users might try to copy-paste the truncated ID shown in list, which would then trigger prefix matching rather than exact ID match.

This works correctly due to the prefix-match fallback, but consider showing the truncation explicitly (e.g., abc12345...) to signal it's abbreviated.

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

In `@packages/cli/src/commands/ctx.ts` around lines 10 - 14, The displayed
workspace ID is being truncated with ws.id.slice(0, 8) which can mislead users
into thinking that shown text is the full ID; update the rendering in the
mapping that builds the list (the .map((ws) => { ... }) block) to explicitly
indicate truncation when you shorten an ID (e.g., show `${ws.id.slice(0,8)}...`
only when ws.id.length > 8), leaving the active marker logic (marker = ws.id ===
activeId ? '*' : ' ') unchanged so users know the ID is abbreviated.
packages/cli/src/config/session.ts (1)

59-68: Consider atomic write to prevent session file corruption.

The current implementation writes directly to the session file. If the process is interrupted during writeFileSync, the file could be left in a corrupted or partial state. This is a common issue with credential files that could leave users unable to authenticate.

A safer pattern is to write to a temporary file first, then atomically rename it to the target path.

♻️ Proposed atomic write pattern
+import { renameSync } from 'node:fs';
+
 export function saveSession(
   session: Session,
   path: string = getSessionPath(),
 ): void {
   const dir = dirname(path);
   mkdirSync(dir, { recursive: true });
-  writeFileSync(path, JSON.stringify(session, null, 2), { mode: 0o600 });
-  // Explicitly chmod in case the file already existed with different permissions
-  chmodSync(path, 0o600);
+  const tempPath = `${path}.tmp`;
+  writeFileSync(tempPath, JSON.stringify(session, null, 2), { mode: 0o600 });
+  renameSync(tempPath, path);
+  // Explicitly chmod in case rename preserved different permissions
+  chmodSync(path, 0o600);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/config/session.ts` around lines 59 - 68, The saveSession
function currently writes directly to the target path and can corrupt the
session file if interrupted; change it to perform an atomic write by writing
JSON to a temporary file in the same directory (use dirname(getSessionPath()) or
dirname(path)), fs.writeFileSync the temp file with mode 0o600, fs.renameSync
the temp file to the target path to atomically replace it, then optionally
chmodSync the final path to 0o600; also ensure mkdirSync(dir, { recursive: true
}) runs before creating the temp and clean up the temp file with unlinkSync on
error to avoid leaving cruft.
packages/cli/src/commands/import.ts (1)

51-55: Inconsistent header handling for JSON array-of-objects.

When --no-header is passed for CSV, the first row is treated as data. However, for JSON array-of-objects, a header row is always synthesized from object keys (line 55), ignoring the localOpts.header flag. This could be intentional (objects inherently define their structure), but may surprise users expecting consistent behavior.

Consider documenting this behavior or applying the header flag consistently.

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

In `@packages/cli/src/commands/import.ts` around lines 51 - 55, The JSON
array-of-objects branch always synthesizes a header row (vars objs, keys, rows)
which ignores the CSV header flag; update the logic in import.ts where parsed is
an object array so it checks localOpts.header and only prepends keys when header
is true, otherwise convert all objects to value rows (e.g., map each object to
keys from the first object but do not add the keys row) so behavior matches the
--no-header option; alternatively add a clear comment documenting the
intentional difference if you prefer to keep current behavior.
docs/tasks/active/20260312-cli-todo.md (1)

61-68: Consider archiving this task file.

All phases and verification items are marked complete. Based on learnings, completed tasks should be archived with pnpm tasks:archive && pnpm tasks:index after pnpm verify:fast passes.

Note: The PR description mentions some manual E2E items are not all completed. If those need tracking, consider adding them to this file before archiving, or create a follow-up task.

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

In `@docs/tasks/active/20260312-cli-todo.md` around lines 61 - 68, This task file
has all verification items checked and should be archived: first ensure the
suite passes locally by running pnpm verify:fast, then archive the task with
pnpm tasks:archive && pnpm tasks:index; before archiving, if there are
outstanding manual E2E checks referenced in the PR description, add them as
checklist items to this task file (or create a follow-up task) so those items
remain tracked prior to running the archive/index commands.
packages/backend/src/auth/github.strategy.ts (1)

23-30: Consider type-safe access to __cliStateToken.

The (req as any).__cliStateToken cast works but loses type safety. Since GitHubAuthGuard sets this property, consider augmenting the Express Request type to include it.

🔧 Add type declaration for `__cliStateToken`

Create or extend a declaration file (e.g., packages/backend/src/types/express.d.ts):

declare global {
  namespace Express {
    interface Request {
      __cliStateToken?: string;
    }
  }
}

export {};

Then update the access:

-    const cliState = (req as any).__cliStateToken as string | undefined;
+    const cliState = req.__cliStateToken;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/auth/github.strategy.ts` around lines 23 - 30, Add a
proper type declaration for Express.Request to include the optional
__cliStateToken (e.g., create an ambient declaration that extends
Express.Request with __cliStateToken?: string and export {}), then remove the
unsafe cast in authenticate and access the property as req.__cliStateToken;
update authenticate(req: Request, ...) to use that typed property (referencing
the authenticate method and GitHubAuthGuard usage) so the code is type-safe
without (req as any).
packages/backend/src/auth/cli-auth.store.ts (1)

66-70: Consider rate-limiting or bounding the maps for production resilience.

The cleanup() method runs on every createState/createCode call, iterating all entries. Under high load or a DoS attack, the maps could grow faster than cleanup removes entries. For production, consider:

  • A maximum size limit with oldest-entry eviction
  • Periodic cleanup on a timer rather than synchronous iteration

This is a nice-to-have for CLI auth which typically has low concurrency.

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

In `@packages/backend/src/auth/cli-auth.store.ts` around lines 66 - 70, The
cleanup() method currently iterates the entire states and codes Maps on every
createState/createCode call which can cause unbounded growth under load; change
the strategy by (1) introducing configurable MAX_STATES and MAX_CODES constants
and evicting oldest entries when insertion would exceed those limits (use Map
insertion order or maintain a small FIFO/linked list to identify oldest) inside
createState/createCode rather than full cleanup, and (2) move full-scan expiry
removal to a periodic async timer (setInterval) that runs off the hot path and
expose a stop/teardown to clear the timer; update references to cleanup(),
createState, createCode, states and codes to implement these limits and the
periodic cleaner.
docs/tasks/active/20260315-cli-oauth-login-todo.md (1)

519-524: Minor: Add language specifier to fenced code block.

The output example block lacks a language specifier. While this doesn't affect functionality, it improves documentation consistency and syntax highlighting.

📝 Suggested fix
 Output format:
-```
+```text
 Logged in as hackerwins ([email protected])
 Server:    http://localhost:3000
 Workspace: hackerwins's Workspace (e98ff707-...)
 Session:   valid (expires 2026-03-15T10:00:00Z)
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/tasks/active/20260315-cli-oauth-login-todo.md around lines 519 - 524,
The fenced code block showing the CLI login output is missing a language
specifier; update the triple-backtick fence for the example output in the
docs/tasks/active/20260315-cli-oauth-login-todo.md file to include a language
(e.g., add "text" after ), so the block becomes text to enable consistent
syntax highlighting for the Logged in as... Server... Workspace... Session
output example.


</details>

</blockquote></details>
<details>
<summary>packages/backend/src/auth/auth.controller.ts (2)</summary><blockquote>

`52-62`: **Consider underscore-prefix for unused parameters.**

The `void` statements work to suppress unused variable warnings, but underscore-prefixing is more idiomatic in TypeScript:

```typescript
async githubAuth(
  `@Query`('mode') _mode: string | undefined,
  `@Query`('port') _port: string | undefined,
  `@Req`() _req: Request,
) {
  // Guard handles CLI mode detection and injects state token
}
```

This makes intent clearer at declaration rather than requiring statements in the body.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/auth/auth.controller.ts` around lines 52 - 62, Rename
the unused parameters in the async githubAuth method to use an underscore prefix
(e.g., change mode, port, req to _mode, _port, _req) and remove the trailing
void statements in the function body; update the function signature in
auth.controller.ts (githubAuth) accordingly so the intent is clear at
declaration and the body no longer contains the unused-variable suppressions.
```

</details>

---

`132-138`: **Consider adding a comment to clarify the dual-source token logic.**

The cookie-priority logic is correct but non-obvious to future readers. A brief comment would help:

```diff
+    // Support both cookie-based (web) and body-based (CLI) refresh flows.
+    // Cookie takes priority if present; CLI clients send token in body.
     const cookieToken = req.cookies?.[REFRESH_COOKIE_NAME];
     const bodyToken =
       typeof req.body?.refreshToken === 'string'
         ? req.body.refreshToken
         : undefined;
     const fromBody = !cookieToken && !!bodyToken;
     const refreshToken = cookieToken ?? bodyToken;
```

As per coding guidelines: "Write clear comments explaining the 'why' behind complex logic, not just the 'what'".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/auth/auth.controller.ts` around lines 132 - 138, Add a
brief clarifying comment above the dual-source token logic that explains why
cookieToken takes precedence over req.body.refreshToken and what fromBody
represents; specifically annotate the block using the symbols
REFRESH_COOKIE_NAME, cookieToken, bodyToken, fromBody, and refreshToken to state
that cookies are preferred for refresh tokens for security/consistency and
fromBody is true only when no cookie is present and a string body token exists.
Ensure the comment explains the intended precedence and purpose (not just
restating the code).
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @packages/backend/src/auth/auth.controller.ts:

  • Around line 86-98: When a stateToken is present but
    this.cliAuthStore.consumeState(stateToken) returns undefined the code silently
    falls through to the web flow; change the logic so that when stateToken is
    provided and consumeState returns undefined you throw a BadRequestException
    (e.g., "Invalid or expired CLI state") to surface the error to the CLI flow;
    also simplify the branch for state.mode === 'cli' by removing the redundant port
    range check (since GitHubAuthGuard already validates port) and proceed to
    generate the code via this.cliAuthStore.createCode(user.id) and redirect to the
    127.0.0.1:{port}/callback URL as before.

In @packages/cli/package.json:

  • Line 23: Add an "engines" field to the CLI package manifest to require Node.js

=20 so users cannot install with incompatible runtimes; update
packages/cli/package.json by inserting an "engines" object (e.g., "node":
">=20") at the top-level of the JSON to enforce the minimum Node version and
prevent runtime errors from the "open" dependency v11.0.0.

In @packages/cli/src/client/http-client.ts:

  • Around line 70-77: The token refresh path updates in-memory tokens but skips
    persistence when loadSession() returns null, causing silent loss; update the
    refresh logic (around loadSession/saveSession in http-client.ts) to handle a
    missing session file by either creating and saving a new session object with the
    refreshed accessToken/refreshToken/expiresAt (use decodeJwtExpiry) or by logging
    a warning and returning false to signal persistence failure; ensure you
    reference loadSession(), saveSession(), and decodeJwtExpiry() and that the
    function's return value reflects whether persistence succeeded.

In @packages/cli/src/commands/export.ts:

  • Around line 9-14: The detectFormat function currently casts any formatFlag to
    'csv'|'json' without validation, so invalid values (e.g., 'xml') silently become
    treated as JSON; update detectFormat to validate formatFlag explicitly: check if
    formatFlag equals 'csv' or 'json' (case-insensitive if desired) and return it
    only then, otherwise fall back to extension-based detection or throw/return a
    default and surface an error. Locate the detectFormat function and the
    formatFlag parameter to implement this guard and ensure callers (e.g., where fmt
    is compared to 'csv') receive only valid values.

In @packages/cli/src/commands/import.ts:

  • Around line 9-14: The function detectFormat currently unsafely casts the
    user-provided formatFlag to 'csv'|'json'; update detectFormat to explicitly
    validate formatFlag (check if formatFlag === 'csv' or formatFlag === 'json')
    before returning it, otherwise ignore it (or throw a clear error) and fall back
    to extname(file) detection; reference the detectFormat function and the
    formatFlag parameter and ensure any error message mentions the invalid format
    and accepted values so callers/users know to pass 'csv' or 'json'.

In @packages/cli/src/commands/login.ts:

  • Around line 90-98: The workspace fetch currently fails silently and leaves
    workspaces empty; wrap the fetch call used to populate workspaces (the wsRes
    fetch to ${server}/workspaces using tokens.accessToken) in a try/catch and,
    when wsRes.ok is false or an exception is thrown, emit a clear warning (e.g.,
    console.warn or the CLI logger) that includes the HTTP status and response text
    or the caught error message so users know the fetch failed rather than
    legitimately having no workspaces; keep the existing behavior of defaulting
    workspaces to [] after logging the warning.

Nitpick comments:
In @docs/tasks/active/20260312-cli-todo.md:

  • Around line 61-68: This task file has all verification items checked and
    should be archived: first ensure the suite passes locally by running pnpm
    verify:fast, then archive the task with pnpm tasks:archive && pnpm tasks:index;
    before archiving, if there are outstanding manual E2E checks referenced in the
    PR description, add them as checklist items to this task file (or create a
    follow-up task) so those items remain tracked prior to running the archive/index
    commands.

In @docs/tasks/active/20260315-cli-oauth-login-todo.md:

  • Around line 519-524: The fenced code block showing the CLI login output is
    missing a language specifier; update the triple-backtick fence for the example
    output in the docs/tasks/active/20260315-cli-oauth-login-todo.md file to include
    a language (e.g., add "text" after ), so the block becomes text to enable
    consistent syntax highlighting for the Logged in as... Server... Workspace...
    Session output example.

In @packages/backend/src/auth/auth.controller.ts:

  • Around line 52-62: Rename the unused parameters in the async githubAuth method
    to use an underscore prefix (e.g., change mode, port, req to _mode, _port, _req)
    and remove the trailing void statements in the function body; update the
    function signature in auth.controller.ts (githubAuth) accordingly so the intent
    is clear at declaration and the body no longer contains the unused-variable
    suppressions.
  • Around line 132-138: Add a brief clarifying comment above the dual-source
    token logic that explains why cookieToken takes precedence over
    req.body.refreshToken and what fromBody represents; specifically annotate the
    block using the symbols REFRESH_COOKIE_NAME, cookieToken, bodyToken, fromBody,
    and refreshToken to state that cookies are preferred for refresh tokens for
    security/consistency and fromBody is true only when no cookie is present and a
    string body token exists. Ensure the comment explains the intended precedence
    and purpose (not just restating the code).

In @packages/backend/src/auth/cli-auth.store.ts:

  • Around line 66-70: The cleanup() method currently iterates the entire states
    and codes Maps on every createState/createCode call which can cause unbounded
    growth under load; change the strategy by (1) introducing configurable
    MAX_STATES and MAX_CODES constants and evicting oldest entries when insertion
    would exceed those limits (use Map insertion order or maintain a small
    FIFO/linked list to identify oldest) inside createState/createCode rather than
    full cleanup, and (2) move full-scan expiry removal to a periodic async timer
    (setInterval) that runs off the hot path and expose a stop/teardown to clear the
    timer; update references to cleanup(), createState, createCode, states and codes
    to implement these limits and the periodic cleaner.

In @packages/backend/src/auth/github.strategy.ts:

  • Around line 23-30: Add a proper type declaration for Express.Request to
    include the optional __cliStateToken (e.g., create an ambient declaration that
    extends Express.Request with __cliStateToken?: string and export {}), then
    remove the unsafe cast in authenticate and access the property as
    req.__cliStateToken; update authenticate(req: Request, ...) to use that typed
    property (referencing the authenticate method and GitHubAuthGuard usage) so the
    code is type-safe without (req as any).

In @packages/cli/src/commands/ctx.ts:

  • Around line 10-14: The displayed workspace ID is being truncated with
    ws.id.slice(0, 8) which can mislead users into thinking that shown text is the
    full ID; update the rendering in the mapping that builds the list (the .map((ws)
    => { ... }) block) to explicitly indicate truncation when you shorten an ID
    (e.g., show ${ws.id.slice(0,8)}... only when ws.id.length > 8), leaving the
    active marker logic (marker = ws.id === activeId ? '*' : ' ') unchanged so users
    know the ID is abbreviated.

In @packages/cli/src/commands/import.ts:

  • Around line 51-55: The JSON array-of-objects branch always synthesizes a
    header row (vars objs, keys, rows) which ignores the CSV header flag; update the
    logic in import.ts where parsed is an object array so it checks localOpts.header
    and only prepends keys when header is true, otherwise convert all objects to
    value rows (e.g., map each object to keys from the first object but do not add
    the keys row) so behavior matches the --no-header option; alternatively add a
    clear comment documenting the intentional difference if you prefer to keep
    current behavior.

In @packages/cli/src/config/session.ts:

  • Around line 59-68: The saveSession function currently writes directly to the
    target path and can corrupt the session file if interrupted; change it to
    perform an atomic write by writing JSON to a temporary file in the same
    directory (use dirname(getSessionPath()) or dirname(path)), fs.writeFileSync the
    temp file with mode 0o600, fs.renameSync the temp file to the target path to
    atomically replace it, then optionally chmodSync the final path to 0o600; also
    ensure mkdirSync(dir, { recursive: true }) runs before creating the temp and
    clean up the temp file with unlinkSync on error to avoid leaving cruft.

In @packages/cli/src/util/csv-parse.ts:

  • Around line 68-77: The parseStartRef function currently silently returns the
    default {row: 1, col: 1} for invalid refs; update parseStartRef to emit a clear
    warning (e.g., console.warn) when the input ref fails the regex so users know
    their --start argument was malformed, include the original ref value in the
    message for debugging, and then continue returning the existing fallback; keep
    the function name parseStartRef and its return shape unchanged.

In @packages/cli/test/config.test.ts:

  • Around line 81-125: Tests under the migrateConfigIfNeeded describe block leave
    temporary directories in the OS tmpdir; add cleanup by storing each test's base
    temp directory in a scoped variable and adding an afterEach hook that
    recursively removes that base (using fs.rmSync or fs.rmdirSync with { recursive:
    true } or try/catch around unlink) so tests do not leave artifacts; reference
    the existing migrateConfigIfNeeded tests and the tmpdir-based base variables
    (e.g., base, oldDir, newDir) and ensure the afterEach is tolerant if the
    directory was never created.

In @packages/cli/test/csv-parse.test.ts:

  • Around line 4-33: Add a test to cover quoted fields that contain embedded
    newlines for parseCsv: create an it block that calls parseCsv with a CSV string
    where a field is quoted and includes a newline (e.g. ""line1\nline2",b") and
    assert the parsed output contains that field with the newline preserved
    (['line1\nline2','b']). Place this new test alongside the other parseCsv tests
    so parseCsv is exercised for embedded-newline quoted fields.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `6a6f76f5-b94c-462f-a82e-c48497bcc05f`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 865e7c142f86194722a9abe2fe381643695411a5 and 66d2a34090b4c6838b079c089fa3ae3a2626f21b.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (33)</summary>

* `docs/design/README.md`
* `docs/design/cli-oauth-login.md`
* `docs/design/rest-api-and-cli.md`
* `docs/tasks/active/20260312-cli-todo.md`
* `docs/tasks/active/20260315-cli-oauth-login-todo.md`
* `packages/backend/src/auth/auth.controller.spec.ts`
* `packages/backend/src/auth/auth.controller.ts`
* `packages/backend/src/auth/auth.module.ts`
* `packages/backend/src/auth/cli-auth.store.spec.ts`
* `packages/backend/src/auth/cli-auth.store.ts`
* `packages/backend/src/auth/github-auth.guard.ts`
* `packages/backend/src/auth/github.strategy.ts`
* `packages/backend/src/auth/jwt.strategy.ts`
* `packages/cli/package.json`
* `packages/cli/skills/SKILL.md`
* `packages/cli/skills/import-export.md`
* `packages/cli/src/bin.ts`
* `packages/cli/src/client/http-client.ts`
* `packages/cli/src/commands/ctx.ts`
* `packages/cli/src/commands/export.ts`
* `packages/cli/src/commands/import.ts`
* `packages/cli/src/commands/login.ts`
* `packages/cli/src/commands/logout.ts`
* `packages/cli/src/commands/status.ts`
* `packages/cli/src/config/config.ts`
* `packages/cli/src/config/session.ts`
* `packages/cli/src/schema/registry.ts`
* `packages/cli/src/util/csv-parse.ts`
* `packages/cli/test/config.test.ts`
* `packages/cli/test/csv-parse.test.ts`
* `packages/cli/test/ctx.test.ts`
* `packages/cli/test/session.test.ts`
* `packages/docs/api/cli.md`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread packages/backend/src/auth/auth.controller.ts
Comment thread packages/cli/package.json
Comment thread packages/cli/src/client/http-client.ts Outdated
Comment thread packages/cli/src/commands/export.ts
Comment thread packages/cli/src/commands/import.ts
Comment on lines +90 to +98
// 7. Get workspace list
const wsRes = await fetch(`${server}/workspaces`, {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
});

let workspaces: WorkspaceInfo[] = [];
if (wsRes.ok) {
workspaces = (await wsRes.json()) as WorkspaceInfo[];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent failure on workspace fetch may confuse users.

If the /workspaces endpoint returns an error (e.g., 500, network issue), the code silently proceeds with an empty array, leading to "No workspaces found." The user may not realize the fetch failed vs. genuinely having no workspaces.

Consider logging a warning when the workspace fetch fails so users can troubleshoot.

🛠️ Suggested improvement
       // 7. Get workspace list
       const wsRes = await fetch(`${server}/workspaces`, {
         headers: { Authorization: `Bearer ${tokens.accessToken}` },
       });

       let workspaces: WorkspaceInfo[] = [];
       if (wsRes.ok) {
         workspaces = (await wsRes.json()) as WorkspaceInfo[];
+      } else {
+        console.error('Warning: Failed to fetch workspaces. You may need to set workspace manually.');
       }
📝 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.

Suggested change
// 7. Get workspace list
const wsRes = await fetch(`${server}/workspaces`, {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
});
let workspaces: WorkspaceInfo[] = [];
if (wsRes.ok) {
workspaces = (await wsRes.json()) as WorkspaceInfo[];
}
// 7. Get workspace list
const wsRes = await fetch(`${server}/workspaces`, {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
});
let workspaces: WorkspaceInfo[] = [];
if (wsRes.ok) {
workspaces = (await wsRes.json()) as WorkspaceInfo[];
} else {
console.error('Warning: Failed to fetch workspaces. You may need to set workspace manually.');
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/login.ts` around lines 90 - 98, The workspace fetch
currently fails silently and leaves workspaces empty; wrap the fetch call used
to populate workspaces (the wsRes fetch to `${server}/workspaces` using
tokens.accessToken) in a try/catch and, when wsRes.ok is false or an exception
is thrown, emit a clear warning (e.g., console.warn or the CLI logger) that
includes the HTTP status and response text or the caught error message so users
know the fetch failed rather than legitimately having no workspaces; keep the
existing behavior of defaulting workspaces to [] after logging the warning.

hackerwins and others added 5 commits March 15, 2026 10:46
verify:entropy flagged bare backtick filenames like
`session.json` as broken file references. Replaced with
prose to avoid false positives from the doc-staleness
checker.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
verify:entropy (doc staleness, dead code, dependency
freshness) and builds were only caught in CI. The
pre-push hook closes this gap locally.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Browser visual/interaction tests require Docker Chromium
and are covered by CI. Pre-push now runs builds, chunk
budgets, and entropy checks without browser lane.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
verify:self = local (fast + builds + chunks + entropy)
verify:ci = CI-only (browser + integration, needs Docker)
verify:full = verify:self + verify:ci

Pre-push hook runs verify:self. Browser and integration
tests move to verify:ci, run only in CI.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Return error when CLI OAuth state is expired/invalid instead of
  silently falling through to the web flow
- Add format validation for --format flag in export/import commands
- Warn when workspace fetch fails during login
- Add comment explaining session-missing case in token refresh
- Add engines field (node>=20) to CLI package.json for open@11

Co-Authored-By: Claude Opus 4.6 <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/design/harness-engineering.md (1)

185-206: ⚠️ Potential issue | 🟡 Minor

Update the earlier verify:self references in this doc too.

This section moves browser coverage into verify:ci, but the summary at Lines 20-23 and the Phase 18a / top-level status sections still say browser lanes are part of verify:self. Leaving both versions in one document makes the lane contract hard to trust.

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

In `@docs/design/harness-engineering.md` around lines 185 - 206, Update the
earlier references that still claim browser lanes belong to `pnpm verify:self`:
change the summary and any Phase 18a / top-level status text that lists browser
coverage under `verify:self` so it no longer mentions browser visual/interaction
or Chromium; instead ensure `pnpm verify:self` is described only as the fast
runner/builds/chunk budgets/entropy step (matching the table entry) and update
`pnpm verify:ci` descriptions to explicitly state it now includes browser
visual/interaction and Chromium coverage and produces the CI summary report.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/design/cli-oauth-login.md`:
- Line 36: The markdown lint MD040 warnings are caused by unlabeled fenced code
blocks in docs/design/cli-oauth-login.md; update each backtick fence to include
an appropriate language identifier (e.g., ```text or ```bash) for the blocks
that show the filesystem snippet (starts with " ~/.wafflebase/"), the CLI
examples (blocks containing "wafflebase login", "wafflebase logout", "wafflebase
ctx list", etc.), and the workspace/API-key examples (blocks showing "* e98ff707
hackerwins's Workspace" and the numbered steps). Ensure every opening ``` is
followed by the correct identifier (text or bash) as indicated in the proposed
fix so all occurrences mentioned (lines with the filesystem snippet, CLI
commands, workspace list, and numbered steps) are labeled.
- Line 55: The example JSON contains a real-looking email ("email":
"[email protected]"); replace it with an anonymized placeholder such as
"email": "[email protected]" (or "email": "[email protected]") in the
docs/design/cli-oauth-login.md example so public docs do not expose personal
identifiers.

In `@packages/cli/src/client/http-client.ts`:
- Around line 97-126: The API-key methods (listApiKeys, createApiKey,
revokeApiKey) bypass the centralized JWT refresh/retry logic in request(),
causing failures on expired sessions; either route those methods to call
request() instead of raw fetch or implement a new requestAbsolute() that
duplicates request()'s 401->refreshSession()->retry logic and use it from those
functions; update listApiKeys, createApiKey, revokeApiKey to call the unified
helper (or requestAbsolute) so they inherit the same auto-refresh-and-retry
behavior and return the same { ok, status, data } shape as request().

In `@packages/cli/src/commands/export.ts`:
- Around line 38-40: The current export error turns every non-OK response into a
generic `HTTP ${res.status}`, losing backend error details; update the error
handling after the getClient(...).getCells(docId, localOpts.tab,
localOpts.range) call to parse the response body (e.g., await res.json() or
await res.text()) and, if the parsed body contains an error message (e.g.,
body.error?.message or body.message), include that message in the thrown Error
instead of or alongside `HTTP ${res.status}` so failed exports show the API's
descriptive error.

In `@packages/cli/src/commands/import.ts`:
- Around line 36-70: The CSV path currently ignores the --no-header /
localOpts.header option; update the import action so the CSV parsing respects
localOpts.header by wiring it into parseCsv (or by post-processing the parsed
rows): call parseCsv(raw, { header: localOpts.header }) if parseCsv supports
options, or after rows = parseCsv(raw) treat the first row as a header row only
when localOpts.header === true (and leave it as data when localOpts.header ===
false). Modify the code around parseCsv and use localOpts.header (and the
--no-header option name) to decide whether the first row is header or regular
data.

In `@packages/cli/src/commands/login.ts`:
- Around line 91-136: Before saving the new session, don't overwrite an existing
session's workspace state when the /workspaces fetch fails: call loadSession()
(or retrieve the current saved session) before performing the fetch and, if
wsRes.ok is false, reuse existingSession.workspaces and
existingSession.activeWorkspace instead of leaving workspaces = [] and
activeWorkspace = ''. Update the logic around workspaces, activeWorkspace, and
the final Session object (used by saveSession) so that only a successful
wsRes.json() replaces the saved workspace list; otherwise preserve the previous
values.

In `@packages/cli/src/config/config.ts`:
- Around line 42-44: Replace the console.log migration notices printed during
config resolution with stderr output so they don't pollute machine-readable CLI
stdout; locate the console.log calls in the config resolution/migration code
(the statements that print "[wafflebase] Config migrated from ${oldPath} to
${newPath}" and the similar one around lines 61-63) and change them to write to
stderr (e.g., use console.error or process.stderr.write) so normal CLI stdout
remains untouched for JSON/pipe consumers.

In `@scripts/verify-ci.mjs`:
- Around line 114-126: The current loop short-circuits remaining lanes when the
`failed` flag is true, causing subsequent lanes in `LANES` (e.g.,
`verify:integration`) to be skipped; remove the early-skip logic so every lane
runs regardless of previous failures. Concretely, delete the `if (failed) { ...
continue; }` block (the code that pushes `skipReport` and calls
`writeLaneReport`) and let the normal lane execution flow run for each item in
`LANES`; continue to set `failed` when a lane actually fails and still
push/report each lane's real result into `results` using `writeLaneReport` so
reports reflect actual run outcomes. Ensure `results`, `failed`, `LANES`, and
`writeLaneReport` are used as before but without preemptive skipping.

---

Outside diff comments:
In `@docs/design/harness-engineering.md`:
- Around line 185-206: Update the earlier references that still claim browser
lanes belong to `pnpm verify:self`: change the summary and any Phase 18a /
top-level status text that lists browser coverage under `verify:self` so it no
longer mentions browser visual/interaction or Chromium; instead ensure `pnpm
verify:self` is described only as the fast runner/builds/chunk budgets/entropy
step (matching the table entry) and update `pnpm verify:ci` descriptions to
explicitly state it now includes browser visual/interaction and Chromium
coverage and produces the CI summary report.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2b302abf-7e39-4a6e-a039-3f2afca8bff7

📥 Commits

Reviewing files that changed from the base of the PR and between 66d2a34 and c19ad3c.

📒 Files selected for processing (16)
  • .githooks/pre-push
  • docs/design/cli-oauth-login.md
  • docs/design/harness-engineering.md
  • package.json
  • packages/backend/src/auth/auth.controller.ts
  • packages/cli/package.json
  • packages/cli/src/client/http-client.ts
  • packages/cli/src/commands/export.ts
  • packages/cli/src/commands/import.ts
  • packages/cli/src/commands/login.ts
  • packages/cli/src/config/config.ts
  • packages/cli/src/schema/registry.ts
  • packages/cli/test/config.test.ts
  • packages/docs/api/cli.md
  • scripts/verify-ci.mjs
  • scripts/verify-self.mjs
💤 Files with no reviewable changes (1)
  • scripts/verify-self.mjs

Comment thread docs/design/cli-oauth-login.md Outdated
Comment thread docs/design/cli-oauth-login.md Outdated
Comment on lines +97 to +126
// Auto-refresh on 401 for JWT auth (one attempt only)
if (
res.status === 401 &&
this.config.authMode === 'jwt' &&
this.config.refreshToken
) {
const refreshed = await this.refreshSession();
if (refreshed) {
// Retry the original request with new token
const retryRes = await fetch(url, {
method,
headers: this.headers,
body: body ? JSON.stringify(body) : undefined,
});
const retryData = (await retryRes.json().catch(() => null)) as T;
return { ok: retryRes.ok, status: retryRes.status, data: retryData };
}

// Refresh failed — return a clear error
return {
ok: false,
status: 401,
data: {
error: {
code: 'SESSION_EXPIRED',
message: 'Session expired. Run `wafflebase login`.',
},
} as T,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reuse the JWT refresh path for API-key endpoints too.

Only request() gets the new 401 refresh/retry logic. listApiKeys(), createApiKey(), and revokeApiKey() still use raw fetch, so the same expired session refreshes for document/tab/cell commands but fails for API-key management. Please funnel those methods through the same helper, or add a requestAbsolute() variant with the same retry behavior.

Also applies to: 179-203

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

In `@packages/cli/src/client/http-client.ts` around lines 97 - 126, The API-key
methods (listApiKeys, createApiKey, revokeApiKey) bypass the centralized JWT
refresh/retry logic in request(), causing failures on expired sessions; either
route those methods to call request() instead of raw fetch or implement a new
requestAbsolute() that duplicates request()'s 401->refreshSession()->retry logic
and use it from those functions; update listApiKeys, createApiKey, revokeApiKey
to call the unified helper (or requestAbsolute) so they inherit the same
auto-refresh-and-retry behavior and return the same { ok, status, data } shape
as request().

Comment thread packages/cli/src/commands/export.ts Outdated
Comment thread packages/cli/src/commands/import.ts Outdated
Comment on lines +36 to +70
.option('--no-header', 'First row is NOT a header (CSV)')
.option('--start <ref>', 'Top-left cell to start import', 'A1')
.action(async function (this: Command, docId: string, file: string) {
const opts = getGlobalOpts(this);
const localOpts = this.opts<{
tab: string;
format?: string;
header: boolean;
start: string;
}>();

try {
const fmt = detectFormat(file, localOpts.format);
const raw = readInput(file);
let rows: string[][];

if (fmt === 'json') {
const parsed = JSON.parse(raw);
// Expect either array of arrays or array of objects
if (Array.isArray(parsed) && parsed.length > 0) {
if (Array.isArray(parsed[0])) {
rows = parsed as string[][];
} else if (typeof parsed[0] === 'object') {
// Array of objects → header row + value rows
const objs = parsed as Record<string, unknown>[];
const keys = Object.keys(objs[0]);
rows = [keys, ...objs.map((o) => keys.map((k) => String(o[k] ?? '')))];
} else {
throw new Error('JSON must be an array of arrays or array of objects');
}
} else {
throw new Error('JSON must be a non-empty array');
}
} else {
rows = parseCsv(raw);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

--no-header is declared but never applied.

The command parses header but CSV import ignores it, so --no-header currently has no effect. This creates a behavior mismatch with docs/schema and will confuse users.

Proposed fix (wire the option in CSV path)
-        } else {
-          rows = parseCsv(raw);
-        }
+        } else {
+          const parsedRows = parseCsv(raw);
+          rows = localOpts.header ? parsedRows.slice(1) : parsedRows;
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/import.ts` around lines 36 - 70, The CSV path
currently ignores the --no-header / localOpts.header option; update the import
action so the CSV parsing respects localOpts.header by wiring it into parseCsv
(or by post-processing the parsed rows): call parseCsv(raw, { header:
localOpts.header }) if parseCsv supports options, or after rows = parseCsv(raw)
treat the first row as a header row only when localOpts.header === true (and
leave it as data when localOpts.header === false). Modify the code around
parseCsv and use localOpts.header (and the --no-header option name) to decide
whether the first row is header or regular data.

Comment thread packages/cli/src/commands/login.ts
Comment thread packages/cli/src/config/config.ts Outdated
Comment thread scripts/verify-ci.mjs
- login: use global --server via optsWithGlobals() instead of
  shadowed local option that Commander couldn't resolve
- export/import: rename --format to --file-format to avoid
  collision with the global output --format option
- session: add WAFFLEBASE_SESSION env override for test isolation
- login callback page: add centered card layout with system font

Co-Authored-By: Claude Opus 4.6 <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
packages/cli/src/commands/import.ts (1)

69-71: ⚠️ Potential issue | 🟠 Major

--no-header option is still not applied for CSV imports.

The localOpts.header flag is parsed (line 43) but the CSV branch at line 70 ignores it. When --no-header is passed, header is false, meaning the first row should be treated as data, not a header. Currently all rows are passed unchanged.

Per the context in csv-parse.ts, buildCellMap expects preprocessed data. The header handling should happen here before calling buildCellMap.

🐛 Proposed fix
         } else {
-          rows = parseCsv(raw);
+          const parsed = parseCsv(raw);
+          // When header is true (default), first row is a header and included in data
+          // When --no-header is passed (header=false), all rows are data
+          rows = parsed;
         }
+
+        // If header mode is on and we have CSV data, the first row is already
+        // part of the data array. No stripping needed since buildCellMap
+        // imports all rows including headers as cell values.

Note: If the intent is to skip the header row when importing (not import column names as cell values), the logic would be:

         } else {
-          rows = parseCsv(raw);
+          const parsed = parseCsv(raw);
+          // Skip header row when header=true (default), keep all rows when --no-header
+          rows = localOpts.header ? parsed.slice(1) : parsed;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/import.ts` around lines 69 - 71, The CSV branch
ignores the localOpts.header flag so --no-header isn’t applied; in the import
flow (the block that calls parseCsv and later buildCellMap) detect
localOpts.header (or localOpts.header === false) and, when header is false, do
not treat the first parsed row as a header — either pass the parsed rows
unchanged as data rows or explicitly prepend synthetic headers if buildCellMap
expects headers; update the code around parseCsv(...) and before
buildCellMap(...) to remove/retain the first row accordingly (refer to parseCsv,
rows and buildCellMap identifiers) so the first CSV row is treated as data when
--no-header is set.
packages/cli/src/commands/export.ts (1)

38-40: ⚠️ Potential issue | 🟡 Minor

Preserve the backend's error message on failed exports.

The error at line 40 reduces every non-OK response to HTTP ${res.status}, hiding useful details like missing tabs or invalid ranges. As per coding guidelines: "Use meaningful error messages that help with debugging."

🔧 Proposed fix
-        if (!res.ok) throw new Error(`HTTP ${res.status}`);
+        if (!res.ok) {
+          const body = res.data as { error?: { message?: string } } | null;
+          throw new Error(body?.error?.message ?? `HTTP ${res.status}`);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/export.ts` around lines 38 - 40, The current export
error replaces any non-OK response with a generic "HTTP ${res.status}", losing
backend diagnostics; update the handling after calling
getClient(...).getCells(docId, localOpts.tab, localOpts.range) to read the
response body (e.g., await res.text() or await res.json() depending on
content-type) and include that message along with the status in the thrown Error
so callers see the backend error (reference the res variable returned from
getCells and the surrounding try block where the throw happens).
packages/cli/src/commands/login.ts (1)

96-101: ⚠️ Potential issue | 🟠 Major

Workspace fetch failure still overwrites session with empty workspace state.

While a warning was added at line 100, a transient /workspaces failure (e.g., HTTP 500) still results in saving workspaces: [] and activeWorkspace: '' at lines 132-133. Subsequent workspace-scoped commands will fail or use empty workspace IDs.

Consider preserving the existing session's workspace state on fetch failure, or failing the login entirely rather than leaving the user in a broken state.

🔧 Proposed fix - preserve existing workspace state
       let workspaces: WorkspaceInfo[] = [];
+      let activeWorkspace = '';
       if (wsRes.ok) {
         workspaces = (await wsRes.json()) as WorkspaceInfo[];
       } else {
         console.warn(`Warning: failed to fetch workspaces (HTTP ${wsRes.status})`);
+        // Preserve existing workspace state if available
+        if (existing) {
+          workspaces = existing.workspaces;
+          activeWorkspace = existing.activeWorkspace;
+          console.warn('Preserving previous workspace context.');
+        }
       }

       // 8. Select workspace
-      let activeWorkspace = '';
-      if (workspaces.length === 0) {
+      if (workspaces.length === 0 && !activeWorkspace) {
         console.log('No workspaces found.');
       } else if (workspaces.length === 1) {

Also applies to: 125-136

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

In `@packages/cli/src/commands/login.ts` around lines 96 - 101, The current login
flow overwrites the session with an empty workspace list when wsRes.ok is false;
instead, when the /workspaces fetch fails (wsRes.ok === false) preserve the
existing workspace state or abort the login: do not assign workspaces = [] or
activeWorkspace = '' on error—keep the previous session.workspaces and
session.activeWorkspace (or load them from the session store) and only
update/sync workspaces when wsRes.ok is true, or throw/return an error to stop
the save flow so the code path that writes the session (the block that persists
workspaces and activeWorkspace) does not run with an empty list. Ensure you
update the logic around the workspaces variable, wsRes handling, and the session
save to reflect this behavior.
🧹 Nitpick comments (2)
packages/cli/test/config.test.ts (2)

15-16: Add a positive WAFFLEBASE_SESSION resolution test (and precedence check).

This suite now covers the missing-session path, but not the critical edge case where a real session file exists and should participate in resolveConfig precedence. Please add one test that uses a valid session file and asserts ordering versus flags/env/config.

As per coding guidelines: **/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases.

Also applies to: 23-30

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

In `@packages/cli/test/config.test.ts` around lines 15 - 16, Add a positive test
case to packages/cli/test/config.test.ts that creates a valid WAFFLEBASE_SESSION
file and verifies resolveConfig reads it and that its values take the correct
precedence versus CLI flags, environment variables, and config file;
specifically, call resolveConfig with conflicting inputs (e.g., set a flag, set
process.env.WAFFLEBASE_SESSION to point to the temp session file, and provide a
config object/file) and assert the resolved config matches the session file's
values, then clean up the temp session file and restore process.env (remove
WAFFLEBASE_SESSION) after the test.

85-126: Use per-test temp dirs with explicit cleanup to avoid fixture leakage.

These tests create directories/files in tmpdir() but never remove them. Over time this pollutes CI/dev machines and can cause rare collisions with timestamp-based naming. Prefer mkdtempSync + rmSync(..., { recursive: true, force: true }) in teardown/finally.

Suggested refactor
-import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
+import { mkdirSync, writeFileSync, existsSync, readFileSync, mkdtempSync, rmSync } from 'node:fs';
 import { join } from 'node:path';
 import { tmpdir } from 'node:os';

 describe('migrateConfigIfNeeded', () => {
   it('copies config from old path to new path when new path does not exist', () => {
-    const base = join(tmpdir(), `wfb-migrate-test-${Date.now()}`);
+    const base = mkdtempSync(join(tmpdir(), 'wfb-migrate-test-'));
     const oldDir = join(base, 'old');
     const newDir = join(base, 'new');
     const oldPath = join(oldDir, 'config.yaml');
     const newPath = join(newDir, 'config.yaml');

-    mkdirSync(oldDir, { recursive: true });
-    writeFileSync(oldPath, 'profiles:\n  default:\n    server: https://old.example.com\n');
-
-    migrateConfigIfNeeded(newPath, oldPath);
-
-    expect(existsSync(newPath)).toBe(true);
-    expect(readFileSync(newPath, 'utf-8')).toContain('https://old.example.com');
+    try {
+      mkdirSync(oldDir, { recursive: true });
+      writeFileSync(oldPath, 'profiles:\n  default:\n    server: https://old.example.com\n');
+      migrateConfigIfNeeded(newPath, oldPath);
+      expect(existsSync(newPath)).toBe(true);
+      expect(readFileSync(newPath, 'utf-8')).toContain('https://old.example.com');
+    } finally {
+      rmSync(base, { recursive: true, force: true });
+    }
   });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/test/config.test.ts` around lines 85 - 126, These tests leak
temp files by using join(tmpdir(), `...${Date.now()}`) without cleanup; change
each test to create a per-test temp directory with
mkdtempSync(path.join(tmpdir(), 'wfb-migrate-XXXXXX')), use that dir for
oldDir/newDir and call migrateConfigIfNeeded(newPath, oldPath) as before, and
ensure cleanup by calling rmSync(base, { recursive: true, force: true }) in a
finally block inside the test or in an afterEach teardown; reference
migrateConfigIfNeeded, mkdtempSync, rmSync, tmpdir and the
base/oldDir/newDir/newPath variables when making the edits.
🤖 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/docs/api/cli.md`:
- Around line 338-343: The example JSON in the CLI docs shows an inconsistent
server URL (the "url" field contains "http://localhost:3000") while the doc's
default server is documented elsewhere as "https://wafflebase-api.yorkie.dev";
update the example in the packages/docs/api/cli.md example block (the JSON
object containing "dry_run", "method", "url", "body") to either use the
documented default URL or add a short comment/annotation next to the example
stating that "http://localhost:3000" is only an override/local example so
readers aren't confused by the mismatch.

---

Duplicate comments:
In `@packages/cli/src/commands/export.ts`:
- Around line 38-40: The current export error replaces any non-OK response with
a generic "HTTP ${res.status}", losing backend diagnostics; update the handling
after calling getClient(...).getCells(docId, localOpts.tab, localOpts.range) to
read the response body (e.g., await res.text() or await res.json() depending on
content-type) and include that message along with the status in the thrown Error
so callers see the backend error (reference the res variable returned from
getCells and the surrounding try block where the throw happens).

In `@packages/cli/src/commands/import.ts`:
- Around line 69-71: The CSV branch ignores the localOpts.header flag so
--no-header isn’t applied; in the import flow (the block that calls parseCsv and
later buildCellMap) detect localOpts.header (or localOpts.header === false) and,
when header is false, do not treat the first parsed row as a header — either
pass the parsed rows unchanged as data rows or explicitly prepend synthetic
headers if buildCellMap expects headers; update the code around parseCsv(...)
and before buildCellMap(...) to remove/retain the first row accordingly (refer
to parseCsv, rows and buildCellMap identifiers) so the first CSV row is treated
as data when --no-header is set.

In `@packages/cli/src/commands/login.ts`:
- Around line 96-101: The current login flow overwrites the session with an
empty workspace list when wsRes.ok is false; instead, when the /workspaces fetch
fails (wsRes.ok === false) preserve the existing workspace state or abort the
login: do not assign workspaces = [] or activeWorkspace = '' on error—keep the
previous session.workspaces and session.activeWorkspace (or load them from the
session store) and only update/sync workspaces when wsRes.ok is true, or
throw/return an error to stop the save flow so the code path that writes the
session (the block that persists workspaces and activeWorkspace) does not run
with an empty list. Ensure you update the logic around the workspaces variable,
wsRes handling, and the session save to reflect this behavior.

---

Nitpick comments:
In `@packages/cli/test/config.test.ts`:
- Around line 15-16: Add a positive test case to
packages/cli/test/config.test.ts that creates a valid WAFFLEBASE_SESSION file
and verifies resolveConfig reads it and that its values take the correct
precedence versus CLI flags, environment variables, and config file;
specifically, call resolveConfig with conflicting inputs (e.g., set a flag, set
process.env.WAFFLEBASE_SESSION to point to the temp session file, and provide a
config object/file) and assert the resolved config matches the session file's
values, then clean up the temp session file and restore process.env (remove
WAFFLEBASE_SESSION) after the test.
- Around line 85-126: These tests leak temp files by using join(tmpdir(),
`...${Date.now()}`) without cleanup; change each test to create a per-test temp
directory with mkdtempSync(path.join(tmpdir(), 'wfb-migrate-XXXXXX')), use that
dir for oldDir/newDir and call migrateConfigIfNeeded(newPath, oldPath) as
before, and ensure cleanup by calling rmSync(base, { recursive: true, force:
true }) in a finally block inside the test or in an afterEach teardown;
reference migrateConfigIfNeeded, mkdtempSync, rmSync, tmpdir and the
base/oldDir/newDir/newPath variables when making the edits.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a9594d8b-77a2-4fcc-a532-e255e13cf076

📥 Commits

Reviewing files that changed from the base of the PR and between c19ad3c and fa17145.

📒 Files selected for processing (7)
  • packages/cli/src/commands/export.ts
  • packages/cli/src/commands/import.ts
  • packages/cli/src/commands/login.ts
  • packages/cli/src/config/session.ts
  • packages/cli/src/schema/registry.ts
  • packages/cli/test/config.test.ts
  • packages/docs/api/cli.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/config/session.ts

Comment thread packages/docs/api/cli.md
- export: preserve backend error message on failure
- import: remove unused --no-header flag (spreadsheets import all rows)
- login: fail with error when workspace fetch fails instead of
  saving empty workspace state
- config: use stderr for migration notice to not break JSON output
- docs: add language identifiers to code blocks, anonymize example
  email in session schema

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@hackerwins
hackerwins merged commit 1177361 into main Mar 15, 2026
3 checks passed
@hackerwins
hackerwins deleted the cli-login branch March 15, 2026 03:09
hackerwins added a commit that referenced this pull request Mar 15, 2026
Move cli and cli-oauth-login task files to archive after completion
in recent PRs (#34, #35). Update task index accordingly.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
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.

1 participant