@phnx-labs/agents-cli 1.20.22 → 1.20.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,12 +7,15 @@
7
7
  */
8
8
  import { Command } from 'commander';
9
9
  import chalk from 'chalk';
10
- import ora from 'ora';
11
10
  import * as fs from 'fs';
12
11
  import * as os from 'os';
13
12
  import * as path from 'path';
14
13
  import { fileURLToPath } from 'url';
15
- import { confirm, select } from '@inquirer/prompts';
14
+ // `ora`, `@inquirer/prompts`, `./commands/utils.js`, and the agents/versions/shims
15
+ // modules are imported dynamically at their use sites: they are needed only on
16
+ // interactive / update / shim-repair paths, never for fast commands like
17
+ // `--version`, `--help`, or `view`. Keeping them off the module-eval path is
18
+ // what gets cold starts under the target.
16
19
  // Force exit on Ctrl+C when no interactive prompt is handling it.
17
20
  process.on('SIGINT', () => process.exit(130));
18
21
  // Ignore SIGPIPE — prevents exit code 13 crashes in piped environments
@@ -54,59 +57,12 @@ if (IS_DEV_BUILD) {
54
57
  if (process.env.AGENTS_CLI_DISABLE_AUTO_UPDATE === undefined)
55
58
  process.env.AGENTS_CLI_DISABLE_AUTO_UPDATE = '1';
56
59
  }
57
- // Import command registrations
58
- import { registerPullCommand } from './commands/pull.js';
59
- import { registerPushCommand } from './commands/push.js';
60
- import { registerRepoCommands } from './commands/repo.js';
61
- import { registerSetupCommand, runSetup } from './commands/setup.js';
62
- import { registerFeedbackCommand } from './commands/feedback.js';
63
- import { registerViewCommand } from './commands/view.js';
64
- import { registerInspectCommand } from './commands/inspect.js';
65
- import { registerCommandsCommands } from './commands/commands.js';
66
- import { registerHooksCommands } from './commands/hooks.js';
67
- import { registerSkillsCommands } from './commands/skills.js';
68
- import { registerRulesCommands } from './commands/rules.js';
69
- import { registerPermissionsCommands } from './commands/permissions.js';
70
- import { registerMcpCommands } from './commands/mcp.js';
71
- import { registerCliCommands } from './commands/cli.js';
72
- import { registerVersionsCommands } from './commands/versions.js';
73
- import { registerImportCommand } from './commands/import.js';
74
- import { registerPackagesCommands } from './commands/packages.js';
75
- import { registerDaemonCommands } from './commands/daemon.js';
76
- import { registerRoutinesCommands } from './commands/routines.js';
77
- import { registerRunCommand } from './commands/exec.js';
78
- import { registerModelsCommand } from './commands/models.js';
79
- import { registerDefaultsCommands } from './commands/defaults.js';
80
- import { registerPruneCommand } from './commands/prune.js';
81
- import { registerTrashCommands, registerRestoreCommand } from './commands/trash.js';
82
- import { registerDoctorCommand } from './commands/doctor.js';
83
- import { registerSubagentsCommands } from './commands/subagents.js';
84
- import { registerPluginsCommands } from './commands/plugins.js';
85
- import { registerWorkflowsCommands } from './commands/workflows.js';
86
- import { registerWorktreeCommands } from './commands/worktree.js';
87
- import { registerSyncCommand } from './commands/sync.js';
88
- import { registerRefreshRulesCommand } from './commands/refresh-rules.js';
89
- import { registerDriveCommands } from './commands/drive.js';
90
- import { registerPtyCommands } from './commands/pty.js';
91
- import { registerTmuxCommands } from './commands/tmux.js';
92
- import { registerBrowserCommand } from './commands/browser.js';
93
- import { registerComputerCommand } from './commands/computer.js';
94
- import { registerProfilesCommands } from './commands/profiles.js';
95
- import { registerSecretsCommands } from './commands/secrets.js';
96
- import { registerWalletCommands } from './commands/wallet.js';
97
- import { registerHelperCommand } from './commands/helper.js';
98
- import { registerMenubarCommands } from './commands/menubar.js';
99
- import { registerFactoryCommands } from './commands/factory.js';
100
- import { registerUsageCommand } from './commands/usage.js';
101
- import { registerCostCommand } from './commands/cost.js';
102
- import { registerBudgetCommand } from './commands/budget.js';
103
- import { registerAliasCommand } from './commands/alias.js';
104
- import { registerBetaCommands } from './commands/beta.js';
60
+ // Command registration is lazy: instead of statically importing every command
61
+ // module on each invocation (which loaded the whole ~50-module tree before the
62
+ // first byte of output), the registry maps a command name to a thunk that
63
+ // imports only what that command needs. See src/lib/startup/command-registry.ts.
64
+ import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadBrowser, loadComputer, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
105
65
  import { applyGlobalHelpConventions } from './lib/help.js';
