@phnx-labs/agents-cli 1.20.21 → 1.20.23

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/commands/cloud.js +142 -13
  3. package/dist/commands/exec.js +13 -1
  4. package/dist/commands/menubar.d.ts +10 -0
  5. package/dist/commands/menubar.js +83 -0
  6. package/dist/commands/routines.js +34 -1
  7. package/dist/commands/secrets.d.ts +1 -1
  8. package/dist/commands/secrets.js +95 -38
  9. package/dist/index.js +292 -225
  10. package/dist/lib/agents.js +8 -0
  11. package/dist/lib/cloud/antigravity.d.ts +70 -0
  12. package/dist/lib/cloud/antigravity.js +196 -0
  13. package/dist/lib/cloud/codex.d.ts +1 -0
  14. package/dist/lib/cloud/codex.js +8 -2
  15. package/dist/lib/cloud/factory.d.ts +79 -18
  16. package/dist/lib/cloud/factory.js +324 -26
  17. package/dist/lib/cloud/registry.d.ts +18 -2
  18. package/dist/lib/cloud/registry.js +28 -4
  19. package/dist/lib/cloud/types.d.ts +73 -2
  20. package/dist/lib/cloud/types.js +17 -0
  21. package/dist/lib/exec.d.ts +2 -0
  22. package/dist/lib/exec.js +5 -0
  23. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +20 -0
  24. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  25. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +115 -0
  26. package/dist/lib/menubar/install-menubar.d.ts +57 -0
  27. package/dist/lib/menubar/install-menubar.js +291 -0
  28. package/dist/lib/secrets/agent.d.ts +9 -1
  29. package/dist/lib/secrets/agent.js +91 -10
  30. package/dist/lib/secrets/bundles.d.ts +19 -12
  31. package/dist/lib/secrets/bundles.js +22 -14
  32. package/dist/lib/self-update.d.ts +34 -0
  33. package/dist/lib/self-update.js +63 -2
  34. package/dist/lib/startup/command-registry.d.ts +99 -0
  35. package/dist/lib/startup/command-registry.js +136 -0
  36. package/dist/lib/types.d.ts +8 -0
  37. package/dist/lib/version.d.ts +11 -0
  38. package/dist/lib/version.js +20 -0
  39. package/package.json +5 -3
  40. package/scripts/postinstall.js +35 -0
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
@@ -23,7 +26,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
26
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
24
27
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
25
28
  const VERSION = packageJson.version;
26
- import { NPM_PACKAGE_NAME, deriveGlobalPrefix, installPackageIntoPrefix, verifyInstalledVersion, refreshAliasShims, } from './lib/self-update.js';
29
+ import { NPM_PACKAGE_NAME, deriveGlobalPrefix, detectPackageManager, installPackageIntoPrefix, installPackageWithBun, verifyInstalledVersion, refreshAliasShims, } from './lib/self-update.js';
27
30
  // Detect dev/working-tree builds and default the noisy startup steps off.
28
31
  // Three cases trip this:
29
32
  // 1. Dev install (scripts/install.sh) — package.json version stamped 0.0.0-dev.<sha>
@@ -54,58 +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 { registerFactoryCommands } from './commands/factory.js';
99
- import { registerUsageCommand } from './commands/usage.js';
100
- import { registerCostCommand } from './commands/cost.js';
101
- import { registerBudgetCommand } from './commands/budget.js';
102
- import { registerAliasCommand } from './commands/alias.js';
103
- 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';
104
65
  import { applyGlobalHelpConventions } from './lib/help.js';
