overlord-cli 3.20.0 → 3.22.0

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/README.md CHANGED
@@ -12,6 +12,9 @@ Install it globally so the `ovld` and `overlord` commands are available on your
12
12
  npm install -g overlord-cli
13
13
  ```
14
14
 
15
+ Use Node.js 20 or newer for every CLI install or update.
16
+ Run `ovld update` any time you want to refresh the global npm install to the latest release.
17
+
15
18
  ## Usage
16
19
 
17
20
  ```bash
@@ -29,6 +32,7 @@ ovld auth login
29
32
  ovld attach
30
33
  ovld create "Investigate the failing build"
31
34
  ovld prompt "Draft a fix for the onboarding flow"
35
+ ovld update
32
36
  ovld setup codex
33
37
  ovld setup cursor
34
38
  ovld setup gemini
@@ -38,7 +42,7 @@ ovld doctor
38
42
 
39
43
  ## Requirements
40
44
 
41
- - Node.js 24 or newer
45
+ - Node.js 20 or newer
42
46
  - Access to an Overlord instance when using authenticated commands
43
47
 
44
48
  ## Commands
@@ -52,6 +56,7 @@ ovld doctor
52
56
  - `protocol` - run ticket lifecycle commands
53
57
  - `connect`, `restart`, `run`, `resume`, `context` - launch or resume an agent session
54
58
  - `setup` - install the Overlord connector or plugin bundle for a supported agent
59
+ - `update` - install the latest CLI release from npm
55
60
  - `doctor` - verify installed agent connectors and check whether a newer CLI version is available
56
61
 
