Skip to content

refactor: migrate CLI to cac and fix monorepo workflow generation#85

Merged
LadyBluenotes merged 16 commits intomainfrom
cli-refactor
Mar 17, 2026
Merged

refactor: migrate CLI to cac and fix monorepo workflow generation#85
LadyBluenotes merged 16 commits intomainfrom
cli-refactor

Conversation

@LadyBluenotes
Copy link
Member

@LadyBluenotes LadyBluenotes commented Mar 16, 2026

Summary

Refactors the @tanstack/intent CLI from hand-rolled argument parsing to cac, decomposing a 745-line monolith into focused command modules. Also fixes monorepo workflow generation from #71. No user-facing API changes — all commands and flags remain the same.

Approach

CLI restructuring: The previous cli.ts mixed command routing, argument parsing, help text, validation, staleness orchestration, and error handling in one file. This PR splits it into:

  • cli.ts — entrypoint: creates the cac instance, registers commands, dispatches
  • cli-error.tsCliFailure type with symbol brand, fail() helper, isCliFailure() guard
  • cli-support.ts — shared utilities (scanIntentsOrFail, resolveStaleTargets, printWarnings, getMetaDir)
  • commands/*.ts — one file per command (list, meta, validate, stale, install, scaffold, edit-package-json, setup-github-actions)

Error handling: CliFailure uses a symbol brand (not duck-typing) to prevent false matches. The catch block handles CliFailure → clean message, Error → clean message, unknown → re-throw. fail() returns never for TypeScript control flow.

Monorepo setup: setup-github-actions now detects workspace roots, writes workflows there, and generates monorepo-aware watch paths. Avoids bogus paths like packages/root/src/** by detecting generic workspace names.

Dependency injection: runListCommand and runStaleCommand accept their async dependencies as parameters, improving testability. Commands use lazy import() to keep CLI startup fast.

Key Invariants

  • All commands signal failure exclusively through fail(), never process.exit()
  • CliFailure is branded with a unique symbol — structural duck-typing cannot produce false positives
  • cac parse expects process.argv format (two ignored prefix entries)
  • YAML validation errors include parse details (line/column) for user diagnostics

Non-goals

  • Did not add --verbose flag or stack trace output for debugging
  • Did not unify all dependency injection patterns across commands (some use DI, others use dynamic imports — both work)
  • Did not add unit tests for individual command modules (tests exercise main() end-to-end, which is resilient to internal refactoring)

Verification

pnpm --filter @tanstack/intent exec vitest run tests/cli.test.ts
pnpm --filter @tanstack/intent test:lib
pnpm --filter @tanstack/intent test:types
pnpm --filter @tanstack/intent test:smoke

Files changed

File Change
src/cli.ts Slimmed to entrypoint — cac setup, command registration, error catch
src/cli-error.ts New — CliFailure type with symbol brand, fail(), isCliFailure()
src/cli-support.ts New — shared utilities, printWarnings, stale target resolution
src/commands/*.ts New — one module per CLI command (8 files)
src/setup.ts Monorepo workspace detection, deriveWorkspacePackageName, watch paths
tests/cli.test.ts Updated for cac, added scaffold/edit-package-json/setup-github-actions/unknown command tests
tests/setup.test.ts Monorepo workflow generation test data
docs/cli/intent-setup.md Complete template variable list, monorepo docs

From #71

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Modular CLI with new commands (list, meta, validate, scaffold, stale, edit-package-json, setup-github-actions, install) and improved help/output modes
  • Bug Fixes

    • Monorepo-aware GitHub Actions workflow generation with correct repo/package fallbacks and generated watch paths
    • Improved CLI handling for unknown commands and centralized error reporting
  • Documentation

    • Updated setup guidance for GitHub Actions in monorepos and expanded variable substitution details

@changeset-bot
Copy link

changeset-bot bot commented Mar 16, 2026

🦋 Changeset detected

Latest commit: 185fb56

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tanstack/intent Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

Replaces the in-file Intent CLI with a cac-based command dispatcher, splits commands into modular handlers, adds CLI error/support utilities, and updates setup logic and template generation to be monorepo-aware (workspace root detection, PACKAGE_LABEL/PAYLOAD_PACKAGE/WATCH_PATHS, and repo-level workflow placement).

Changes

Cohort / File(s) Summary
CLI Core & Support
packages/intent/src/cli.ts, packages/intent/src/cli-error.ts, packages/intent/src/cli-support.ts
Swap monolithic CLI for cac builder; add CliFailure type/thrower and helpers for meta dir, scanning, and stale-target resolution.
Command Handlers
packages/intent/src/commands/list.ts, .../meta.ts, .../validate.ts, .../scaffold.ts, .../stale.ts, .../install.ts, .../edit-package-json.ts, .../setup-github-actions.ts
Introduce modular runXCommand implementations moved out of main CLI; each handles parsing/output for its responsibility.
Setup & Monorepo Pathing
packages/intent/src/setup.ts
Detect workspace root and generic workspace names; derive packageName/packageLabel/PAYLOAD_PACKAGE; compute monorepo-aware DOCS_PATH, SRC_PATH, and WATCH_PATHS; write workflows at workspace root.
Intent Library Import
packages/intent/src/intent-library.ts
Update INSTALL_PROMPT import to ./commands/install.js.
Package Manifest
packages/intent/package.json
Add cac dependency and test:smoke script.
Docs
docs/cli/intent-setup.md
Document monorepo-aware setup behavior, new substitution variables (PACKAGE_LABEL, PAYLOAD_PACKAGE, WATCH_PATHS) and repo-root guidance.
Tests & Changeset
packages/intent/tests/cli.test.ts, packages/intent/tests/setup.test.ts, .changeset/thin-rings-behave.md
Update tests to use runtime help output, adjust fixtures for workspace root name, assert monorepo-aware workflow substitutions, and add changeset entry.
Misc / Build & Wiring
packages/intent/src/intent-library.ts, tests imports, dynamic imports across commands
Rewire dynamic imports and adjust test import sites to match new command modules.

Sequence Diagram(s)

sequenceDiagram
  participant User as Client
  participant CLI as Intent CLI
  participant Cmd as Command Module
  participant FS as File System
  participant GH as Workflow Renderer

  User->>CLI: run `intent setup-github-actions` (or other command)
  CLI->>Cmd: parse args (cac) and dispatch to runSetupGithubActionsCommand
  Cmd->>FS: detect workspace root, read package.json(s)
  FS-->>Cmd: return workspace & package info
  Cmd->>GH: render workflow templates with substitutions (PACKAGE_LABEL, PAYLOAD_PACKAGE, WATCH_PATHS)
  GH-->>Cmd: rendered workflow content
  Cmd->>FS: write workflows to workspace root (skip if exists)
  Cmd-->>CLI: return status
  CLI-->>User: print status / messages
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • LadyBluenotes
  • tannerlinsley

Poem

🐇
I hopped through modules, split each command,
Turned tangled switches into cac’s clear land.
Workspaces wink, workflows find their way,
I planted watch paths where packages play,
Now I nibble code- carrots at end of day.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% 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 PR title clearly and concisely summarizes the two main changes: CLI migration to cac and monorepo workflow generation fixes.
Description check ✅ Passed The PR description comprehensively covers the changes, approach, key invariants, verification steps, and file modifications, exceeding template requirements.

✏️ 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
  • Commit unit tests in branch cli-refactor
📝 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.

Tip

CodeRabbit can generate a title for your PR based on the changes with custom instructions.

Set the reviews.auto_title_instructions setting to generate a title for your PR based on the changes in the PR with custom instructions.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 16, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@tanstack/intent@85

commit: 185fb56

Copy link

@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: 8

🧹 Nitpick comments (1)
packages/intent/src/commands/list.ts (1)

44-51: Defer display.js import until non-JSON output path.

Line 44 eagerly imports table/tree helpers even when --json returns early. Moving the import below the JSON branch keeps the fast path leaner.

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

In `@packages/intent/src/commands/list.ts` around lines 44 - 51, The import of
computeSkillNameWidth, printSkillTree, and printTable from '../display.js' is
done before checking options.json, causing unnecessary module loading on the
JSON fast path; to fix, move the dynamic import of those symbols to after the if
(options.json) { ... return } block so you only await import('../display.js')
when options.json is false (keep the call to scanIntentsOrFail and the JSON
output unchanged and reference computeSkillNameWidth, printSkillTree, printTable
only after that relocated import).
🤖 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/intent/package.json`:
- Line 41: The smoke-test script uses POSIX redirection ("> /dev/null") which
breaks on Windows; update the "test:smoke" npm script to use a cross-platform
Node-based suppression instead of shell redirection. Replace the current value
of the "test:smoke" script with a Node one-liner that runs "dist/cli.mjs --help"
via child_process (e.g., spawnSync or execSync) with stdio set to "ignore", so
the command exits successfully on all platforms while suppressing output; locate
the "test:smoke" script entry in package.json and update it accordingly.

In `@packages/intent/src/cli-support.ts`:
- Around line 40-42: The path resolution for targetDir uses join(process.cwd(),
targetDir) which incorrectly concatenates when targetDir is absolute; change to
use resolve(process.cwd(), targetDir) (or simply resolve(targetDir) if
appropriate) so absolute targetDir resets to the filesystem root; update the
variable resolvedRoot and any related tests/usages to import/use path.resolve
instead of path.join and ensure behavior remains the same for relative inputs.

In `@packages/intent/src/commands/meta.ts`:
- Around line 61-62: The usage footer prints a hard-coded path to SKILL.md which
can be incorrect; change it to derive the path from the existing metaDir
variable used in this module. Replace the second console.log that currently
prints 'node_modules/@tanstack/intent/meta/<name>/SKILL.md' with one that
constructs the path using metaDir and the meta name (the same value used
elsewhere in this file), e.g. join metaDir with the meta name and 'SKILL.md'
before logging so the hint always reflects the real install location.
- Around line 37-59: Wrap filesystem reads and frontmatter parsing in try/catch
so one bad skill doesn't abort the whole listing: guard the initial
readdirSync(metaDir, ...) call with try/catch and log/exit only on fatal errors,
and inside the for (const entry of entries) loop wrap the
parseFrontmatter(skillFile) (and any read of SKILL.md) in a try/catch; if
parseFrontmatter throws or the file is unreadable/invalid, console.warn a brief
message including entry.name and the error, then continue to the next entry and
skip printing that skill. Ensure you still build shortDesc from a safe
description string and only call .replace/.slice on validated strings returned
from parseFrontmatter.

In `@packages/intent/src/commands/scaffold.ts`:
- Line 63: Update the scaffold prompt text to use the consistent artifact
directory name `_artifacts/` instead of `artifacts`; locate the prompt string in
packages/intent/src/commands/scaffold.ts (the scaffold command/prompt generation
code that emits the step "2. Commit skills/ and artifacts") and change that
occurrence to "2. Commit skills/ and _artifacts/" so it matches the earlier
guidance and avoids operator confusion.

In `@packages/intent/src/commands/stale.ts`:
- Around line 12-20: The early return when reports.length === 0 bypasses JSON
output; update the logic in the stale command so that the options.json branch
runs before the plain-text early exit (or explicitly emit JSON empty array when
options.json is true). Concretely, adjust the block that checks reports.length
=== 0 and the options.json handling so that when options.json is true you always
call console.log(JSON.stringify(reports, null, 2)) (yielding [] for an empty
reports array) and then return; keep the existing plain text "No intent-enabled
packages found." only for the non-JSON path.

In `@packages/intent/src/commands/validate.ts`:
- Around line 66-79: The monorepo detection in isMonorepoPkg prematurely returns
false when encountering a parent package.json that exists but lacks workspaces;
change the logic so that after reading/parsing parent (parentPkg -> parent) you
only return true when Array.isArray(parent.workspaces) ||
parent.workspaces?.packages is truthy, otherwise continue the ancestor loop (do
not return false), and likewise on JSON parse errors (catch) continue to the
next ancestor instead of returning false; this ensures isMonorepoPkg checks
higher-up package.json files before deciding it is not a monorepo.
- Around line 154-167: The code currently assumes parseYaml(match[1]) returns an
object; instead after parsing (parseYaml) validate the type of fm by checking
typeof fm === 'object' && fm !== null && !Array.isArray(fm); if that check fails
push errors.push({ file: rel, message: 'Invalid YAML frontmatter' }) and
continue, otherwise cast fm to Record<string, unknown> and then perform the
existing checks for fm.name and fm.description (optionally tighten those checks
to typeof fm.name !== 'string' / typeof fm.description !== 'string' if you want
to require strings). This uses the existing symbols parseYaml, fm, errors, rel
and the name/description checks in validate.ts.

---

Nitpick comments:
In `@packages/intent/src/commands/list.ts`:
- Around line 44-51: The import of computeSkillNameWidth, printSkillTree, and
printTable from '../display.js' is done before checking options.json, causing
unnecessary module loading on the JSON fast path; to fix, move the dynamic
import of those symbols to after the if (options.json) { ... return } block so
you only await import('../display.js') when options.json is false (keep the call
to scanIntentsOrFail and the JSON output unchanged and reference
computeSkillNameWidth, printSkillTree, printTable only after that relocated
import).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 03fb6035-0f69-4455-9349-c387f59c5188

📥 Commits

Reviewing files that changed from the base of the PR and between 18a8fc1 and bb0608d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • .changeset/thin-rings-behave.md
  • docs/cli/intent-setup.md
  • packages/intent/package.json
  • packages/intent/src/cli-error.ts
  • packages/intent/src/cli-support.ts
  • packages/intent/src/cli.ts
  • packages/intent/src/commands/edit-package-json.ts
  • packages/intent/src/commands/install.ts
  • packages/intent/src/commands/list.ts
  • packages/intent/src/commands/meta.ts
  • packages/intent/src/commands/scaffold.ts
  • packages/intent/src/commands/setup-github-actions.ts
  • packages/intent/src/commands/stale.ts
  • packages/intent/src/commands/validate.ts
  • packages/intent/src/intent-library.ts
  • packages/intent/src/setup.ts
  • packages/intent/tests/cli.test.ts
  • packages/intent/tests/setup.test.ts

"scripts": {
"prepack": "npm run build",
"build": "tsdown src/index.ts src/cli.ts src/setup.ts src/intent-library.ts src/library-scanner.ts --format esm --dts",
"test:smoke": "pnpm run build && node dist/cli.mjs --help > /dev/null",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use a cross-platform smoke test command.

Line 41 uses > /dev/null, which is POSIX-specific and fails in Windows shells. Prefer keeping output or using a Node-based suppression strategy.

Suggested minimal fix
-    "test:smoke": "pnpm run build && node dist/cli.mjs --help > /dev/null",
+    "test:smoke": "pnpm run build && node dist/cli.mjs --help",
📝 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
"test:smoke": "pnpm run build && node dist/cli.mjs --help > /dev/null",
"test:smoke": "pnpm run build && node dist/cli.mjs --help",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/package.json` at line 41, The smoke-test script uses POSIX
redirection ("> /dev/null") which breaks on Windows; update the "test:smoke" npm
script to use a cross-platform Node-based suppression instead of shell
redirection. Replace the current value of the "test:smoke" script with a Node
one-liner that runs "dist/cli.mjs --help" via child_process (e.g., spawnSync or
execSync) with stdio set to "ignore", so the command exits successfully on all
platforms while suppressing output; locate the "test:smoke" script entry in
package.json and update it accordingly.

Comment on lines +40 to +42
const resolvedRoot = targetDir
? join(process.cwd(), targetDir)
: process.cwd()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Node.js, what is the behavior difference between path.join(process.cwd(), absolutePath) and path.resolve(process.cwd(), absolutePath), especially when absolutePath starts with "/"?

💡 Result:

  • path.join(process.cwd(), absolutePath) always concatenates the segments and then normalizes. So if absolutePath starts with /, it’s treated like “a segment that happens to begin with a separator”, not as “reset to filesystem root”. Example (POSIX): path.join('/home/me/app', '/tmp')/home/me/app/tmp (the // becomes / during normalization). [1]

  • path.resolve(process.cwd(), absolutePath) resolves segments right-to-left and stops once it hits an absolute path; any earlier segments (like process.cwd()) are discarded. So if absolutePath starts with /, it wins. Example (POSIX): path.resolve('/home/me/app', '/tmp/file/')/tmp/file. [2]

So when absolutePath starts with /:

  • join(cwd, absolutePath) ≈ “force it under cwd
  • resolve(cwd, absolutePath) ≈ “use absolutePath as-is (absolute overrides)”

[1] (nodejs.org)
[2] (nodejs.org)

Citations:


🏁 Script executed:

cat -n packages/intent/src/cli-support.ts | head -50

Repository: TanStack/intent

Length of output: 1816


🏁 Script executed:

rg -A 2 "resolveStaleTargets" --type ts

Repository: TanStack/intent

Length of output: 1028


🏁 Script executed:

grep -A 10 -B 5 "targetDir" packages/intent/src/cli.ts | head -40

Repository: TanStack/intent

Length of output: 531


Use resolve() instead of join() for targetDir path resolution.

When targetDir is an absolute path (e.g., stale /tmp/project), join(process.cwd(), targetDir) concatenates the segments incorrectly. The / in the absolute path is treated as a regular path segment, not a filesystem root reset. Use resolve() instead, which correctly handles absolute paths by stopping resolution once an absolute component is encountered.

🔧 Proposed fix
-import { dirname, join, relative } from 'node:path'
+import { dirname, join, relative, resolve } from 'node:path'
@@
-  const resolvedRoot = targetDir
-    ? join(process.cwd(), targetDir)
-    : process.cwd()
+  const resolvedRoot = targetDir
+    ? resolve(process.cwd(), targetDir)
+    : process.cwd()
📝 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
const resolvedRoot = targetDir
? join(process.cwd(), targetDir)
: process.cwd()
import { dirname, join, relative, resolve } from 'node:path'
// ... other code ...
const resolvedRoot = targetDir
? resolve(process.cwd(), targetDir)
: process.cwd()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/cli-support.ts` around lines 40 - 42, The path resolution
for targetDir uses join(process.cwd(), targetDir) which incorrectly concatenates
when targetDir is absolute; change to use resolve(process.cwd(), targetDir) (or
simply resolve(targetDir) if appropriate) so absolute targetDir resets to the
filesystem root; update the variable resolvedRoot and any related tests/usages
to import/use path.resolve instead of path.join and ensure behavior remains the
same for relative inputs.

Comment on lines +37 to +59
const entries = readdirSync(metaDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.filter((entry) => existsSync(join(metaDir, entry.name, 'SKILL.md')))

if (entries.length === 0) {
console.log('No meta-skills found.')
return
}

console.log('Meta-skills (for library maintainers):\n')

for (const entry of entries) {
const skillFile = join(metaDir, entry.name, 'SKILL.md')
const fm = parseFrontmatter(skillFile)
let description = ''
if (typeof fm?.description === 'string') {
description = fm.description.replace(/\s+/g, ' ').trim()
}

const shortDesc =
description.length > 60 ? `${description.slice(0, 57)}...` : description
console.log(` ${entry.name.padEnd(28)} ${shortDesc}`)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make listing mode tolerant to unreadable/invalid SKILL.md files.

A single readdirSync/parseFrontmatter failure currently aborts the whole intent meta listing. This should degrade gracefully so one bad skill doesn’t hide all others.

Proposed hardening
-  const entries = readdirSync(metaDir, { withFileTypes: true })
-    .filter((entry) => entry.isDirectory())
-    .filter((entry) => existsSync(join(metaDir, entry.name, 'SKILL.md')))
+  let entries
+  try {
+    entries = readdirSync(metaDir, { withFileTypes: true })
+      .filter((entry) => entry.isDirectory())
+      .filter((entry) => existsSync(join(metaDir, entry.name, 'SKILL.md')))
+  } catch (err) {
+    const msg = err instanceof Error ? err.message : String(err)
+    fail(`Failed to read meta-skills directory: ${msg}`)
+  }

   if (entries.length === 0) {
     console.log('No meta-skills found.')
     return
   }

   console.log('Meta-skills (for library maintainers):\n')

   for (const entry of entries) {
     const skillFile = join(metaDir, entry.name, 'SKILL.md')
-    const fm = parseFrontmatter(skillFile)
+    let fm: { description?: unknown } | undefined
+    try {
+      fm = parseFrontmatter(skillFile)
+    } catch {
+      fm = undefined
+    }
     let description = ''
     if (typeof fm?.description === 'string') {
       description = fm.description.replace(/\s+/g, ' ').trim()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/commands/meta.ts` around lines 37 - 59, Wrap filesystem
reads and frontmatter parsing in try/catch so one bad skill doesn't abort the
whole listing: guard the initial readdirSync(metaDir, ...) call with try/catch
and log/exit only on fatal errors, and inside the for (const entry of entries)
loop wrap the parseFrontmatter(skillFile) (and any read of SKILL.md) in a
try/catch; if parseFrontmatter throws or the file is unreadable/invalid,
console.warn a brief message including entry.name and the error, then continue
to the next entry and skip printing that skill. Ensure you still build shortDesc
from a safe description string and only call .replace/.slice on validated
strings returned from parseFrontmatter.

Comment on lines +61 to +62
console.log('\nUsage: load the SKILL.md into your AI agent conversation.')
console.log('Path: node_modules/@tanstack/intent/meta/<name>/SKILL.md')
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use metaDir for the path hint instead of a hard-coded install path.

The current footer can print the wrong location when invoked from non-node_modules contexts. Build the hint from metaDir to keep it accurate.

Suggested adjustment
-  console.log('Path: node_modules/@tanstack/intent/meta/<name>/SKILL.md')
+  console.log(`Path: ${join(metaDir, '<name>', 'SKILL.md')}`)
📝 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
console.log('\nUsage: load the SKILL.md into your AI agent conversation.')
console.log('Path: node_modules/@tanstack/intent/meta/<name>/SKILL.md')
console.log('\nUsage: load the SKILL.md into your AI agent conversation.')
console.log(`Path: ${join(metaDir, '<name>', 'SKILL.md')}`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/commands/meta.ts` around lines 61 - 62, The usage footer
prints a hard-coded path to SKILL.md which can be incorrect; change it to derive
the path from the existing metaDir variable used in this module. Replace the
second console.log that currently prints
'node_modules/@tanstack/intent/meta/<name>/SKILL.md' with one that constructs
the path using metaDir and the meta name (the same value used elsewhere in this
file), e.g. join metaDir with the meta name and 'SKILL.md' before logging so the
hint always reflects the real install location.

## After all skills are generated

1. Run \`intent validate\` in each package directory
2. Commit skills/ and artifacts
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep artifact directory naming consistent in the prompt.

Line 63 says artifacts, while earlier guidance uses _artifacts/ (Line 22). This can cause incorrect operator action.

Suggested fix
-2. Commit skills/ and artifacts
+2. Commit skills/ and _artifacts/
📝 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
2. Commit skills/ and artifacts
2. Commit skills/ and _artifacts/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/commands/scaffold.ts` at line 63, Update the scaffold
prompt text to use the consistent artifact directory name `_artifacts/` instead
of `artifacts`; locate the prompt string in
packages/intent/src/commands/scaffold.ts (the scaffold command/prompt generation
code that emits the step "2. Commit skills/ and artifacts") and change that
occurrence to "2. Commit skills/ and _artifacts/" so it matches the earlier
guidance and avoids operator confusion.

Comment on lines +12 to +20
if (reports.length === 0) {
console.log('No intent-enabled packages found.')
return
}

if (options.json) {
console.log(JSON.stringify(reports, null, 2))
return
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

--json mode should return JSON even when no reports exist.

Line 12 currently short-circuits before JSON handling, so intent stale --json can emit plain text instead of [].

🔧 Proposed fix
-  if (reports.length === 0) {
-    console.log('No intent-enabled packages found.')
-    return
-  }
-
   if (options.json) {
     console.log(JSON.stringify(reports, null, 2))
     return
   }
+
+  if (reports.length === 0) {
+    console.log('No intent-enabled packages found.')
+    return
+  }
📝 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
if (reports.length === 0) {
console.log('No intent-enabled packages found.')
return
}
if (options.json) {
console.log(JSON.stringify(reports, null, 2))
return
}
if (options.json) {
console.log(JSON.stringify(reports, null, 2))
return
}
if (reports.length === 0) {
console.log('No intent-enabled packages found.')
return
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/commands/stale.ts` around lines 12 - 20, The early return
when reports.length === 0 bypasses JSON output; update the logic in the stale
command so that the options.json branch runs before the plain-text early exit
(or explicitly emit JSON empty array when options.json is true). Concretely,
adjust the block that checks reports.length === 0 and the options.json handling
so that when options.json is true you always call
console.log(JSON.stringify(reports, null, 2)) (yielding [] for an empty reports
array) and then return; keep the existing plain text "No intent-enabled packages
found." only for the non-JSON path.

Comment on lines +66 to +79
const isMonorepoPkg = (() => {
let dir = join(root, '..')
for (let i = 0; i < 5; i++) {
const parentPkg = join(dir, 'package.json')
if (existsSync(parentPkg)) {
try {
const parent = JSON.parse(readFileSync(parentPkg, 'utf8'))
return (
Array.isArray(parent.workspaces) || parent.workspaces?.packages
)
} catch {
return false
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Monorepo detection returns too early on non-workspace parent package.json.

At Lines 70-78, if a parent package.json exists but has no workspaces, the function returns false immediately. That skips checking higher ancestors and can produce false packaging warnings in nested repos.

💡 Suggested fix
     const isMonorepoPkg = (() => {
       let dir = join(root, '..')
       for (let i = 0; i < 5; i++) {
         const parentPkg = join(dir, 'package.json')
         if (existsSync(parentPkg)) {
           try {
             const parent = JSON.parse(readFileSync(parentPkg, 'utf8'))
-            return (
-              Array.isArray(parent.workspaces) || parent.workspaces?.packages
-            )
+            if (
+              Array.isArray(parent.workspaces) ||
+              (parent.workspaces &&
+                typeof parent.workspaces === 'object' &&
+                'packages' in parent.workspaces)
+            ) {
+              return true
+            }
           } catch {
-            return false
+            // ignore malformed intermediate package.json and continue walking up
           }
         }
         const next = dirname(dir)
         if (next === dir) break
         dir = next
       }
       return false
     })()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/commands/validate.ts` around lines 66 - 79, The monorepo
detection in isMonorepoPkg prematurely returns false when encountering a parent
package.json that exists but lacks workspaces; change the logic so that after
reading/parsing parent (parentPkg -> parent) you only return true when
Array.isArray(parent.workspaces) || parent.workspaces?.packages is truthy,
otherwise continue the ancestor loop (do not return false), and likewise on JSON
parse errors (catch) continue to the next ancestor instead of returning false;
this ensures isMonorepoPkg checks higher-up package.json files before deciding
it is not a monorepo.

Comment on lines +154 to +167
let fm: Record<string, unknown>
try {
fm = parseYaml(match[1]) as Record<string, unknown>
} catch {
errors.push({ file: rel, message: 'Invalid YAML frontmatter' })
continue
}

if (!fm.name) {
errors.push({ file: rel, message: 'Missing required field: name' })
}
if (!fm.description) {
errors.push({ file: rel, message: 'Missing required field: description' })
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

For npm package yamlversion 2.7.0, what canparse()return for YAML inputs likenull, a string scalar, and a sequence? Is a mapping/object guaranteed?

💡 Result:

In yaml@2.7.0, parse() returns the JavaScript value corresponding to the root node type of the YAML document (it’s not guaranteed to be a mapping/object) [1][2]:

  • YAML null (e.g. null, ~, or an empty value) → null [1][2]
  • A string scalar (e.g. hello) → a JS scalar (typically a string; other scalars may become boolean/number depending on schema) [1][2]
  • A sequence (e.g. - a\n- b) → a JS Array [1][2]
  • A mapping (e.g. a: 1) → a JS plain object [1][2]

So: no, a mapping/object is not guaranteedparse() can return null, a primitive, an array, or an object depending on the YAML input [1][2].

Sources: [1] eemeli.org/yaml docs for YAML.parse return behavior, [2] yaml@2.7.0 TypeScript declarations (dist/public-api.d.ts) on UNPKG.


Guard parsed YAML type before accessing frontmatter fields.

Line 156 casts parseYaml() result to Record<string, unknown>, but yaml@2.7.0 parse() returns the root YAML node type without guarantee of an object—it can return null, a scalar, or an array. Accessing fm.name or fm.description on a non-object will throw a TypeError at runtime.

Suggested fix
-    let fm: Record<string, unknown>
+    let fm: Record<string, unknown>
     try {
-      fm = parseYaml(match[1]) as Record<string, unknown>
+      const parsed = parseYaml(match[1])
+      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
+        errors.push({
+          file: rel,
+          message: 'Frontmatter must be a YAML mapping/object',
+        })
+        continue
+      }
+      fm = parsed as Record<string, unknown>
     } catch {
       errors.push({ file: rel, message: 'Invalid YAML frontmatter' })
       continue
     }
📝 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
let fm: Record<string, unknown>
try {
fm = parseYaml(match[1]) as Record<string, unknown>
} catch {
errors.push({ file: rel, message: 'Invalid YAML frontmatter' })
continue
}
if (!fm.name) {
errors.push({ file: rel, message: 'Missing required field: name' })
}
if (!fm.description) {
errors.push({ file: rel, message: 'Missing required field: description' })
}
let fm: Record<string, unknown>
try {
const parsed = parseYaml(match[1])
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
errors.push({
file: rel,
message: 'Frontmatter must be a YAML mapping/object',
})
continue
}
fm = parsed as Record<string, unknown>
} catch {
errors.push({ file: rel, message: 'Invalid YAML frontmatter' })
continue
}
if (!fm.name) {
errors.push({ file: rel, message: 'Missing required field: name' })
}
if (!fm.description) {
errors.push({ file: rel, message: 'Missing required field: description' })
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/commands/validate.ts` around lines 154 - 167, The code
currently assumes parseYaml(match[1]) returns an object; instead after parsing
(parseYaml) validate the type of fm by checking typeof fm === 'object' && fm !==
null && !Array.isArray(fm); if that check fails push errors.push({ file: rel,
message: 'Invalid YAML frontmatter' }) and continue, otherwise cast fm to
Record<string, unknown> and then perform the existing checks for fm.name and
fm.description (optionally tighten those checks to typeof fm.name !== 'string' /
typeof fm.description !== 'string' if you want to require strings). This uses
the existing symbols parseYaml, fm, errors, rel and the name/description checks
in validate.ts.

Copy link

@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.

♻️ Duplicate comments (1)
packages/intent/package.json (1)

41-41: ⚠️ Potential issue | 🟡 Minor

Make smoke test script shell-agnostic (still POSIX-specific at Line 41).

> /dev/null is not portable to Windows shells. This was previously flagged and still appears unresolved.

Suggested minimal fix
-    "test:smoke": "pnpm run build && node dist/cli.mjs --help > /dev/null",
+    "test:smoke": "pnpm run build && node dist/cli.mjs --help",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/package.json` at line 41, The "test:smoke" script currently
uses a POSIX-only redirection ("> /dev/null") which breaks on Windows; change
the script to invoke Node directly and discard output without shell redirection
— e.g. replace the current command for "test:smoke" in package.json with a Node
one-liner that runs the CLI and ignores stdio (for example using
child_process.spawnSync('node',['dist/cli.mjs','--help'],{stdio:'ignore'}) in a
node -e invocation) so it works cross-platform and does not rely on /dev/null.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/intent/package.json`:
- Line 41: The "test:smoke" script currently uses a POSIX-only redirection (">
/dev/null") which breaks on Windows; change the script to invoke Node directly
and discard output without shell redirection — e.g. replace the current command
for "test:smoke" in package.json with a Node one-liner that runs the CLI and
ignores stdio (for example using
child_process.spawnSync('node',['dist/cli.mjs','--help'],{stdio:'ignore'}) in a
node -e invocation) so it works cross-platform and does not rely on /dev/null.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 380300a8-60bb-4b85-b44a-1aeded9d1653

📥 Commits

Reviewing files that changed from the base of the PR and between bb0608d and fce70da.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • packages/intent/package.json
  • packages/intent/tests/cli.test.ts

KyleAMathews and others added 2 commits March 16, 2026 16:42
- Brand CliFailure with symbol to prevent duck-typing false positives
- Handle CACError in catch block for clean CLI error output
- Fix unsafe (err as Error).message cast in scanIntentsOrFail
- Include YAML parse error details in validation messages
- Add monorepo artifact rationale comment in validate.ts
- Fix misleading src path comment in setup.ts
- Complete template variable list in docs
- Add comment explaining cac double-argument quirk
- Extract shared printWarnings to cli-support module
- Move ValidationError interface to module scope
- Extract isInsideMonorepo helper from IIFE

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

@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 (1)
packages/intent/src/commands/validate.ts (1)

148-161: ⚠️ Potential issue | 🔴 Critical

Validate the parsed frontmatter shape before reading name or description.

yaml.parse() is not guaranteed to return an object. If the frontmatter root is null, this branch throws before you can report a validation error.

Suggested fix
     let fm: Record<string, unknown>
     try {
-      fm = parseYaml(match[1]) as Record<string, unknown>
+      const parsed = parseYaml(match[1])
+      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
+        errors.push({ file: rel, message: 'Invalid YAML frontmatter' })
+        continue
+      }
+      fm = parsed as Record<string, unknown>
     } catch (err) {
       const detail = err instanceof Error ? err.message : String(err)
       errors.push({ file: rel, message: `Invalid YAML frontmatter: ${detail}` })
       continue
     }
For the npm package `yaml`, does `parse()` guarantee an object result, or can it return `null`, scalars, or arrays depending on the document root? Please use the docs and type declarations for the version used in this repository.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/commands/validate.ts` around lines 148 - 161, The parsed
frontmatter result (fm from parseYaml(match[1])) can be null or a non-object
(scalar/array), so before accessing fm.name or fm.description validate that fm
is an object (e.g., typeof fm === 'object' && fm !== null &&
!Array.isArray(fm')) and if not, push an errors entry for rel like "Invalid
frontmatter: expected mapping/object" and continue; then safely read fm.name and
fm.description (with appropriate type guards or casting) and keep the existing
missing-field error pushes unchanged.
🤖 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/intent/src/commands/list.ts`:
- Around line 93-99: Replace the hard-coded node_modules meta path printed by
the console.log call that outputs "  Load:
node_modules/@tanstack/intent/meta/feedback-collection/SKILL.md" with an
install-location-agnostic hint; change the message emitted by the console.log
statement to point users to run the CLI command "intent meta
feedback-collection" (or programmatically resolve the meta path if you prefer)
so the output no longer assumes a node_modules layout. Locate the console.log
that prints the "Load: ..." string in list.ts and update its text accordingly or
replace it with a resolved path lookup before logging.

In `@packages/intent/src/commands/scaffold.ts`:
- Around line 23-25: The post-generation checklist is missing a repo-root
validation pass for monorepos: when the scaffold logic detects a workspace
(monorepo) it should also validate artifact files placed at the workspace root
(_artifacts/domain_map.yaml and _artifacts/skill_tree.yaml). Update the function
that builds the checklist (e.g., postGenerationChecklist or scaffoldCommand) to
add a step that either invokes the existing validation routine (the same code
used by package-level checks, e.g., intent validate) against the workspace root
or directly calls the domain/skill validation helpers to check
_artifacts/domain_map.yaml and _artifacts/skill_tree.yaml; ensure the new step
only runs when monorepo/workspace detection returns true.

---

Duplicate comments:
In `@packages/intent/src/commands/validate.ts`:
- Around line 148-161: The parsed frontmatter result (fm from
parseYaml(match[1])) can be null or a non-object (scalar/array), so before
accessing fm.name or fm.description validate that fm is an object (e.g., typeof
fm === 'object' && fm !== null && !Array.isArray(fm')) and if not, push an
errors entry for rel like "Invalid frontmatter: expected mapping/object" and
continue; then safely read fm.name and fm.description (with appropriate type
guards or casting) and keep the existing missing-field error pushes unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e9a39637-0341-4e73-9c09-67f7b86b0783

📥 Commits

Reviewing files that changed from the base of the PR and between fce70da and 9361932.

📒 Files selected for processing (9)
  • docs/cli/intent-setup.md
  • packages/intent/src/cli-error.ts
  • packages/intent/src/cli-support.ts
  • packages/intent/src/cli.ts
  • packages/intent/src/commands/list.ts
  • packages/intent/src/commands/meta.ts
  • packages/intent/src/commands/scaffold.ts
  • packages/intent/src/commands/validate.ts
  • packages/intent/src/setup.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/intent/src/cli-support.ts

Comment on lines +93 to +99
console.log('Feedback:')
console.log(
' Submit feedback on skill usage to help maintainers improve the skills.',
)
console.log(
' Load: node_modules/@tanstack/intent/meta/feedback-collection/SKILL.md',
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use an install-location-agnostic feedback hint.

This hard-coded node_modules/... path breaks for global, linked, and npx installs. Point users at intent meta feedback-collection or derive the real meta path instead.

Suggested adjustment
   console.log(
-    '  Load: node_modules/@tanstack/intent/meta/feedback-collection/SKILL.md',
+    '  Run: intent meta feedback-collection',
   )
📝 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
console.log('Feedback:')
console.log(
' Submit feedback on skill usage to help maintainers improve the skills.',
)
console.log(
' Load: node_modules/@tanstack/intent/meta/feedback-collection/SKILL.md',
)
console.log('Feedback:')
console.log(
' Submit feedback on skill usage to help maintainers improve the skills.',
)
console.log(
' Run: intent meta feedback-collection',
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/commands/list.ts` around lines 93 - 99, Replace the
hard-coded node_modules meta path printed by the console.log call that outputs "
Load: node_modules/@tanstack/intent/meta/feedback-collection/SKILL.md" with an
install-location-agnostic hint; change the message emitted by the console.log
statement to point users to run the CLI command "intent meta
feedback-collection" (or programmatically resolve the meta path if you prefer)
so the output no longer assumes a node_modules layout. Locate the console.log
that prints the "Load: ..." string in list.ts and update its text accordingly or
replace it with a resolved path lookup before logging.

Comment on lines +23 to +25
4. For monorepos:
- Domain map artifacts go at the REPO ROOT: _artifacts/
- Skills go INSIDE EACH PACKAGE: packages/<pkg>/skills/
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The monorepo checklist skips repo-root artifact validation.

Earlier steps place monorepo artifacts under the workspace-root _artifacts/, but the post-generation checklist only tells maintainers to run intent validate inside each package. That leaves domain_map.yaml and skill_tree.yaml unchecked unless they add a separate root-level validation pass.

Suggested adjustment
 ## After all skills are generated

-1. Run `intent validate` in each package directory
+1. For monorepos, run `intent validate .` once at the repo root to validate `_artifacts/`
+2. Run `intent validate` in each package directory

Also applies to: 64-64

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

In `@packages/intent/src/commands/scaffold.ts` around lines 23 - 25, The
post-generation checklist is missing a repo-root validation pass for monorepos:
when the scaffold logic detects a workspace (monorepo) it should also validate
artifact files placed at the workspace root (_artifacts/domain_map.yaml and
_artifacts/skill_tree.yaml). Update the function that builds the checklist
(e.g., postGenerationChecklist or scaffoldCommand) to add a step that either
invokes the existing validation routine (the same code used by package-level
checks, e.g., intent validate) against the workspace root or directly calls the
domain/skill validation helpers to check _artifacts/domain_map.yaml and
_artifacts/skill_tree.yaml; ensure the new step only runs when
monorepo/workspace detection returns true.

Copy link

@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.

🧹 Nitpick comments (2)
packages/intent/tests/cli.test.ts (2)

48-52: Improve getHelpOutput() fidelity to avoid losing output text

On Line 50, only call[0] is captured, so multi-arg console calls are partially dropped. Joining without delimiters on Line 51 can also blur boundaries between lines.

Suggested diff
 function getHelpOutput(): string {
   return [...infoSpy.mock.calls, ...logSpy.mock.calls]
-    .map((call) => String(call[0] ?? ''))
-    .join('')
+    .map((call) => call.map((arg) => String(arg)).join(' '))
+    .join('\n')
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/tests/cli.test.ts` around lines 48 - 52, getHelpOutput()
currently only records call[0] so any additional console args are lost and
concatenating calls with '' removes boundaries; update getHelpOutput to
serialize all arguments from each call (use infoSpy.mock.calls and
logSpy.mock.calls), convert each arg to string, join args with a space for each
call, and then join the calls with a newline (or other delimiter) to preserve
argument order and line boundaries; reference the getHelpOutput function and the
infoSpy/logSpy mock calls when implementing this change.

176-183: Make scaffold output assertion less brittle

Line 178 assumes the prompt is always the first console.log call. If preface logs are added later, this test can fail even when the command is correct.

Suggested diff
   it('prints the scaffold prompt', async () => {
     const exitCode = await main(['scaffold'])
-    const output = String(logSpy.mock.calls[0]?.[0])
+    const output = logSpy.mock.calls
+      .map((call) => call.map((arg) => String(arg)).join(' '))
+      .join('\n')

     expect(exitCode).toBe(0)
     expect(output).toContain('## Step 1')
     expect(output).toContain('meta/domain-discovery/SKILL.md')
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/tests/cli.test.ts` around lines 176 - 183, The test currently
assumes the scaffold prompt is the first console.log by using
logSpy.mock.calls[0]?.[0]; update the assertion to search the recorded log calls
for the scaffold output instead (e.g., use logSpy.mock.calls.find(...) or
map/join over logSpy.mock.calls to locate an entry that contains '## Step 1' or
'meta/domain-discovery/SKILL.md') so the test no longer breaks if preface logs
are added; adjust the test around main(['scaffold']) and logSpy to assert
against the found log entry rather than index 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/intent/tests/cli.test.ts`:
- Around line 48-52: getHelpOutput() currently only records call[0] so any
additional console args are lost and concatenating calls with '' removes
boundaries; update getHelpOutput to serialize all arguments from each call (use
infoSpy.mock.calls and logSpy.mock.calls), convert each arg to string, join args
with a space for each call, and then join the calls with a newline (or other
delimiter) to preserve argument order and line boundaries; reference the
getHelpOutput function and the infoSpy/logSpy mock calls when implementing this
change.
- Around line 176-183: The test currently assumes the scaffold prompt is the
first console.log by using logSpy.mock.calls[0]?.[0]; update the assertion to
search the recorded log calls for the scaffold output instead (e.g., use
logSpy.mock.calls.find(...) or map/join over logSpy.mock.calls to locate an
entry that contains '## Step 1' or 'meta/domain-discovery/SKILL.md') so the test
no longer breaks if preface logs are added; adjust the test around
main(['scaffold']) and logSpy to assert against the found log entry rather than
index 0.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e54853cd-4c3e-4a61-8698-eea2a906be85

📥 Commits

Reviewing files that changed from the base of the PR and between 9361932 and 185fb56.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • packages/intent/package.json
  • packages/intent/tests/cli.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/intent/package.json

@LadyBluenotes LadyBluenotes merged commit d79bd45 into main Mar 17, 2026
5 checks passed
@LadyBluenotes LadyBluenotes deleted the cli-refactor branch March 17, 2026 16:00
WolfieLeader added a commit to WolfieLeader/tanstack-intent that referenced this pull request Mar 17, 2026
Resolve conflicts with PR TanStack#85 (cac CLI migration):
- Accept upstream's cac-based CLI structure
- Keep our enhanced workspace detection (pnpm, deno, nested patterns)
- Port pnpm-aware monorepo detection to commands/validate.ts
- Integrate upstream's isGenericWorkspaceName/deriveWorkspacePackageName
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