105
- import { isInteractiveTerminal, isPromptCancelled } from './commands/utils.js';
106
- import { AGENTS } from './lib/agents.js';
107
- import { getGlobalDefault, listInstalledVersions } from './lib/versions.js';
108
- import { addShimsToPath, ensureShimCurrent, ensureVersionedAliasCurrent, getPathShadowingExecutable, getPathSetupInstructions, getShimsDir, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
109
66
  import { IS_WINDOWS } from './lib/platform/index.js';
110
67
  // Transparent shim delegate: the generated Windows `.cmd` shims invoke
111
68
  // `agents __shim <agent>[@version] <raw args>`. Intercept here, before commander
@@ -347,8 +304,17 @@ function printResolvedPackage(metadata) {
347
304
  }
348
305
  async function installResolvedPackage(metadata) {
349
306
  const packageRoot = path.resolve(__dirname, '..');
350
- const prefix = deriveGlobalPrefix(packageRoot);
351
- await installPackageIntoPrefix(`${NPM_PACKAGE_NAME}@${metadata.version}`, prefix);
307
+ const spec = `${NPM_PACKAGE_NAME}@${metadata.version}`;
308
+ // Upgrade with the package manager that owns this install. A bun global
309
+ // install lives at <bunGlobalDir>/node_modules/... (no `lib` segment), so an
310
+ // `npm install --prefix` would write to <bunGlobalDir>/lib/node_modules and
311
+ // never touch the running copy — npm exits 0, the verify below fails.
312
+ if (detectPackageManager(packageRoot) === 'bun') {
313
+ await installPackageWithBun(spec);
314
+ }
315
+ else {
316
+ await installPackageIntoPrefix(spec, deriveGlobalPrefix(packageRoot));
317
+ }
352
318
  verifyInstalledVersion(packageRoot, metadata.version);
353
319
  refreshAliasShims(packageRoot);
354
320
  // The npm install above runs with --ignore-scripts, so the postinstall that
@@ -372,6 +338,9 @@ async function installResolvedPackage(metadata) {
372
338
  }
373
339
  /** Present an interactive upgrade prompt (TTY) or a one-line hint (non-TTY). */
374
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');
375
344
  if (!isInteractiveTerminal()) {
376
345
  console.error(chalk.yellow(`Update available: ${VERSION} -> ${latestVersion}. Run: agents upgrade --yes`));
377
346
  return;
@@ -470,6 +439,7 @@ async function checkForUpdates() {
470
439
  await promptUpgrade(cache.latestVersion);
471
440
  }
472
441
  catch (err) {
442
+ const { isPromptCancelled } = await import('./commands/utils.js');
473
443
  if (isPromptCancelled(err))
474
444
  return;
475
445
  /* prompt error, ignore */
@@ -490,6 +460,13 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
490
460
  if (requestedCommand === 'sync' || requestedCommand === 'refresh-rules') {
491
461
  return;
492
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');
493
470
  const installedAgents = listAgentsWithInstalledVersions();
494
471
  if (installedAgents.length === 0) {
495
472
  return;
@@ -611,87 +588,50 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
611
588
  }
612
589
  catch { /* best-effort */ }
613
590
  }
614
- // Register all commands
615
- registerViewCommand(program);
616
- registerInspectCommand(program);
617
- registerFeedbackCommand(program);
618
- registerCommandsCommands(program);
619
- registerHooksCommands(program);
620
- registerSkillsCommands(program);
621
- registerRulesCommands(program);
622
- // Deprecated 'memory' command - hard error, force users to use 'rules'
623
- program
624
- .command('memory', { hidden: true })
625
- .allowUnknownOption()
626
- .allowExcessArguments()
627
- .action(() => {
628
- console.error(chalk.red('"agents memory" has been renamed to "agents rules".'));
629
- console.error(chalk.gray('Run "agents rules --help" for usage.\n'));
630
- process.exit(1);
631
- });
632
- registerPermissionsCommands(program);
633
- // Deprecated 'perms' alias for 'permissions'
634
- program
635
- .command('perms', { hidden: true })
636
- .allowUnknownOption()
637
- .allowExcessArguments()
638
- .action(async (opts, cmd) => {
639
- console.log(chalk.yellow('Deprecated: Use "agents permissions" instead of "agents perms"\n'));
640
- // Re-parse with 'permissions' command
641
- const args = process.argv.slice(2);
642
- args[0] = 'permissions';
643
- await program.parseAsync(['node', 'agents', ...args]);
644
- });
645
- registerMcpCommands(program);
646
- registerCliCommands(program);
647
- registerSubagentsCommands(program);
648
- registerPluginsCommands(program);
649
- registerWorkflowsCommands(program);
650
- registerWorktreeCommands(program);
651
- registerVersionsCommands(program);
652
- registerImportCommand(program);
653
- registerPackagesCommands(program);
654
- registerDaemonCommands(program);
655
- registerRoutinesCommands(program);
656
- registerRunCommand(program);
657
- registerDefaultsCommands(program);
658
- registerModelsCommand(program);
659
- registerPruneCommand(program);
660
- registerTrashCommands(program);
661
- registerRestoreCommand(program);
662
- registerDoctorCommand(program);
663
- // Deprecated 'exec' alias for 'run'
664
- program
665
- .command('exec', { hidden: true })
666
- .allowUnknownOption()
667
- .allowExcessArguments()
668
- .action(async () => {
669
- console.log(chalk.yellow('Deprecated: Use "agents run" instead of "agents exec"\n'));
670
- const args = process.argv.slice(2);
671
- args[0] = 'run';
672
- await program.parseAsync(['node', 'agents', ...args]);
673
- });
674
- registerProfilesCommands(program);
675
- registerSecretsCommands(program);
676
- registerWalletCommands(program);
677
- registerHelperCommand(program);
678
- registerBetaCommands(program);
679
- registerSyncCommand(program);
680
- registerRefreshRulesCommand(program);
681
- registerDriveCommands(program);
682
- registerFactoryCommands(program);
683
- registerUsageCommand(program);
684
- registerCostCommand(program);
685
- registerBudgetCommand(program);
686
- registerAliasCommand(program);
687
- registerPtyCommands(program);
688
- registerTmuxCommands(program);
689
- registerBrowserCommand(program);
690
- registerComputerCommand(program);
691
- // Deprecated 'jobs' and 'cron' aliases for 'routines'
692
- for (const alias of ['jobs', 'cron']) {
693
- program
694
- .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 })
695
635
  .allowUnknownOption()
696
636
  .allowExcessArguments()
697
637
  .action(async () => {
@@ -701,60 +641,166 @@ for (const alias of ['jobs', 'cron']) {
701
641
  await program.parseAsync(['node', 'agents', ...args]);
702
642
  });
703
643
  }
704
- program
705
- .command('upgrade')
706
- .description('Upgrade agents-cli to the latest version (or a specific [version])')
707
- .argument('[version]', 'Target version or dist-tag to install (default: latest)')
708
- .option('-y, --yes', 'Install without an interactive confirmation prompt')
709
- .action(async (version, options) => {
710
- const target = version ?? 'latest';
711
- let spinner = ora(version ? `Resolving ${NPM_PACKAGE_NAME}@${target}...` : 'Checking for updates...').start();
712
- try {
713
- const metadata = await fetchNpmPackageMetadata(target);
714
- const resolvedVersion = metadata.version;
715
- if (resolvedVersion === VERSION) {
716
- spinner.succeed(`Already on ${VERSION}`);
717
- return;
718
- }
719
- // For `latest` (no explicit version) skip when already ahead. When a
720
- // version is named explicitly, honor it even if it's a downgrade.
721
- if (!version && compareVersions(resolvedVersion, VERSION) <= 0) {
722
- spinner.succeed(`Already ahead of latest (${VERSION} >= ${resolvedVersion})`);
723
- return;
724
- }
725
- const direction = compareVersions(resolvedVersion, VERSION) < 0 ? 'Downgrade' : 'Upgrade';
726
- spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${resolvedVersion}`);
727
- printResolvedPackage(metadata);
728
- if (isInteractiveTerminal() && !options.yes) {
729
- const approved = await confirm({
730
- message: `Install ${NPM_PACKAGE_NAME}@${resolvedVersion}?`,
731
- default: false,
732
- });
733
- if (!approved) {
734
- 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}`);
735
661
  return;
736
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
+ }
737
689
  }
738
- spinner = ora(`${direction === 'Downgrade' ? 'Downgrading' : 'Upgrading'} ${VERSION} -> ${resolvedVersion}...`).start();
739
- await installResolvedPackage(metadata);
740
- spinner.succeed(`${direction}d to ${resolvedVersion}`);
741
- // Only show the changelog for a genuine upgrade range.
742
- if (compareVersions(resolvedVersion, VERSION) > 0) {
743
- 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`));
744
695
  }
745
- }
746
- catch (err) {
747
- if (isPromptCancelled(err))
748
- return;
749
- spinner.fail(`Upgrade failed: ${err instanceof Error ? err.message : String(err)}`);
750
- console.log(chalk.gray(`Run manually: agents upgrade ${version ? version + ' ' : ''}--yes`));
751
- }
752
- });
753
- registerPullCommand(program);
754
- registerPushCommand(program);
755
- registerRepoCommands(program);
756
- registerSetupCommand(program);
757
- 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
+ }
758
804
  /** Calculate the Levenshtein edit distance between two strings. */
759
805
  function levenshtein(a, b) {
760
806
  const m = a.length;
@@ -801,45 +847,54 @@ program.on('command:*', (operands) => {
801
847
  }
802
848
  process.exit(1);
803
849
  });
804
- // Run update check on EVERY invocation before parsing
805
- await checkForUpdates();
806
- // Surface any "behind upstream" notices from the previous detached sync, then
807
- // fire-and-forget the next background sync. System repo gets a real fast-forward
808
- // pull (read-only locally, safe). User repo and extras get fetch-only + a
809
- // status marker that we'll print on the *next* invocation.
810
- const { spawnDetachedSync } = await import('./lib/auto-pull.js');
811
- spawnDetachedSync();
812
- // First-run experience: no args + no config yet + TTY -> launch interactive setup.
813
- // 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.
814
853
  const passedArgs = process.argv.slice(2);
815
854
  const requestedCommand = passedArgs.find((arg) => !arg.startsWith('-'));
816
- /**
817
- * Lazily register command trees that pull in the SQLite-backed session/cloud
818
- * stack. This keeps lightweight commands like `agents view` from loading the
819
- * DB layer during CLI startup.
820
- */
821
- async function registerLazyCommands() {
822
- switch (requestedCommand) {
823
- case 'sessions': {
824
- const { registerSessionsCommands } = await import('./commands/sessions.js');
825
- registerSessionsCommands(program);
826
- break;
827
- }
828
- case 'teams': {
829
- const { registerTeamsCommands } = await import('./commands/teams.js');
830
- registerTeamsCommands(program);
831
- break;
832
- }
833
- case 'cloud': {
834
- const { registerCloudCommands } = await import('./commands/cloud.js');
835
- registerCloudCommands(program);
836
- break;
837
- }
838
- default:
839
- 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();
840
868
  }
841
869
  }
842
- 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.
843
898
  const metaFilePath = path.join(getUserAgentsDir(), 'agents.yaml');
844
899
  const firstRun = passedArgs.length === 0 &&
845
900
  !fs.existsSync(metaFilePath) &&
@@ -847,6 +902,7 @@ const firstRun = passedArgs.length === 0 &&
847
902
  process.stdout.isTTY;
848
903
  if (firstRun) {
849
904
  try {
905
+ const { runSetup } = await import('./commands/setup.js');
850
906
  await runSetup(program);
851
907
  }
852
908
  catch (err) {
@@ -859,9 +915,6 @@ if (firstRun) {
859
915
  // Every command requires the system repo to be cloned first. `setup` is the
860
916
  // only exemption — it's the command that does the cloning.
861
917
  const SETUP_EXEMPT_COMMANDS = new Set(['setup', 'help']);
862
- // Help and version output are pure documentation — they must never gate on
863
- // setup, otherwise `agents <cmd> --help` becomes useless on a fresh box.
864
- const helpOrVersionRequested = passedArgs.some((arg) => arg === '--help' || arg === '-h' || arg === '--version' || arg === '-V');
865
918
  // Fold legacy ~/.agents-system/ into ~/.agents/.system/ BEFORE ensureInitialized
866
919
  // runs. ensureInitialized checks for .git inside the new path; if the user is
867
920
  // upgrading from a layout where .git lives under the legacy path, the check
@@ -916,6 +969,20 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
916
969
  }
917
970
  catch { /* migration must never block CLI startup */ }
918
971
  }
972
+ // Auto-enable the macOS menu-bar helper once, for every user. Best-effort and
973
+ // idempotent: installMenubarLaunchAgentOnUpgrade() no-ops when not on darwin,
974
+ // when the user ran `agents menubar disable` (sticky opt-out), when the service
975
+ // is already installed, or when no helper bundle ships with this build. This is
976
+ // a lightweight startup self-heal (two existsSync checks then return) rather
977
+ // than a migration-sentinel bump, so it covers fresh installs AND upgrades
978
+ // without re-running the full migration for the whole user base (issue #20).
979
+ if (process.platform === 'darwin' && process.env.AGENTS_SKIP_MIGRATION !== '1') {
980
+ try {
981
+ const { installMenubarLaunchAgentOnUpgrade } = await import('./lib/menubar/install-menubar.js');
982
+ installMenubarLaunchAgentOnUpgrade();
983
+ }
984
+ catch { /* never block CLI startup on the menu bar */ }
985
+ }
919
986
  try {
920
987
  await maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequested);
921
988
  await program.parseAsync();