feat(cli): agent-mode support via once() and batch()#1287
feat(cli): agent-mode support via once() and batch()#1287
Conversation
|
did you see #1264 and do you have an opinion? I imagine this is a test PR to evaluate how it works, before opening a PR/merging into clack/prompts? |
Alias @clack/prompts to @posva/clack-prompts so interactive commands
work under AI-agent harnesses (Claude Code, Cursor, Aider) that replay
the script to answer each prompt via a JSON session file.
- Wrap non-idempotent side effects with once(id, fn) so they run a
single time across replays: template download + slug rename, nightly
version setup, install + git init, module install + nuxt.config
update, and the target-dir existence check (otherwise the second
replay would see the folder created by the first run).
- Collapse independent prompt pairs into batch({...}) so agents answer
them in one round-trip: package-manager + git-init in init,
nightly-channel + upgrade-method in upgrade.
- Give every prompt a stable id across batch and non-batch branches;
thread an optional id through selectModulesAutocomplete so its two
call sites disambiguate.
- Drop the !hasTTY early-exit in selectModulesAutocomplete when
running under isAgent() - agents handle the question via the
session file without a TTY.
38466d5 to
ae143a3
Compare
commit: |
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 52 minutes and 0 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThe pull request migrates from Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/nuxi/src/commands/init.ts (1)
196-242:⚠️ Potential issue | 🟡 MinorStale
templateDownloadPathreference ininit:download-templatewhen user chose "different" directory.At line 233 the
'different'branch reassignstemplateDownloadPathbut notdir. Inside theonce('init:download-template', …)block (lines 260-278) the slug-rename logic readsdir.length > 0and laterbasename(templateDownloadPath)— the basename is correct (it uses the updated path), but the guardif (dir.length > 0)can now be wrong: if the user originally rannuxi initwith no positional and then picked a "different" directory at the prompt,diris still''and the slug rename is skipped even though a real target was chosen. This is pre-existing behavior that the once-wrapping preserves, but since you're already touching this block it would be a good opportunity to fix.🐛 Suggested fix
case 'different': { const result = await text({ id: 'init:different-dir', message: 'Please specify a different directory:', }) if (isCancel(result)) { cancel('Operation cancelled.') process.exit(1) } + dir = result templateDownloadPath = resolve(cwd, result) break }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/nuxi/src/commands/init.ts` around lines 196 - 242, The slug-rename guard uses the stale variable dir instead of the possibly-updated templateDownloadPath, so update the logic inside the once('init:download-template', ...) handler to derive the directory name from templateDownloadPath (e.g., use basename(templateDownloadPath) or resolve a localDir variable from templateDownloadPath) rather than checking dir.length; adjust any references to dir in the slug-rename block to use the new derived value so the rename runs correctly when the user selects "different" and templateDownloadPath was reassigned in the 'different' branch.
🧹 Nitpick comments (5)
packages/nuxi/src/commands/init.ts (2)
293-330: Nightlyset-nightlyonce block:process.exit(1)inside cached function.Calling
process.exit(1)from inside theonce(…)callback means the process tears down beforeonce()ever returns — so nothing is cached for the error path (fine) but it also means the error messaging is the only signal to the agent. Consider throwing instead and letting the outer flow handle it consistently with thedownloadTemplateblock at lines 284-290, whichthrow errunderDEBUG. Keeps cancellation/cleanup semantics uniform across the init pipeline.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/nuxi/src/commands/init.ts` around lines 293 - 330, Replace the direct process.exit(1) calls inside the once('init:set-nightly', ...) callback with throwing an Error so the outer init flow can handle cleanup and caching consistently; specifically, in the init:set-nightly handler (references: once('init:set-nightly', ...), nightlySpinner, nightlyChannelTag, nightlyChannelVersion) change the two branches that currently call process.exit(1) to nightlySpinner.error(...) then throw new Error(...) (or rethrow the caught error) and ensure callers that wrap this once handler (consistent with the downloadTemplate error handling) will catch and handle the thrown error.
344-344: Normalize thegitInitstring/boolean handling to match theinstallpattern.Line 401 handles the same citty-returns-string-
'false'quirk with the more readablectx.args.install === false || (ctx.args.install as unknown) === 'false'. ThegitInitline squashes both branches into a single ternary with'false' as unknownwhich reads oddly and, ifctx.args.gitInitis ever the string'true', will propagate the string into aboolean | undefinedvariable. Aligning the two paths makes the intent clearer.♻️ Proposed alignment
- let gitInit: boolean | undefined = ctx.args.gitInit === 'false' as unknown ? false : ctx.args.gitInit + const gitInitArg = ctx.args.gitInit as boolean | string | undefined + let gitInit: boolean | undefined + = gitInitArg === false || gitInitArg === 'false' + ? false + : gitInitArg === true || gitInitArg === 'true' + ? true + : undefined🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/nuxi/src/commands/init.ts` at line 344, The gitInit assignment incorrectly mixes ternary and string propagation; change it to mirror the install handling: compute gitInit from ctx.args.gitInit using the same boolean/string normalization like "let gitInit: boolean | undefined = ctx.args.gitInit === false || (ctx.args.gitInit as unknown) === 'false' ? false : (ctx.args.gitInit as boolean | undefined)"; update the assignment to use ctx.args.gitInit, the gitInit variable, and the same comparison pattern used for ctx.args.install so the string 'false' is normalized to boolean false and other values remain properly typed.packages/nuxi/src/commands/module/add.ts (1)
221-252: Minor:updateConfig(...).catch(...)insideonce()returns the wrong value.
await updateConfig({...}).catch(err => { logger.error(...) })resolves withundefinedon failure, so the block always falls through toreturn null— good. But because errors are swallowed here, a failed nuxt.config write is cached as a "success" byonce(). On replay the agent won't see this again. That's fine for the "config file exists, partially updated" case, but consider logging enough detail (e.g., includeerror.stackbehindDEBUG) so an operator can diagnose from the CLI output alone since the failure won't recur.💡 Optional: surface more detail in debug mode
}).catch((error) => { logger.error(`Failed to update ${colors.cyan('nuxt.config')}: ${error.message}`) + if (process.env.DEBUG) { + logger.error(error.stack) + } logger.error(`Please manually add ${colors.cyan(modules.map(module => module.pkgName).join(', '))} to the ${colors.cyan('modules')} in ${colors.cyan('nuxt.config.ts')}`) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/nuxi/src/commands/module/add.ts` around lines 221 - 252, The catch on updateConfig inside once('module-add:update-config') currently only logs error.message and swallows the error; update the catch to also log detailed diagnostics (e.g., error.stack) using a debug-level logger call so operators can diagnose failures even though the error is not rethrown—specifically, inside the .catch((error) => { ... }) block for updateConfig({...}) add logger.debug/error with error.stack (or logger.debug({ err: error })) in addition to the existing logger.error calls while keeping the return behavior unchanged; reference the once('module-add:update-config') wrapper, the updateConfig call, and the logger.error/logger.debug calls to locate the change.packages/nuxi/src/commands/upgrade.ts (1)
213-268:once('upgrade:tasks', …)wraps the full install sequence — confirm replay intent.Wrapping all three
tasks(...)stages inside a singleonce()means on replay the agent will skip the entire install/dedupe/cleanup sequence as a unit. That's correct for idempotency (no duplicate installs), but if any sub-task failed silently on first run, the replay won't retry any of them — users have to remove the cache manually. Given the mixed scope (install + dedupe + cleanup), consider whether a finer-grained split (oneonceper logical step) would give better recovery semantics. Not a blocker.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/nuxi/src/commands/upgrade.ts` around lines 213 - 268, The current once('upgrade:tasks', ...) wraps the entire tasks([...]) sequence (including addDependency, dedupeDependencies, cleanupNuxtDirs) so a replay will skip all sub-steps if any previously succeeded; split the single once into finer-grained once calls for each logical step to allow partial replays: wrap the install step (the task with addDependency) in its own once key (e.g., 'upgrade:install'), wrap the force/dedupe steps (calls to dedupeDependencies and recreate lockfile) in their own once keys like 'upgrade:dedupe'/'upgrade:recreate-lockfile' depending on method, and wrap the cleanup step (cleanupNuxtDirs and buildDir resolution) in a separate once like 'upgrade:cleanup'; preserve spinner() usage and existing task titles but move the respective task entries into their corresponding once wrappers so each can be retried independently on replay.packages/nuxt-cli/package.json (1)
45-45: Downstream consumers will inherit the aliased fork.Since
@nuxt/cliis a published package, this alias flows into every user'snode_modulestree. The fork (@posva/clack-prompts) is actively maintained but distinct from the official@clack/promptslibrary. Worth making sure the fork is publicly documented (README note / release notes) so downstream projects aren't surprised to find@posva/clack-prompts@^1.2.2being pulled in under@clack/prompts. Also consider:
- Pinning to an exact version (
1.2.2) instead of^1.2.2to avoid silently picking up unreviewed upstream changes from the fork, until the fork reaches a stable/maintained state.- Tracking upstream
@clack/promptsfor whenonce/batchland there, so you can revert the alias.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/nuxt-cli/package.json` at line 45, The package.json currently aliases "@clack/prompts" to "npm:`@posva/clack-prompts`@^1.2.2" which will propagate the fork to downstream consumers; update this by pinning the alias to an exact version (e.g., change ^1.2.2 to 1.2.2) in the packages/nuxt-cli package.json, add a brief note in the repository README or release notes documenting that the forked package "@posva/clack-prompts" is being used under the "@clack/prompts" name, and add a short TODO in the repo (or an issue/task) to track the official "@clack/prompts" upstream for the desired features (once/batch) so the alias can be reverted when appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@packages/nuxi/src/commands/init.ts`:
- Around line 196-242: The slug-rename guard uses the stale variable dir instead
of the possibly-updated templateDownloadPath, so update the logic inside the
once('init:download-template', ...) handler to derive the directory name from
templateDownloadPath (e.g., use basename(templateDownloadPath) or resolve a
localDir variable from templateDownloadPath) rather than checking dir.length;
adjust any references to dir in the slug-rename block to use the new derived
value so the rename runs correctly when the user selects "different" and
templateDownloadPath was reassigned in the 'different' branch.
---
Nitpick comments:
In `@packages/nuxi/src/commands/init.ts`:
- Around line 293-330: Replace the direct process.exit(1) calls inside the
once('init:set-nightly', ...) callback with throwing an Error so the outer init
flow can handle cleanup and caching consistently; specifically, in the
init:set-nightly handler (references: once('init:set-nightly', ...),
nightlySpinner, nightlyChannelTag, nightlyChannelVersion) change the two
branches that currently call process.exit(1) to nightlySpinner.error(...) then
throw new Error(...) (or rethrow the caught error) and ensure callers that wrap
this once handler (consistent with the downloadTemplate error handling) will
catch and handle the thrown error.
- Line 344: The gitInit assignment incorrectly mixes ternary and string
propagation; change it to mirror the install handling: compute gitInit from
ctx.args.gitInit using the same boolean/string normalization like "let gitInit:
boolean | undefined = ctx.args.gitInit === false || (ctx.args.gitInit as
unknown) === 'false' ? false : (ctx.args.gitInit as boolean | undefined)";
update the assignment to use ctx.args.gitInit, the gitInit variable, and the
same comparison pattern used for ctx.args.install so the string 'false' is
normalized to boolean false and other values remain properly typed.
In `@packages/nuxi/src/commands/module/add.ts`:
- Around line 221-252: The catch on updateConfig inside
once('module-add:update-config') currently only logs error.message and swallows
the error; update the catch to also log detailed diagnostics (e.g., error.stack)
using a debug-level logger call so operators can diagnose failures even though
the error is not rethrown—specifically, inside the .catch((error) => { ... })
block for updateConfig({...}) add logger.debug/error with error.stack (or
logger.debug({ err: error })) in addition to the existing logger.error calls
while keeping the return behavior unchanged; reference the
once('module-add:update-config') wrapper, the updateConfig call, and the
logger.error/logger.debug calls to locate the change.
In `@packages/nuxi/src/commands/upgrade.ts`:
- Around line 213-268: The current once('upgrade:tasks', ...) wraps the entire
tasks([...]) sequence (including addDependency, dedupeDependencies,
cleanupNuxtDirs) so a replay will skip all sub-steps if any previously
succeeded; split the single once into finer-grained once calls for each logical
step to allow partial replays: wrap the install step (the task with
addDependency) in its own once key (e.g., 'upgrade:install'), wrap the
force/dedupe steps (calls to dedupeDependencies and recreate lockfile) in their
own once keys like 'upgrade:dedupe'/'upgrade:recreate-lockfile' depending on
method, and wrap the cleanup step (cleanupNuxtDirs and buildDir resolution) in a
separate once like 'upgrade:cleanup'; preserve spinner() usage and existing task
titles but move the respective task entries into their corresponding once
wrappers so each can be retried independently on replay.
In `@packages/nuxt-cli/package.json`:
- Line 45: The package.json currently aliases "@clack/prompts" to
"npm:`@posva/clack-prompts`@^1.2.2" which will propagate the fork to downstream
consumers; update this by pinning the alias to an exact version (e.g., change
^1.2.2 to 1.2.2) in the packages/nuxt-cli package.json, add a brief note in the
repository README or release notes documenting that the forked package
"@posva/clack-prompts" is being used under the "@clack/prompts" name, and add a
short TODO in the repo (or an issue/task) to track the official "@clack/prompts"
upstream for the desired features (once/batch) so the alias can be reverted when
appropriate.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 65b453de-72ac-496a-bb6c-b7d2731e813b
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
knip.jsonpackages/nuxi/package.jsonpackages/nuxi/src/commands/init.tspackages/nuxi/src/commands/module/_autocomplete.tspackages/nuxi/src/commands/module/add.tspackages/nuxi/src/commands/upgrade.tspackages/nuxt-cli/package.json
| await once('init:add-modules', async () => { | ||
| await runCommand(addModuleCommand, args) | ||
| return null | ||
| }) |
There was a problem hiding this comment.
can be simplified to () => runCommand()
|
Yes, I saw #1264 . This is different: it's an experiment about bringing the interactivity of the CLI to agents so they can make choices based on instructions. It's not meant to be merged Oh, and I think #1264 is great and that one should def be shipped. Interactivety in CLIs should, at the very least fallback to showing the help when run by an agent, that would be so much better. Some times this is not possible: e.g. the dev server prompting the user to install a package (this is done in nuxt dev for example when using |
Summary
Alias
@clack/promptsto@posva/clack-promptsso that interactive commands keep working under AI-agent harnesses (Claude Code, Cursor, Aider) that re-run the script to answer each prompt through a JSON session file.once(id, fn)so they run exactly once across replays:git init,nuxt module addinstall +nuxt.configupdate.existsSynccheck — otherwise the second replay would see the folder the first run just created and diverge into the "directory already exists" branch.batch({...})so agents answer them in one round-trip:init— package-manager select + git-init confirm.upgrade— nightly-channel select + dedupe/force/skip method select (only when both would fire).ids on every prompt so clack can resolve answers deterministically across replays. Both batched and single-prompt fallbacks share the same ids so the same question maps to the same session entry regardless of which branch runs.selectModulesAutocompletegains an optionalidso the two call sites (init,module add) disambiguate, and no longer bails on!hasTTYwhen running underisAgent()— agents don't need a TTY.Human UX is unchanged:
onceruns transparently in interactive mode, andbatchdisplays its items sequentially just likegroup.Test plan
pnpm test:types— cleanpnpm lint— cleanpnpm build— all three buildable packages compilenuxi init,nuxi upgrade,nuxi module addeach walk through prompts identically to todayoncesteps execute once and batched prompts emit together