106
- import { isInteractiveTerminal, isPromptCancelled } from './commands/utils.js';
107
- import { AGENTS } from './lib/agents.js';
108
- import { getGlobalDefault, listInstalledVersions } from './lib/versions.js';
109
- import { addShimsToPath, ensureShimCurrent, ensureVersionedAliasCurrent, getPathShadowingExecutable, getPathSetupInstructions, getShimsDir, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
110
66
  import { IS_WINDOWS } from './lib/platform/index.js';
111
67
  // Transparent shim delegate: the generated Windows `.cmd` shims invoke
112
68
  // `agents __shim <agent>[@version] <raw args>`. Intercept here, before commander
@@ -382,6 +338,9 @@ async function installResolvedPackage(metadata) {
382
338
  }
383
339
  /** Present an interactive upgrade prompt (TTY) or a one-line hint (non-TTY). */
384
340
  async function promptUpgrade(latestVersion) {
341
+ const { default: ora } = await import('ora');
342
+ const { confirm, select } = await import('@inquirer/prompts');
343
+ const { isInteractiveTerminal, isPromptCancelled } = await import('./commands/utils.js');
385
344
  if (!isInteractiveTerminal()) {
386
345
  console.error(chalk.yellow(`Update available: ${VERSION} -> ${latestVersion}. Run: agents upgrade --yes`));
387
346
  return;
@@ -480,6 +439,7 @@ async function checkForUpdates() {
480
439
  await promptUpgrade(cache.latestVersion);
481
440
  }
482
441
  catch (err) {
442
+ const { isPromptCancelled } = await import('./commands/utils.js');
483
443
  if (isPromptCancelled(err))
484
444
  return;
485
445
  /* prompt error, ignore */
@@ -500,6 +460,13 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
500
460
  if (requestedCommand === 'sync' || requestedCommand === 'refresh-rules') {
501
461
  return;
502
462
  }
463
+ // Past the documentation/non-TTY guards: only now load the shim + agent
464
+ // tables this interactive repair flow needs, so fast commands never pay for
465
+ // them at module-eval time.
466
+ const { confirm } = await import('@inquirer/prompts');
467
+ const { AGENTS } = await import('./lib/agents.js');
468
+ const { getGlobalDefault, listInstalledVersions } = await import('./lib/versions.js');
469
+ const { addShimsToPath, ensureShimCurrent, ensureVersionedAliasCurrent, getPathShadowingExecutable, getPathSetupInstructions, getShimsDir, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } = await import('./lib/shims.js');
503
470
  const installedAgents = listAgentsWithInstalledVersions();
504
471
  if (installedAgents.length === 0) {
505
472
  return;
@@ -621,88 +588,50 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
621
588
  }
622
589
  catch { /* best-effort */ }
623
590
  }
624
- // Register all commands
625
- registerViewCommand(program);
626
- registerInspectCommand(program);
627
- registerFeedbackCommand(program);
628
- registerCommandsCommands(program);
629
- registerHooksCommands(program);
630
- registerSkillsCommands(program);
631
- registerRulesCommands(program);
632
- // Deprecated 'memory' command - hard error, force users to use 'rules'
633
- program
634
- .command('memory', { hidden: true })
635
- .allowUnknownOption()
636
- .allowExcessArguments()
637
- .action(() => {
638
- console.error(chalk.red('"agents memory" has been renamed to "agents rules".'));
639
- console.error(chalk.gray('Run "agents rules --help" for usage.\n'));
640
- process.exit(1);
641
- });
642
- registerPermissionsCommands(program);
643
- // Deprecated 'perms' alias for 'permissions'
644
- program
645
- .command('perms', { hidden: true })
646
- .allowUnknownOption()
647
- .allowExcessArguments()
648
- .action(async (opts, cmd) => {
649
- console.log(chalk.yellow('Deprecated: Use "agents permissions" instead of "agents perms"\n'));
650
- // Re-parse with 'permissions' command
651
- const args = process.argv.slice(2);
652
- args[0] = 'permissions';
653
- await program.parseAsync(['node', 'agents', ...args]);
654
- });
655
- registerMcpCommands(program);
656
- registerCliCommands(program);
657
- registerSubagentsCommands(program);
658
- registerPluginsCommands(program);
659
- registerWorkflowsCommands(program);
660
- registerWorktreeCommands(program);
661
- registerVersionsCommands(program);
662
- registerImportCommand(program);
663
- registerPackagesCommands(program);
664
- registerDaemonCommands(program);
665
- registerRoutinesCommands(program);
666
- registerRunCommand(program);
667
- registerDefaultsCommands(program);
668
- registerModelsCommand(program);
669
- registerPruneCommand(program);
670
- registerTrashCommands(program);
671
- registerRestoreCommand(program);
672
- registerDoctorCommand(program);
673
- // Deprecated 'exec' alias for 'run'
674
- program
675
- .command('exec', { hidden: true })
676
- .allowUnknownOption()
677
- .allowExcessArguments()
678
- .action(async () => {
679
- console.log(chalk.yellow('Deprecated: Use "agents run" instead of "agents exec"\n'));
680
- const args = process.argv.slice(2);
681
- args[0] = 'run';
682
- await program.parseAsync(['node', 'agents', ...args]);
683
- });
684
- registerProfilesCommands(program);
685
- registerSecretsCommands(program);
686
- registerWalletCommands(program);
687
- registerHelperCommand(program);
688
- registerMenubarCommands(program);
689
- registerBetaCommands(program);
690
- registerSyncCommand(program);
691
- registerRefreshRulesCommand(program);
692
- registerDriveCommands(program);
693
- registerFactoryCommands(program);
694
- registerUsageCommand(program);
695
- registerCostCommand(program);
696
- registerBudgetCommand(program);
697
- registerAliasCommand(program);
698
- registerPtyCommands(program);
699
- registerTmuxCommands(program);
700
- registerBrowserCommand(program);
701
- registerComputerCommand(program);
702
- // Deprecated 'jobs' and 'cron' aliases for 'routines'
703
- for (const alias of ['jobs', 'cron']) {
704
- program
705
- .command(alias, { hidden: true })
591
+ // --- Inline command registrars ----------------------------------------------
592
+ // These commands are defined here rather than in a command module because they
593
+ // close over entry-point-local state (program re-parsing, VERSION, the npm
594
+ // upgrade helpers). The lazy registrar and the all-commands fallback below both
595
+ // call them, so the behavior is identical to the old eager registration.
596
+ /** Deprecated `memory` command — hard error pointing users at `rules`. */
597
+ function registerMemoryCommand(p) {
598
+ p.command('memory', { hidden: true })
599
+ .allowUnknownOption()
600
+ .allowExcessArguments()
601
+ .action(() => {
602
+ console.error(chalk.red('"agents memory" has been renamed to "agents rules".'));
603
+ console.error(chalk.gray('Run "agents rules --help" for usage.\n'));
604
+ process.exit(1);
605
+ });
606
+ }
607
+ /** Deprecated `perms` alias — re-parses as `permissions`. */
608
+ function registerPermsAliasCommand(p) {
609
+ p.command('perms', { hidden: true })
610
+ .allowUnknownOption()
611
+ .allowExcessArguments()
612
+ .action(async () => {
613
+ console.log(chalk.yellow('Deprecated: Use "agents permissions" instead of "agents perms"\n'));
614
+ // Re-parse with 'permissions' command
615
+ const args = process.argv.slice(2);
616
+ args[0] = 'permissions';
617
+ await program.parseAsync(['node', 'agents', ...args]);
618
+ });
619
+ }
620
+ /** Deprecated `exec` alias — re-parses as `run`. */
621
+ function registerExecAliasCommand(p) {
622
+ p.command('exec', { hidden: true })
623
+ .allowUnknownOption()
624
+ .allowExcessArguments()
625
+ .action(async () => {
626
+ console.log(chalk.yellow('Deprecated: Use "agents run" instead of "agents exec"\n'));
627
+ const args = process.argv.slice(2);
628
+ args[0] = 'run';
629
+ await program.parseAsync(['node', 'agents', ...args]);
630
+ });
631
+ }
632
+ /** Deprecated `jobs` / `cron` aliases — re-parse as `routines`. */
633
+ function registerJobsCronAliasCommand(p, alias) {
634
+ p.command(alias, { hidden: true })
706
635
  .allowUnknownOption()
707
636
  .allowExcessArguments()
708
637
  .action(async () => {
@@ -712,60 +641,166 @@ for (const alias of ['jobs', 'cron']) {
712
641
  await program.parseAsync(['node', 'agents', ...args]);
713
642
  });
714
643
  }
715
- program
716
- .command('upgrade')
717
- .description('Upgrade agents-cli to the latest version (or a specific [version])')
718
- .argument('[version]', 'Target version or dist-tag to install (default: latest)')
719
- .option('-y, --yes', 'Install without an interactive confirmation prompt')
720
- .action(async (version, options) => {
721
- const target = version ?? 'latest';
722
- let spinner = ora(version ? `Resolving ${NPM_PACKAGE_NAME}@${target}...` : 'Checking for updates...').start();
723
- try {
724
- const metadata = await fetchNpmPackageMetadata(target);
725
- const resolvedVersion = metadata.version;
726
- if (resolvedVersion === VERSION) {
727
- spinner.succeed(`Already on ${VERSION}`);
728
- return;
729
- }
730
- // For `latest` (no explicit version) skip when already ahead. When a
731
- // version is named explicitly, honor it even if it's a downgrade.
732
- if (!version && compareVersions(resolvedVersion, VERSION) <= 0) {
733
- spinner.succeed(`Already ahead of latest (${VERSION} >= ${resolvedVersion})`);
734
- return;
735
- }
736
- const direction = compareVersions(resolvedVersion, VERSION) < 0 ? 'Downgrade' : 'Upgrade';
737
- spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${resolvedVersion}`);
738
- printResolvedPackage(metadata);
739
- if (isInteractiveTerminal() && !options.yes) {
740
- const approved = await confirm({
741
- message: `Install ${NPM_PACKAGE_NAME}@${resolvedVersion}?`,
742
- default: false,
743
- });
744
- if (!approved) {
745
- console.log(chalk.gray('Upgrade cancelled'));
644
+ /** Self-upgrade command (`agents upgrade [version]`). */
645
+ function registerUpgradeCommand(p) {
646
+ p.command('upgrade')
647
+ .description('Upgrade agents-cli to the latest version (or a specific [version])')
648
+ .argument('[version]', 'Target version or dist-tag to install (default: latest)')
649
+ .option('-y, --yes', 'Install without an interactive confirmation prompt')
650
+ .action(async (version, options) => {
651
+ const { default: ora } = await import('ora');
652
+ const { confirm } = await import('@inquirer/prompts');
653
+ const { isInteractiveTerminal, isPromptCancelled } = await import('./commands/utils.js');
654
+ const target = version ?? 'latest';
655
+ let spinner = ora(version ? `Resolving ${NPM_PACKAGE_NAME}@${target}...` : 'Checking for updates...').start();
656
+ try {
657
+ const metadata = await fetchNpmPackageMetadata(target);
658
+ const resolvedVersion = metadata.version;
659
+ if (resolvedVersion === VERSION) {
660
+ spinner.succeed(`Already on ${VERSION}`);
746
661
  return;
747
662
  }
663
+ // For `latest` (no explicit version) skip when already ahead. When a
664
+ // version is named explicitly, honor it even if it's a downgrade.
665
+ if (!version && compareVersions(resolvedVersion, VERSION) <= 0) {
666
+ spinner.succeed(`Already ahead of latest (${VERSION} >= ${resolvedVersion})`);
667
+ return;
668
+ }
669
+ const direction = compareVersions(resolvedVersion, VERSION) < 0 ? 'Downgrade' : 'Upgrade';
670
+ spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${resolvedVersion}`);
671
+ printResolvedPackage(metadata);
672
+ if (isInteractiveTerminal() && !options.yes) {
673
+ const approved = await confirm({
674
+ message: `Install ${NPM_PACKAGE_NAME}@${resolvedVersion}?`,
675
+ default: false,
676
+ });
677
+ if (!approved) {
678
+ console.log(chalk.gray('Upgrade cancelled'));
679
+ return;
680
+ }
681
+ }
682
+ spinner = ora(`${direction === 'Downgrade' ? 'Downgrading' : 'Upgrading'} ${VERSION} -> ${resolvedVersion}...`).start();
683
+ await installResolvedPackage(metadata);
684
+ spinner.succeed(`${direction}d to ${resolvedVersion}`);
685
+ // Only show the changelog for a genuine upgrade range.
686
+ if (compareVersions(resolvedVersion, VERSION) > 0) {
687
+ await showWhatsNew(VERSION, resolvedVersion);
688
+ }
748
689
  }
749
- spinner = ora(`${direction === 'Downgrade' ? 'Downgrading' : 'Upgrading'} ${VERSION} -> ${resolvedVersion}...`).start();
750
- await installResolvedPackage(metadata);
751
- spinner.succeed(`${direction}d to ${resolvedVersion}`);
752
- // Only show the changelog for a genuine upgrade range.
753
- if (compareVersions(resolvedVersion, VERSION) > 0) {
754
- await showWhatsNew(VERSION, resolvedVersion);
690
+ catch (err) {
691
+ if (isPromptCancelled(err))
692
+ return;
693
+ spinner.fail(`Upgrade failed: ${err instanceof Error ? err.message : String(err)}`);
694
+ console.log(chalk.gray(`Run manually: agents upgrade ${version ? version + ' ' : ''}--yes`));
755
695
  }
756
- }
757
- catch (err) {
758
- if (isPromptCancelled(err))
759
- return;
760
- spinner.fail(`Upgrade failed: ${err instanceof Error ? err.message : String(err)}`);
761
- console.log(chalk.gray(`Run manually: agents upgrade ${version ? version + ' ' : ''}--yes`));
762
- }
763
- });
764
- registerPullCommand(program);
765
- registerPushCommand(program);
766
- registerRepoCommands(program);
767
- registerSetupCommand(program);
768
- applyGlobalHelpConventions(program);
696
+ });
697
+ }
698
+ // --- Lazy registration orchestration -----------------------------------------
699
+ /** Import a command module via its loader and register it on the program. */
700
+ async function reg(loader) {
701
+ (await loader())(program);
702
+ }
703
+ /**
704
+ * Register exactly the command(s) the requested top-level name needs.
705
+ * Returns false when the name maps to no known command (typo / unknown) so the
706
+ * caller can fall back to registering everything for spellcheck.
707
+ *
708
+ * Lazy commands (sessions/teams/cloud) are intentionally NOT handled here — they
709
+ * must register after applyGlobalHelpConventions to match main's ordering.
710
+ */
711
+ async function registerEagerForRequest(name) {
712
+ switch (name) {
713
+ case 'memory':
714
+ registerMemoryCommand(program);
715
+ return true;
716
+ case 'perms':
717
+ // The action re-parses as `permissions`, so that target must exist too.
718
+ registerPermsAliasCommand(program);
719
+ await reg(loadPermissions);
720
+ return true;
721
+ case 'exec':
722
+ registerExecAliasCommand(program);
723
+ await reg(loadRun);
724
+ return true;
725
+ case 'jobs':
726
+ case 'cron':
727
+ registerJobsCronAliasCommand(program, name);
728
+ await reg(loadRoutines);
729
+ return true;
730
+ case 'upgrade':
731
+ registerUpgradeCommand(program);
732
+ return true;
733
+ }
734
+ const loaders = COMMAND_LOADERS[name];
735
+ if (!loaders)
736
+ return false;
737
+ for (const loader of loaders)
738
+ await reg(loader);
739
+ return true;
740
+ }
741
+ /**
742
+ * Register every command in the EXACT order main does (old src/index.ts lines
743
+ * 691-844), including the inline deprecated aliases. Used only on the slow paths
744
+ * (unknown command spellcheck, "did you mean" auto-correct) where the full set
745
+ * of names — and their registration order, which breaks ties in the suggestion
746
+ * picker — must match main byte-for-byte.
747
+ */
748
+ async function registerAllEagerCommands() {
749
+ await reg(loadView);
750
+ await reg(loadInspect);
751
+ await reg(loadFeedback);
752
+ await reg(loadCommands);
753
+ await reg(loadHooks);
754
+ await reg(loadSkills);
755
+ await reg(loadRules);
756
+ registerMemoryCommand(program);
757
+ await reg(loadPermissions);
758
+ registerPermsAliasCommand(program);
759
+ await reg(loadMcp);
760
+ await reg(loadCli);
761
+ await reg(loadSubagents);
762
+ await reg(loadPlugins);
763
+ await reg(loadWorkflows);
764
+ await reg(loadWorktree);
765
+ await reg(loadVersions);
766
+ await reg(loadImport);
767
+ await reg(loadPackages);
768
+ await reg(loadDaemon);
769
+ await reg(loadRoutines);
770
+ await reg(loadRun);
771
+ await reg(loadDefaults);
772
+ await reg(loadModels);
773
+ await reg(loadPrune);
774
+ await reg(loadTrash);
775
+ await reg(loadRestore);
776
+ await reg(loadDoctor);
777
+ registerExecAliasCommand(program);
778
+ await reg(loadProfiles);
779
+ await reg(loadSecrets);
780
+ await reg(loadWallet);
781
+ await reg(loadHelper);
782
+ await reg(loadMenubar);
783
+ await reg(loadBeta);
784
+ await reg(loadSync);
785
+ await reg(loadRefreshRules);
786
+ await reg(loadDrive);
787
+ await reg(loadFactory);
788
+ await reg(loadUsage);
789
+ await reg(loadCost);
790
+ await reg(loadBudget);
791
+ await reg(loadAlias);
792
+ await reg(loadPty);
793
+ await reg(loadTmux);
794
+ await reg(loadBrowser);
795
+ await reg(loadComputer);
796
+ registerJobsCronAliasCommand(program, 'jobs');
797
+ registerJobsCronAliasCommand(program, 'cron');
798
+ registerUpgradeCommand(program);
799
+ await reg(loadPull);
800
+ await reg(loadPush);
801
+ await reg(loadRepo);
802
+ await reg(loadSetup);
803
+ }
769
804
  /** Calculate the Levenshtein edit distance between two strings. */
770
805
  function levenshtein(a, b) {
771
806
  const m = a.length;
@@ -812,45 +847,54 @@ program.on('command:*', (operands) => {
812
847
  }
813
848
  process.exit(1);
814
849
  });
815
- // Run update check on EVERY invocation before parsing
816
- await checkForUpdates();
817
- // Surface any "behind upstream" notices from the previous detached sync, then
818
- // fire-and-forget the next background sync. System repo gets a real fast-forward
819
- // pull (read-only locally, safe). User repo and extras get fetch-only + a
820
- // status marker that we'll print on the *next* invocation.
821
- const { spawnDetachedSync } = await import('./lib/auto-pull.js');
822
- spawnDetachedSync();
823
- // First-run experience: no args + no config yet + TTY -> launch interactive setup.
824
- // Skipped when stdin/stdout isn't a terminal (CI, pipes) or when user passes any args.
850
+ // Parse the invocation shape up front: the first non-flag token is the command,
851
+ // and the doc flags (--version/--help/-h) drive both the registration strategy
852
+ // and whether the update check + background sync run at all.
825
853
  const passedArgs = process.argv.slice(2);
826
854
  const requestedCommand = passedArgs.find((arg) => !arg.startsWith('-'));
827
- /**
828
- * Lazily register command trees that pull in the SQLite-backed session/cloud
829
- * stack. This keeps lightweight commands like `agents view` from loading the
830
- * DB layer during CLI startup.
831
- */
832
- async function registerLazyCommands() {
833
- switch (requestedCommand) {
834
- case 'sessions': {
835
- const { registerSessionsCommands } = await import('./commands/sessions.js');
836
- registerSessionsCommands(program);
837
- break;
838
- }
839
- case 'teams': {
840
- const { registerTeamsCommands } = await import('./commands/teams.js');
841
- registerTeamsCommands(program);
842
- break;
843
- }
844
- case 'cloud': {
845
- const { registerCloudCommands } = await import('./commands/cloud.js');
846
- registerCloudCommands(program);
847
- break;
848
- }
849
- default:
850
- break;
855
+ // Help and version output are pure documentation — they must never gate on
856
+ // setup, otherwise `agents <cmd> --help` becomes useless on a fresh box.
857
+ const helpOrVersionRequested = passedArgs.some((arg) => arg === '--help' || arg === '-h' || arg === '--version' || arg === '-V');
858
+ // Register only the command(s) this invocation actually uses. Lazy commands
859
+ // (sessions/teams/cloud) are handled after applyGlobalHelpConventions below.
860
+ const isLazyRequest = requestedCommand !== undefined && LAZY_COMMAND_NAMES.has(requestedCommand);
861
+ if (requestedCommand !== undefined && !isLazyRequest) {
862
+ const known = await registerEagerForRequest(requestedCommand);
863
+ if (!known) {
864
+ // Unknown top-level command: register the full tree so the "did you mean"
865
+ // spellcheck and edit-distance-1 auto-correct (the command:* handler above)
866
+ // see the same candidate set — and ordering — as main.
867
+ await registerAllEagerCommands();
851
868
  }
852
869
  }
853
- await registerLazyCommands();
870
+ // When requestedCommand is undefined (bare invocation, --version, --help, -h) no
871
+ // command modules are needed: --version is built in and the root help text is a
872
+ // static string.
873
+ // Mirror main: help conventions are applied after the eager command tree and
874
+ // before the lazy commands, so the latter inherit the root's custom help
875
+ // formatter instead of getting the per-command recursive pass.
876
+ applyGlobalHelpConventions(program);
877
+ // Lazy commands pull in the SQLite-backed session/cloud stack; register them
878
+ // only when explicitly requested, keeping lightweight commands off that path.
879
+ if (isLazyRequest) {
880
+ for (const loader of COMMAND_LOADERS[requestedCommand])
881
+ await reg(loader);
882
+ }
883
+ // Pure documentation paths (--version / --help / -h) return immediately: skip
884
+ // the update check (PATH scan + cache read) and the detached background sync
885
+ // (spawns a child process) that every other invocation runs.
886
+ if (!helpOrVersionRequested) {
887
+ // Run update check before parsing so the upgrade notice/prompt precedes output.
888
+ await checkForUpdates();
889
+ // Surface any "behind upstream" notices from the previous detached sync, then
890
+ // fire-and-forget the next background sync. System repo gets a real fast-forward
891
+ // pull (read-only locally, safe). User repo and extras get fetch-only + a
892
+ // status marker that we'll print on the *next* invocation.
893
+ const { spawnDetachedSync } = await import('./lib/auto-pull.js');
894
+ spawnDetachedSync();
895
+ }
896
+ // First-run experience: no args + no config yet + TTY -> launch interactive setup.
897
+ // Skipped when stdin/stdout isn't a terminal (CI, pipes) or when user passes any args.
854
898
  const metaFilePath = path.join(getUserAgentsDir(), 'agents.yaml');
855
899
  const firstRun = passedArgs.length === 0 &&
856
900
  !fs.existsSync(metaFilePath) &&
@@ -858,6 +902,7 @@ const firstRun = passedArgs.length === 0 &&
858
902
  process.stdout.isTTY;
859
903
  if (firstRun) {
860
904
  try {
905
+ const { runSetup } = await import('./commands/setup.js');
861
906
  await runSetup(program);
862
907
  }
863
908
  catch (err) {
@@ -870,9 +915,6 @@ if (firstRun) {
870
915
  // Every command requires the system repo to be cloned first. `setup` is the
871
916
  // only exemption — it's the command that does the cloning.
872
917
  const SETUP_EXEMPT_COMMANDS = new Set(['setup', 'help']);
873
- // Help and version output are pure documentation — they must never gate on
874
- // setup, otherwise `agents <cmd> --help` becomes useless on a fresh box.
875
- const helpOrVersionRequested = passedArgs.some((arg) => arg === '--help' || arg === '-h' || arg === '--version' || arg === '-V');
876
918
  // Fold legacy ~/.agents-system/ into ~/.agents/.system/ BEFORE ensureInitialized
877
919
  // runs. ensureInitialized checks for .git inside the new path; if the user is
878
920
  // upgrading from a layout where .git lives under the legacy path, the check
@@ -9,6 +9,7 @@ import type { CloudProvider, CloudTask, CloudTaskStatus, CloudEvent, DispatchOpt
9
9
  export declare class CodexCloudProvider implements CloudProvider {
10
10
  id: "codex";
11
11
  name: string;
12
+ targetKind: "env";
12
13
  private defaultEnv?;
13
14
  constructor(config?: {
14
15
  env?: string;
@@ -8,7 +8,7 @@
8
8
  import { spawn, execFileSync } from 'child_process';
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
- import { resolveDispatchRepos } from './types.js';
11
+ import { resolveDispatchRepos, MissingTargetError } from './types.js';
12
12
  import { getShimsDir } from '../state.js';
13
13
  const SHIMS_DIR = getShimsDir();
14
14
  /** Map a Codex Cloud status string to the canonical CloudTaskStatus enum. */
@@ -85,6 +85,10 @@ function parseTaskFromText(text) {
85
85
  export class CodexCloudProvider {
86
86
  id = 'codex';
87
87
  name = 'Codex Cloud';
88
+ targetKind = 'env';
89
+ // No listTargets: OpenAI ships no non-interactive "list environments"
90
+ // command. `codex cloud exec` requires --env and the help points at the
91
+ // interactive `codex cloud` TUI to browse. So discovery is guidance-only.
88
92
  defaultEnv;
89
93
  constructor(config) {
90
94
  this.defaultEnv = config?.env;
@@ -107,7 +111,9 @@ export class CodexCloudProvider {
107
111
  async dispatch(options) {
108
112
  const env = options.providerOptions?.env ?? this.defaultEnv;
109
113
  if (!env) {
110
- throw new Error('Codex Cloud requires --env <id>. Set a default in ~/.agents/agents.yaml under cloud.providers.codex.env.');
114
+ throw new MissingTargetError('env', 'Codex Cloud requires --env <id>.', 'Codex environments are created in the Codex web UI and bundle a repo + setup. ' +
115
+ 'Browse yours with `codex cloud` (interactive), then re-run with --env <id> ' +
116
+ 'or set a default in ~/.agents/agents.yaml under cloud.providers.codex.env.');
111
117
  }
112
118
  // Codex envs bundle their own repo list — the repos a task can touch are
113
119
  // fixed at env-creation time, not per-dispatch. Passing 2+ repos here is