@openagents-org/agent-launcher 0.2.110 → 0.2.111

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.110",
3
+ "version": "0.2.111",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -459,6 +459,28 @@ async function cmdVersion() {
459
459
  print(`${pkg.name} v${pkg.version}`);
460
460
  }
461
461
 
462
+ async function cmdUpdate() {
463
+ const { checkForUpdate, runUpdate, currentVersion } = require('./update-check');
464
+ const info = await checkForUpdate();
465
+ if (!info) {
466
+ print('Could not reach the npm registry. Check your network.');
467
+ process.exitCode = 1;
468
+ return;
469
+ }
470
+ if (!info.isNewer) {
471
+ print(`Already on the latest version (${currentVersion()}).`);
472
+ return;
473
+ }
474
+ print(`Updating ${info.current} → ${info.latest}...`);
475
+ const ok = runUpdate();
476
+ if (!ok) {
477
+ print('Update failed.');
478
+ process.exitCode = 1;
479
+ return;
480
+ }
481
+ print(`Updated to ${info.latest}.`);
482
+ }
483
+
462
484
  async function cmdHelp() {
463
485
  print(`Usage: agn <command> [options]
464
486
 
@@ -485,6 +507,7 @@ Commands:
485
507
  workspace join <token> Join workspace with token
486
508
  workspace list List configured workspaces
487
509
  mcp-server Start MCP server (stdio) for workspace tools
510
+ update Upgrade launcher to the latest npm release
488
511
  version Show version
489
512
  help Show this help
490
513
 
@@ -503,6 +526,26 @@ async function main() {
503
526
  if (cmd === 'help' || flags.help) { await cmdHelp(); return; }
504
527
  if (cmd === 'version' || flags.version) { await cmdVersion(); return; }
505
528
 
529
+ // Check for a newer launcher version and offer to install it. Skip for:
530
+ // - mcp-server: JSON-RPC subprocess spawned by Claude Code
531
+ // - up --foreground: the backgrounded daemon child
532
+ // - tui / auto-TUI: interactive UI manages its own rendering
533
+ // - update: already updating; avoid recursion
534
+ const skipUpdateCheck =
535
+ cmd === 'mcp-server' ||
536
+ (cmd === 'up' && flags.foreground) ||
537
+ cmd === 'tui' ||
538
+ cmd === 'update' ||
539
+ flags['no-update-check'] ||
540
+ process.env.OPENAGENTS_SKIP_UPDATE_CHECK === '1' ||
541
+ (cmd === 'status' && process.argv.length <= 2 && process.stdin.isTTY);
542
+ if (!skipUpdateCheck) {
543
+ try {
544
+ const { notifyAndMaybeUpdate } = require('./update-check');
545
+ await notifyAndMaybeUpdate();
546
+ } catch {}
547
+ }
548
+
506
549
  const connector = getConnector(flags);
507
550
 
508
551
  // Launch TUI if command is 'tui' or no command with interactive terminal
@@ -542,6 +585,7 @@ async function main() {
542
585
  workspace: () => cmdWorkspace(connector, flags, positional),
543
586
  env: () => cmdEnv(connector, flags, positional),
544
587
  'test-llm': () => cmdTestLLM(connector, flags, positional),
588
+ update: () => cmdUpdate(),
545
589
  'mcp-server': () => {
546
590
  const { runMcpServer } = require('./mcp-server');
547
591
  const workspaceId = flags['workspace-id'] || process.env.OPENAGENTS_WORKSPACE_ID;
@@ -0,0 +1,203 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Update check + prompt flow.
5
+ *
6
+ * Queries the npm registry for the latest @openagents-org/agent-launcher
7
+ * version and, if newer than the running version, prints a notification
8
+ * and (for TTY invocations) prompts the user to update in place.
9
+ */
10
+
11
+ const https = require('https');
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+ const path = require('path');
15
+ const readline = require('readline');
16
+ const { execSync, spawnSync } = require('child_process');
17
+
18
+ const PKG_NAME = '@openagents-org/agent-launcher';
19
+ const CACHE_FILE = path.join(os.homedir(), '.openagents', '.update-check.json');
20
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
21
+
22
+ function currentVersion() {
23
+ return require('../package.json').version;
24
+ }
25
+
26
+ function compareSemver(a, b) {
27
+ const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
28
+ const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
29
+ for (let i = 0; i < 3; i++) {
30
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
31
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
32
+ }
33
+ return 0;
34
+ }
35
+
36
+ function loadCache() {
37
+ try {
38
+ const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
39
+ if (data && data.latest && data.checkedAt &&
40
+ Date.now() - data.checkedAt < CACHE_TTL_MS) {
41
+ return data.latest;
42
+ }
43
+ } catch {}
44
+ return null;
45
+ }
46
+
47
+ function saveCache(latest) {
48
+ try {
49
+ fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
50
+ fs.writeFileSync(CACHE_FILE, JSON.stringify({ latest, checkedAt: Date.now() }));
51
+ } catch {}
52
+ }
53
+
54
+ function fetchLatest(timeoutMs = 2500) {
55
+ return new Promise((resolve) => {
56
+ const req = https.request(
57
+ `https://registry.npmjs.org/${PKG_NAME}/latest`,
58
+ { method: 'GET', timeout: timeoutMs, headers: { Accept: 'application/json' } },
59
+ (res) => {
60
+ let body = '';
61
+ res.on('data', (c) => { body += c; });
62
+ res.on('end', () => {
63
+ try {
64
+ const parsed = JSON.parse(body);
65
+ resolve(parsed.version || null);
66
+ } catch { resolve(null); }
67
+ });
68
+ }
69
+ );
70
+ req.on('error', () => resolve(null));
71
+ req.on('timeout', () => { req.destroy(); resolve(null); });
72
+ req.end();
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Return { current, latest, isNewer } or null if the check failed / offline.
78
+ */
79
+ async function checkForUpdate() {
80
+ const current = currentVersion();
81
+ let latest = loadCache();
82
+ if (!latest) {
83
+ latest = await fetchLatest();
84
+ if (latest) saveCache(latest);
85
+ }
86
+ if (!latest) return null;
87
+ return { current, latest, isNewer: compareSemver(latest, current) > 0 };
88
+ }
89
+
90
+ /**
91
+ * Walk up from __dirname until we find the enclosing node_modules; the
92
+ * directory containing node_modules is the install prefix.
93
+ */
94
+ function detectInstallPrefix() {
95
+ let dir = __dirname;
96
+ for (let i = 0; i < 10; i++) {
97
+ const parent = path.dirname(dir);
98
+ if (parent === dir) break;
99
+ if (path.basename(parent) === 'node_modules') {
100
+ return path.dirname(parent);
101
+ }
102
+ dir = parent;
103
+ }
104
+ return null;
105
+ }
106
+
107
+ function findNpmBin() {
108
+ const nodeDir = path.dirname(process.execPath);
109
+ for (const name of ['npm', 'npm.cmd']) {
110
+ const candidate = path.join(nodeDir, name);
111
+ if (fs.existsSync(candidate)) return candidate;
112
+ }
113
+ try {
114
+ return execSync(process.platform === 'win32' ? 'where npm' : 'which npm', {
115
+ encoding: 'utf-8', timeout: 3000,
116
+ }).trim().split(/\r?\n/)[0];
117
+ } catch { return 'npm'; }
118
+ }
119
+
120
+ /**
121
+ * Run npm install to update to the latest version. Blocking.
122
+ * Returns true on success.
123
+ */
124
+ function runUpdate() {
125
+ const prefix = detectInstallPrefix();
126
+ const npmBin = findNpmBin();
127
+ const args = ['install', '--no-save', `${PKG_NAME}@latest`];
128
+ if (prefix) args.push('--prefix', prefix);
129
+ else args.push('-g');
130
+ process.stderr.write(`[launcher] Running: ${npmBin} ${args.join(' ')}\n`);
131
+ const r = spawnSync(npmBin, args, { stdio: 'inherit' });
132
+ return r.status === 0;
133
+ }
134
+
135
+ /**
136
+ * Prompt for a Y/n keystroke. Defaults to Y on empty input.
137
+ * Times out after `timeoutMs` and returns false.
138
+ */
139
+ function promptYes(question, timeoutMs = 30000) {
140
+ return new Promise((resolve) => {
141
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return resolve(false);
142
+ process.stderr.write(question);
143
+ let answered = false;
144
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
145
+ const timer = setTimeout(() => {
146
+ if (answered) return;
147
+ answered = true;
148
+ process.stderr.write('\n');
149
+ rl.close();
150
+ resolve(false);
151
+ }, timeoutMs);
152
+ rl.question('', (ans) => {
153
+ if (answered) return;
154
+ answered = true;
155
+ clearTimeout(timer);
156
+ rl.close();
157
+ const a = (ans || '').trim().toLowerCase();
158
+ resolve(a === '' || a === 'y' || a === 'yes');
159
+ });
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Check, notify, and offer to update. Exits the process on successful update.
165
+ * Safe to call at the top of any user-facing CLI command.
166
+ */
167
+ async function notifyAndMaybeUpdate() {
168
+ let info;
169
+ try { info = await checkForUpdate(); } catch { return; }
170
+ if (!info || !info.isNewer) return;
171
+
172
+ process.stderr.write(
173
+ `\n[launcher] Update available: ${info.current} → ${info.latest}\n`
174
+ );
175
+
176
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
177
+ if (!interactive) {
178
+ process.stderr.write(
179
+ '[launcher] Run `agn update` (or re-run install.sh) to upgrade.\n\n'
180
+ );
181
+ return;
182
+ }
183
+
184
+ const accepted = await promptYes('[launcher] Update now? [Y/n] ');
185
+ if (!accepted) {
186
+ process.stderr.write('[launcher] Skipped. Run `agn update` later to upgrade.\n\n');
187
+ return;
188
+ }
189
+
190
+ const ok = runUpdate();
191
+ if (ok) {
192
+ process.stderr.write(`[launcher] Updated to ${info.latest}. Re-run your command.\n`);
193
+ process.exit(0);
194
+ }
195
+ process.stderr.write('[launcher] Update failed — continuing with current version.\n\n');
196
+ }
197
+
198
+ module.exports = {
199
+ checkForUpdate,
200
+ notifyAndMaybeUpdate,
201
+ runUpdate,
202
+ currentVersion,
203
+ };