57
62
  ## License
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import { createRequire } from 'node:module';
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const cliPackage = require('../../package.json');
8
+
9
+ const CURRENT_CLI_VERSION = typeof cliPackage.version === 'string' ? cliPackage.version : '0.0.0';
10
+ const CLI_PACKAGE_NAME =
11
+ typeof cliPackage.name === 'string' && cliPackage.name ? cliPackage.name : 'overlord-cli';
12
+
13
+ const ORANGE = '\x1b[38;5;208m';
14
+ const RESET = '\x1b[0m';
15
+
16
+ function colorizeOrange(text) {
17
+ return `${ORANGE}${text}${RESET}`;
18
+ }
19
+
20
+ export function getCurrentCliVersion() {
21
+ return CURRENT_CLI_VERSION;
22
+ }
23
+
24
+ export function getCliPackageName() {
25
+ return CLI_PACKAGE_NAME;
26
+ }
27
+
28
+ export async function fetchLatestCliVersion({
29
+ fetchImpl = fetch,
30
+ packageName = CLI_PACKAGE_NAME,
31
+ timeoutMs = 2500
32
+ } = {}) {
33
+ const controller = new AbortController();
34
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
35
+
36
+ try {
37
+ const response = await fetchImpl(`https://registry.npmjs.org/${packageName}/latest`, {
38
+ signal: controller.signal,
39
+ headers: { Accept: 'application/json' }
40
+ });
41
+
42
+ if (!response.ok) return null;
43
+
44
+ const payload = await response.json();
45
+ return typeof payload?.version === 'string' ? payload.version : null;
46
+ } catch {
47
+ return null;
48
+ } finally {
49
+ clearTimeout(timeout);
50
+ }
51
+ }
52
+
53
+ export async function checkForCliUpdate(options = {}) {
54
+ const currentVersion = options.currentVersion ?? CURRENT_CLI_VERSION;
55
+ const latestVersion = await fetchLatestCliVersion(options);
56
+ if (!latestVersion || latestVersion === currentVersion) return null;
57
+ return latestVersion;
58
+ }
59
+
60
+ export function formatCliUpdateNotice(latestVersion, { currentVersion = CURRENT_CLI_VERSION } = {}) {
61
+ return `New Overlord CLI version available: v${latestVersion} (installed v${currentVersion}). Run \`ovld update\` to update via npm.`;
62
+ }
63
+
64
+ export function printCliUpdateNotice(
65
+ latestVersion,
66
+ { currentVersion = CURRENT_CLI_VERSION, stream = process.stderr } = {}
67
+ ) {
68
+ if (!latestVersion) return false;
69
+ stream.write(`${colorizeOrange(formatCliUpdateNotice(latestVersion, { currentVersion }))}\n`);
70
+ return true;
71
+ }
72
+
73
+ export async function runCliUpdateCommand({
74
+ currentVersion = CURRENT_CLI_VERSION,
75
+ fetchLatestVersionFn = fetchLatestCliVersion,
76
+ logger = console,
77
+ npmCommand = 'npm',
78
+ packageName = CLI_PACKAGE_NAME,
79
+ spawnSyncImpl = spawnSync
80
+ } = {}) {
81
+ const latestVersion = await fetchLatestVersionFn({ currentVersion, packageName });
82
+
83
+ if (latestVersion && latestVersion === currentVersion) {
84
+ logger.log(`Overlord CLI ${currentVersion} is already the latest version.`);
85
+ return { alreadyLatest: true, currentVersion, latestVersion };
86
+ }
87
+
88
+ const target = `${packageName}@latest`;
89
+ if (latestVersion) {
90
+ logger.log(`Updating Overlord CLI ${currentVersion} -> ${latestVersion} via npm...`);
91
+ } else {
92
+ logger.log(`Updating Overlord CLI via npm...`);
93
+ }
94
+
95
+ const result = spawnSyncImpl(npmCommand, ['install', '-g', target], {
96
+ env: process.env,
97
+ stdio: 'inherit'
98
+ });
99
+
100
+ if (result.error) {
101
+ throw result.error;
102
+ }
103
+
104
+ if (typeof result.status === 'number' && result.status !== 0) {
105
+ throw new Error(`\`${npmCommand} install -g ${target}\` exited with status ${result.status}.`);
106
+ }
107
+
108
+ if (typeof result.signal === 'string') {
109
+ throw new Error(`\`${npmCommand} install -g ${target}\` was terminated by ${result.signal}.`);
110
+ }
111
+
112
+ if (latestVersion) {
113
+ logger.log(`Overlord CLI updated to v${latestVersion}.`);
114
+ } else {
115
+ logger.log('Overlord CLI update complete. Run `ovld version` to confirm the installed version.');
116
+ }
117
+
118
+ return { alreadyLatest: false, currentVersion, latestVersion, result };
119
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { runAttachCommand } from './attach.mjs';
4
4
  import { runAuthCommand } from './auth.mjs';
5
+ import { checkForCliUpdate, printCliUpdateNotice, runCliUpdateCommand } from './cli-update.mjs';
5
6
  import { runLauncherCommand } from './launcher.mjs';
6
7
  import { runProtocolCommand } from './protocol.mjs';
7
8
  import { runDoctorCommand, runSetupCommand } from './setup.mjs';
@@ -9,6 +10,18 @@ import { runTicketCommand } from './ticket.mjs';
9
10
  import { runTicketsCommand } from './tickets.mjs';
10
11
  import { runVersionCommand } from './version.mjs';
11
12
 
13
+ const MIN_NODE_MAJOR = 20;
14
+
15
+ function assertSupportedNodeVersion() {
16
+ const major = Number.parseInt(process.versions.node.split('.')[0] ?? '', 10);
17
+ if (Number.isNaN(major) || major < MIN_NODE_MAJOR) {
18
+ throw new Error(
19
+ `Overlord CLI requires Node.js ${MIN_NODE_MAJOR} or newer. Found ${process.version}.\n` +
20
+ `Update Node before running \`ovld\`. If you installed the desktop wrapper, you can also point it at a newer runtime with \`OVLD_NODE_BIN=/path/to/node\`.`
21
+ );
22
+ }
23
+ }
24
+
12
25
  function printHelp(primaryCommand) {
13
26
  console.log(`Overlord CLI
14
27
 
@@ -26,6 +39,7 @@ Usage:
26
39
  ${primaryCommand} restart <agent> Resume an agent session
27
40
  ${primaryCommand} context Print ticket context (requires TICKET_ID)
28
41
  ${primaryCommand} setup <agent|all> Install Overlord agent connector
42
+ ${primaryCommand} update Install the latest CLI version from npm
29
43
  ${primaryCommand} doctor Validate installed agent connectors and check for CLI updates
30
44
  ${primaryCommand} version Show the installed CLI version
31
45
  ${primaryCommand} help Show this help message
@@ -52,7 +66,17 @@ Run a subcommand with --help for more detail.
52
66
  }
53
67
 
54
68
  export async function runCli({ primaryCommand }) {
69
+ assertSupportedNodeVersion();
55
70
  const [command, ...rest] = process.argv.slice(2);
71
+ const shouldCheckForUpdate =
72
+ Boolean(process.stdout.isTTY || process.stderr.isTTY) &&
73
+ command !== 'doctor' &&
74
+ command !== 'update';
75
+ const latestCliVersion = shouldCheckForUpdate ? await checkForCliUpdate() : null;
76
+
77
+ if (latestCliVersion && command !== 'doctor' && command !== 'update') {
78
+ printCliUpdateNotice(latestCliVersion);
79
+ }
56
80
 
57
81
  if (!command || command === 'help' || command === '--help' || command === '-h') {
58
82
  printHelp(primaryCommand);
@@ -106,8 +130,18 @@ export async function runCli({ primaryCommand }) {
106
130
  return;
107
131
  }
108
132
 
133
+ if (command === 'update') {
134
+ if (rest[0] === '--help' || rest[0] === '-h' || rest[0] === 'help') {
135
+ console.log(`Usage:
136
+ ${primaryCommand} update Install the latest CLI version from npm`);
137
+ return;
138
+ }
139
+ await runCliUpdateCommand();
140
+ return;
141
+ }
142
+
109
143
  if (command === 'doctor') {
110
- await runDoctorCommand();
144
+ await runDoctorCommand({ latestCliVersion });
111
145
  return;
112
146
  }
113
147
 
@@ -12,6 +12,7 @@ import fs from 'node:fs';
12
12
  import os from 'node:os';
13
13
  import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
+ import { checkForCliUpdate, getCurrentCliVersion, printCliUpdateNotice } from './cli-update.mjs';
15
16
 
16
17
  const BUNDLE_VERSION = '1.8.0';
17
18
  const MD_MARKER_START = '<!-- overlord:managed:start -->';
@@ -32,6 +33,7 @@ const CODEX_TARGET_RULES = path.join(os.homedir(), '.codex', 'rules', 'default.r
32
33
  const CODEX_LEGACY_AGENTS = path.join(os.homedir(), '.codex', 'AGENTS.md');
33
34
  const CODEX_RULES_START = '# overlord:permissions:start';
34
35
  const CODEX_RULES_END = '# overlord:permissions:end';
36
+ const REQUIRED_NODE_MAJOR = 20;
35
37
 
36
38
  const supportedAgents = ['claude', 'codex', 'cursor', 'gemini', 'opencode'];
37
39
 
@@ -279,11 +281,6 @@ function readJsonFileOrNull(filePath) {
279
281
  }
280
282
  }
281
283
 
282
- function localCliVersion() {
283
- const cliPackage = readJsonFileOrNull(path.resolve(__dirname, '..', '..', 'package.json'));
284
- return typeof cliPackage?.version === 'string' ? cliPackage.version : null;
285
- }
286
-
287
284
  function writeJsonFile(filePath, data) {
288
285
  const dir = path.dirname(filePath);
289
286
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -482,28 +479,6 @@ function currentContentHashForAgent(agent) {
482
479
  );
483
480
  }
484
481
 
485
- async function checkForCliUpdate() {
486
- const currentVersion = localCliVersion();
487
- if (!currentVersion) return null;
488
- const controller = new AbortController();
489
- const timeout = setTimeout(() => controller.abort(), 2500);
490
- try {
491
- const response = await fetch('https://registry.npmjs.org/overlord-cli/latest', {
492
- signal: controller.signal,
493
- headers: { Accept: 'application/json' }
494
- });
495
- if (!response.ok) return null;
496
- const payload = await response.json();
497
- const latestVersion = typeof payload?.version === 'string' ? payload.version : null;
498
- if (!latestVersion) return null;
499
- return latestVersion === currentVersion ? null : latestVersion;
500
- } catch {
501
- return null;
502
- } finally {
503
- clearTimeout(timeout);
504
- }
505
- }
506
-
507
482
  function codexSourcePluginDir() {
508
483
  if (fs.existsSync(PACKAGE_PLUGIN_DIR)) return PACKAGE_PLUGIN_DIR;
509
484
  if (fs.existsSync(REPO_PLUGIN_DIR)) return REPO_PLUGIN_DIR;
@@ -971,6 +946,10 @@ function doctorAgent(agent) {
971
946
  return true;
972
947
  }
973
948
 
949
+ function currentNodeMajor() {
950
+ return Number.parseInt(process.versions.node.split('.')[0] ?? '', 10);
951
+ }
952
+
974
953
  // ---------------------------------------------------------------------------
975
954
  // Public API
976
955
  // ---------------------------------------------------------------------------
@@ -1030,22 +1009,31 @@ export async function runSetupCommand(args) {
1030
1009
  }
1031
1010
  }
1032
1011
 
1033
- export async function runDoctorCommand() {
1012
+ export async function runDoctorCommand({ latestCliVersion = null } = {}) {
1034
1013
  console.log('Overlord agent bundle status:\n');
1035
1014
  let allOk = true;
1015
+ const nodeMajor = currentNodeMajor();
1016
+ if (Number.isNaN(nodeMajor) || nodeMajor < REQUIRED_NODE_MAJOR) {
1017
+ console.log(
1018
+ ` ✗ node: unsupported runtime (${process.version}; requires Node.js ${REQUIRED_NODE_MAJOR}+)`
1019
+ );
1020
+ allOk = false;
1021
+ } else {
1022
+ console.log(` ✓ node: ${process.version}`);
1023
+ }
1024
+ console.log();
1036
1025
  for (const agent of supportedAgents) {
1037
1026
  if (!doctorAgent(agent)) allOk = false;
1038
1027
  }
1039
- const latestCliVersion = await checkForCliUpdate();
1028
+ const updateVersion = latestCliVersion ?? (await checkForCliUpdate());
1040
1029
  console.log();
1041
1030
  if (allOk) {
1042
1031
  console.log('All bundles are up to date.');
1043
1032
  } else {
1044
1033
  console.log('Run `ovld setup <agent>` or `ovld setup all` to install/repair.');
1045
1034
  }
1046
- if (latestCliVersion) {
1035
+ if (updateVersion) {
1047
1036
  console.log();
1048
- console.log(`CLI update available: ${latestCliVersion}`);
1049
- console.log('Run `npm install -g overlord-cli@latest` to update.');
1037
+ printCliUpdateNotice(updateVersion, { currentVersion: getCurrentCliVersion(), stream: process.stdout });
1050
1038
  }
1051
1039
  }
@@ -1,10 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { createRequire } from 'node:module';
4
-
5
- const require = createRequire(import.meta.url);
6
- const { version } = require('../../package.json');
3
+ import { getCurrentCliVersion } from './cli-update.mjs';
7
4
 
8
5
  export function runVersionCommand() {
9
- console.log(`Overlord CLI ${version}`);
6
+ console.log(`Overlord CLI ${getCurrentCliVersion()}`);
10
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "3.20.0",
3
+ "version": "3.22.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "plugins/"
16
16
  ],
17
17
  "engines": {
18
- "node": ">=24"
18
+ "node": ">=20"
19
19
  },
20
20
  "keywords": [
21
21
  "overlord",