@the-open-engine/zeroshot 6.10.2 → 6.12.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.
Files changed (76) hide show
  1. package/cli/commands/providers.js +13 -13
  2. package/cli/index.js +77 -35
  3. package/cli/lib/first-run.js +12 -11
  4. package/cli/lib/update-checker.js +517 -132
  5. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
  6. package/lib/agent-cli-provider/adapters/opencode.js +3 -0
  7. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  8. package/lib/agent-cli-provider/types.d.ts +1 -0
  9. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  10. package/lib/agent-cli-provider/types.js.map +1 -1
  11. package/lib/cluster/client.cjs +146 -0
  12. package/lib/cluster/client.d.ts +44 -0
  13. package/lib/cluster/client.mjs +141 -0
  14. package/lib/cluster/connection.cjs +382 -0
  15. package/lib/cluster/connection.d.ts +41 -0
  16. package/lib/cluster/connection.mjs +378 -0
  17. package/lib/cluster/errors.cjs +44 -0
  18. package/lib/cluster/errors.d.ts +23 -0
  19. package/lib/cluster/errors.mjs +32 -0
  20. package/lib/cluster/frames.cjs +49 -0
  21. package/lib/cluster/frames.d.ts +12 -0
  22. package/lib/cluster/frames.mjs +45 -0
  23. package/lib/cluster/generated/protocol-schema.cjs +5 -0
  24. package/lib/cluster/generated/protocol-schema.d.ts +1 -0
  25. package/lib/cluster/generated/protocol-schema.mjs +2 -0
  26. package/lib/cluster/generated/protocol.cjs +22 -0
  27. package/lib/cluster/generated/protocol.d.ts +781 -0
  28. package/lib/cluster/generated/protocol.mjs +19 -0
  29. package/lib/cluster/index.cjs +40 -0
  30. package/lib/cluster/index.d.ts +10 -0
  31. package/lib/cluster/index.mjs +6 -0
  32. package/lib/cluster/queue.cjs +71 -0
  33. package/lib/cluster/queue.d.ts +15 -0
  34. package/lib/cluster/queue.mjs +67 -0
  35. package/lib/cluster/socket.cjs +15 -0
  36. package/lib/cluster/socket.d.ts +11 -0
  37. package/lib/cluster/socket.mjs +12 -0
  38. package/lib/cluster/subscriptions.cjs +270 -0
  39. package/lib/cluster/subscriptions.d.ts +69 -0
  40. package/lib/cluster/subscriptions.mjs +264 -0
  41. package/lib/cluster/validators.cjs +46 -0
  42. package/lib/cluster/validators.d.ts +3 -0
  43. package/lib/cluster/validators.mjs +42 -0
  44. package/lib/settings.js +195 -23
  45. package/lib/setup-apply.js +45 -32
  46. package/lib/setup-undo.js +25 -16
  47. package/package.json +25 -3
  48. package/scripts/build-cluster.js +43 -0
  49. package/scripts/generate-cluster-types.js +203 -0
  50. package/scripts/release-dry-run.js +6 -6
  51. package/scripts/rust-distribution.js +933 -0
  52. package/src/agent/agent-hook-executor.js +14 -6
  53. package/src/agent/agent-lifecycle.js +25 -8
  54. package/src/agent/agent-task-executor.js +711 -139
  55. package/src/agent/output-reformatter.js +54 -41
  56. package/src/agent/pr-verification.js +11 -3
  57. package/src/agent/task-execution-handle.js +373 -0
  58. package/src/agent-cli-provider/adapters/opencode.ts +3 -0
  59. package/src/agent-cli-provider/types.ts +1 -0
  60. package/src/agent-wrapper.js +5 -2
  61. package/src/cluster/client.ts +199 -0
  62. package/src/cluster/connection.ts +300 -0
  63. package/src/cluster/errors.ts +32 -0
  64. package/src/cluster/frames.ts +44 -0
  65. package/src/cluster/generated/protocol-schema.ts +2 -0
  66. package/src/cluster/generated/protocol.ts +183 -0
  67. package/src/cluster/index.ts +38 -0
  68. package/src/cluster/queue.ts +84 -0
  69. package/src/cluster/socket.ts +31 -0
  70. package/src/cluster/subscriptions.ts +256 -0
  71. package/src/cluster/validators.ts +56 -0
  72. package/src/cluster/ws.d.ts +7 -0
  73. package/src/isolation-manager.js +16 -0
  74. package/src/issue-providers/jira-provider.js +1 -1
  75. package/src/issue-providers/linear-provider.js +4 -4
  76. package/task-lib/runner.js +3 -0
@@ -1,19 +1,16 @@
1
1
  /**
2
- * Update Checker - Checks npm registry for newer versions
2
+ * Update Checker - cached startup notices and the explicit npm updater.
3
3
  *
4
- * Features:
5
- * - 24-hour check interval (avoids registry spam)
6
- * - 5-second timeout (non-blocking if offline)
7
- * - Interactive prompt for manual update
8
- * - Respects quiet mode (no prompts in CI/scripts)
4
+ * Automatic startup work is notification-only and stale-while-revalidate.
5
+ * Installation remains exclusive to the explicit `zeroshot update` command.
9
6
  */
10
7
 
11
8
  const https = require('https');
12
9
  const childProcess = require('child_process');
10
+ const crypto = require('crypto');
13
11
  const fs = require('fs');
14
12
  const path = require('path');
15
- const readline = require('readline');
16
- const { loadSettings, saveSettings } = require('../../lib/settings');
13
+ const { loadSettings, mutateSettings } = require('../../lib/settings');
17
14
 
18
15
  const NEW_PACKAGE_NAME = '@the-open-engine/zeroshot';
19
16
  const LEGACY_PACKAGE_NAME = '@covibes/zeroshot';
@@ -28,6 +25,13 @@ const FETCH_TIMEOUT_MS = 5000;
28
25
  // npm registry URL
29
26
  const REGISTRY_URL = `https://registry.npmjs.org/${NEW_PACKAGE_NAME}/latest`;
30
27
 
28
+ const MAX_RESPONSE_BYTES = 64 * 1024;
29
+ const STABLE_CORE_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
30
+ const BUILD_IDENTIFIER_PATTERN = /^[0-9A-Za-z-]+$/;
31
+ const MAX_VERSION_LENGTH = 256;
32
+
33
+ let inFlightRefresh = null;
34
+
31
35
  function getPackageMetadata() {
32
36
  return require('../../package.json');
33
37
  }
@@ -48,6 +52,10 @@ function isLegacyDistro(packageName = getCurrentPackageName()) {
48
52
  return packageName === LEGACY_PACKAGE_NAME;
49
53
  }
50
54
 
55
+ function shouldForceLegacyUpdate(argv, packageName = getCurrentPackageName()) {
56
+ return isLegacyDistro(packageName) && Array.isArray(argv) && argv[0] === 'update';
57
+ }
58
+
51
59
  function printLegacyDistroNotice(packageName = getCurrentPackageName()) {
52
60
  if (!isLegacyDistro(packageName)) {
53
61
  return false;
@@ -199,89 +207,142 @@ function buildInstallArgs(updateTarget) {
199
207
  return args;
200
208
  }
201
209
 
210
+ function parseStableVersion(version) {
211
+ if (typeof version !== 'string' || version.length > MAX_VERSION_LENGTH) return null;
212
+ const plusIndex = version.indexOf('+');
213
+ const core = plusIndex === -1 ? version : version.slice(0, plusIndex);
214
+ if (plusIndex !== -1) {
215
+ const build = version.slice(plusIndex + 1);
216
+ if (!build || build.split('.').some((part) => !BUILD_IDENTIFIER_PATTERN.test(part))) {
217
+ return null;
218
+ }
219
+ }
220
+ const match = STABLE_CORE_VERSION_PATTERN.exec(core);
221
+ if (!match) return null;
222
+ const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
223
+ return parts.every(Number.isSafeInteger) ? parts : null;
224
+ }
225
+
202
226
  /**
203
- * Compare semver versions
204
- * @param {string} current - Current version (e.g., "1.5.0")
205
- * @param {string} latest - Latest version (e.g., "1.6.0")
206
- * @returns {boolean} True if latest > current
227
+ * Compare a running version with a validated stable registry release.
228
+ * Non-release source versions are older for the explicit update command;
229
+ * automatic eligibility independently rejects them before checker work.
230
+ * @param {string} current
231
+ * @param {string} latest
232
+ * @returns {boolean}
207
233
  */
208
234
  function isNewerVersion(current, latest) {
209
- const currentParts = current.split('.').map(Number);
210
- const latestParts = latest.split('.').map(Number);
211
-
212
- for (let i = 0; i < 3; i++) {
213
- const c = currentParts[i] || 0;
214
- const l = latestParts[i] || 0;
215
- if (l > c) return true;
216
- if (l < c) return false;
235
+ const latestParts = parseStableVersion(latest);
236
+ if (!latestParts) return false;
237
+ const currentParts = parseStableVersion(current);
238
+ if (!currentParts) return true;
239
+
240
+ for (let index = 0; index < 3; index += 1) {
241
+ if (latestParts[index] > currentParts[index]) return true;
242
+ if (latestParts[index] < currentParts[index]) return false;
217
243
  }
218
244
  return false;
219
245
  }
220
246
 
247
+ function validatedManifestVersion(manifest) {
248
+ if (
249
+ !manifest ||
250
+ typeof manifest !== 'object' ||
251
+ manifest.name !== NEW_PACKAGE_NAME ||
252
+ !parseStableVersion(manifest.version) ||
253
+ !manifest.dist ||
254
+ typeof manifest.dist !== 'object' ||
255
+ typeof manifest.dist.tarball !== 'string' ||
256
+ !manifest.dist.tarball.trim().startsWith('https://') ||
257
+ typeof manifest.dist.integrity !== 'string' ||
258
+ manifest.dist.integrity.trim().length === 0
259
+ ) {
260
+ return null;
261
+ }
262
+ return manifest.version;
263
+ }
264
+
221
265
  /**
222
- * Fetch latest version from npm registry
223
- * @returns {Promise<string|null>} Latest version or null on failure
266
+ * Fetch and validate the installable npm latest manifest.
267
+ * @param {object} options
268
+ * @returns {Promise<string|null>}
224
269
  */
225
- function fetchLatestVersion() {
226
- return new Promise((resolve) => {
227
- const req = https.get(REGISTRY_URL, { timeout: FETCH_TIMEOUT_MS }, (res) => {
228
- if (res.statusCode !== 200) {
229
- resolve(null);
230
- return;
231
- }
270
+ function fetchLatestVersion(options = {}) {
271
+ const httpsModule = options.httpsModule || https;
272
+ const timeoutMs = options.timeoutMs ?? FETCH_TIMEOUT_MS;
273
+ const maxResponseBytes = options.maxResponseBytes ?? MAX_RESPONSE_BYTES;
274
+ const scheduleTimeout = options.setTimeout || setTimeout;
275
+ const cancelTimeout = options.clearTimeout || clearTimeout;
232
276
 
233
- let data = '';
234
- res.on('data', (chunk) => {
235
- data += chunk;
236
- });
277
+ return new Promise((resolve) => {
278
+ let request;
279
+ let safetyTimer;
280
+ let settled = false;
281
+
282
+ const finish = (value) => {
283
+ if (settled) return;
284
+ settled = true;
285
+ if (safetyTimer) cancelTimeout(safetyTimer);
286
+ request?.setTimeout?.(0);
287
+ resolve(value);
288
+ };
237
289
 
238
- res.on('end', () => {
239
- try {
240
- const json = JSON.parse(data);
241
- resolve(json.version || null);
242
- } catch {
243
- resolve(null);
290
+ try {
291
+ request = httpsModule.get(REGISTRY_URL, { timeout: timeoutMs }, (response) => {
292
+ if (response.statusCode !== 200) {
293
+ response.destroy?.();
294
+ request.destroy?.();
295
+ finish(null);
296
+ return;
244
297
  }
245
- });
246
- });
247
298
 
248
- req.on('error', () => {
249
- resolve(null);
250
- });
299
+ const chunks = [];
300
+ let size = 0;
301
+ response.on('data', (chunk) => {
302
+ if (settled) return;
303
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
304
+ size += buffer.length;
305
+ if (size > maxResponseBytes) {
306
+ response.destroy?.();
307
+ request.destroy?.();
308
+ finish(null);
309
+ return;
310
+ }
311
+ chunks.push(buffer);
312
+ });
313
+ response.on('error', () => finish(null));
314
+ response.on('end', () => {
315
+ if (settled) return;
316
+ try {
317
+ const manifest = JSON.parse(Buffer.concat(chunks, size).toString('utf8'));
318
+ finish(validatedManifestVersion(manifest));
319
+ } catch {
320
+ finish(null);
321
+ }
322
+ });
323
+ });
324
+ } catch {
325
+ finish(null);
326
+ return;
327
+ }
251
328
 
252
- req.on('timeout', () => {
253
- req.destroy();
254
- resolve(null);
329
+ request.on('error', () => finish(null));
330
+ request.on('timeout', () => {
331
+ request.destroy();
332
+ finish(null);
255
333
  });
334
+ if (options.unref) {
335
+ request.on('socket', (socket) => socket.unref?.());
336
+ }
256
337
 
257
- // Additional safety timeout
258
- setTimeout(() => {
259
- req.destroy();
260
- resolve(null);
261
- }, FETCH_TIMEOUT_MS + 1000);
338
+ safetyTimer = scheduleTimeout(() => {
339
+ request.destroy();
340
+ finish(null);
341
+ }, timeoutMs + 1000);
342
+ safetyTimer.unref?.();
262
343
  });
263
344
  }
264
345
 
265
- /**
266
- * Prompt user for update confirmation
267
- * @param {string} currentVersion
268
- * @param {string} latestVersion
269
- * @returns {Promise<boolean>} True if user wants to update
270
- */
271
- function promptForUpdate(currentVersion, latestVersion) {
272
- return new Promise((resolve) => {
273
- const rl = readline.createInterface({
274
- input: process.stdin,
275
- output: process.stdout,
276
- });
277
-
278
- console.log(`\nšŸ“¦ Update available: ${currentVersion} → ${latestVersion}`);
279
- rl.question(' Install now? [y/N] ', (answer) => {
280
- rl.close();
281
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
282
- });
283
- });
284
- }
285
346
 
286
347
  /**
287
348
  * Check if we have write permission to npm global directory
@@ -349,98 +410,419 @@ function runUpdate(options = {}) {
349
410
  });
350
411
  }
351
412
 
352
- /**
353
- * Check if update check should run
354
- * @param {object} settings - Current settings
355
- * @returns {boolean}
356
- */
357
- function shouldCheckForUpdates(settings) {
358
- // Disabled by user
359
- if (!settings.autoCheckUpdates) {
360
- return false;
413
+ function runLegacyUpdateIfRequested(argv, options = {}) {
414
+ const packageName = options.packageName || getCurrentPackageName();
415
+ if (!shouldForceLegacyUpdate(argv, packageName)) return null;
416
+ const installer = options.runUpdate || runUpdate;
417
+ return Promise.resolve(installer());
418
+ }
419
+
420
+ const GROUPABLE_GLOBAL_SHORT_FLAGS = new Set(['q', 'h', 'V']);
421
+
422
+ function hasGroupedGlobalShortFlag(argv) {
423
+ const end = argv.indexOf('--');
424
+ const limit = end === -1 ? argv.length : end;
425
+ for (let index = 0; index < limit; index += 1) {
426
+ const token = argv[index];
427
+ if (
428
+ token.length > 2 &&
429
+ token.startsWith('-') &&
430
+ !token.startsWith('--') &&
431
+ [...token.slice(1)].every((flag) => GROUPABLE_GLOBAL_SHORT_FLAGS.has(flag))
432
+ ) {
433
+ return true;
434
+ }
361
435
  }
436
+ return false;
437
+ }
362
438
 
363
- // Never checked before
364
- if (!settings.lastUpdateCheckAt) {
365
- return true;
439
+ function findSubcommand(command, name) {
440
+ return (command.commands || []).find((candidate) => {
441
+ return candidate.name() === name || candidate.aliases().includes(name);
442
+ });
443
+ }
444
+
445
+ function optionMaps(commandHierarchy) {
446
+ const short = new Map();
447
+ const long = new Map();
448
+ for (const command of commandHierarchy) {
449
+ const helpOption = command._getHelpOption?.();
450
+ for (const option of [helpOption, ...(command.options || [])]) {
451
+ if (!option) continue;
452
+ if (option.short) short.set(option.short, option);
453
+ if (option.long) long.set(option.long, option);
454
+ }
455
+ }
456
+ return { short, long };
457
+ }
458
+
459
+ function optionOccurrence(option, value = null) {
460
+ return { short: option.short || null, long: option.long || null, value };
461
+ }
462
+
463
+ function matchLongOption(token, maps) {
464
+ const equalsIndex = token.indexOf('=');
465
+ const name = equalsIndex === -1 ? token : token.slice(0, equalsIndex);
466
+ const option = maps.long.get(name);
467
+ if (!option) return null;
468
+ if (equalsIndex !== -1 && !option.required && !option.optional) return null;
469
+ const value = equalsIndex === -1 ? null : token.slice(equalsIndex + 1);
470
+ return {
471
+ occurrences: [optionOccurrence(option, value)],
472
+ needsValue: equalsIndex === -1 && (option.required || option.optional),
473
+ optionalValue: option.optional,
474
+ };
475
+ }
476
+
477
+ function matchShortOptions(token, maps) {
478
+ const exact = maps.short.get(token);
479
+ if (exact) {
480
+ return {
481
+ occurrences: [optionOccurrence(exact)],
482
+ needsValue: exact.required || exact.optional,
483
+ optionalValue: exact.optional,
484
+ };
485
+ }
486
+ if (token.length < 3 || !token.startsWith('-') || token.startsWith('--')) return null;
487
+
488
+ const occurrences = [];
489
+ for (let index = 1; index < token.length; index += 1) {
490
+ const option = maps.short.get(`-${token[index]}`);
491
+ if (!option) return null;
492
+ if (option.required || option.optional) {
493
+ occurrences.push(optionOccurrence(option, token.slice(index + 1) || null));
494
+ return {
495
+ occurrences,
496
+ needsValue: index === token.length - 1,
497
+ optionalValue: option.optional,
498
+ };
499
+ }
500
+ occurrences.push(optionOccurrence(option));
501
+ }
502
+ return { occurrences, needsValue: false, optionalValue: false };
503
+ }
504
+
505
+ function consumeOptionValue(match, argv, index, limit) {
506
+ if (!match.needsValue || index + 1 >= limit) return 0;
507
+ const value = argv[index + 1];
508
+ if (match.optionalValue && value.startsWith('-')) return 0;
509
+ match.occurrences[match.occurrences.length - 1].value = value;
510
+ return 1;
511
+ }
512
+
513
+ function classifyCommanderArguments(program, argv, defaultCommandName) {
514
+ const selectedCommandPath = [];
515
+ const occurrences = [];
516
+ const hierarchy = [program];
517
+ let consumedValueIndex = -1;
518
+ let command = program;
519
+ let positionalSeen = false;
520
+
521
+ for (let index = 0; index < argv.length; index += 1) {
522
+ if (index === consumedValueIndex) continue;
523
+ const token = argv[index];
524
+ if (token === '--') break;
525
+ const maps = optionMaps(hierarchy);
526
+ const match = token.startsWith('--')
527
+ ? matchLongOption(token, maps)
528
+ : matchShortOptions(token, maps);
529
+ if (match) {
530
+ if (consumeOptionValue(match, argv, index, argv.length) === 1) {
531
+ consumedValueIndex = index + 1;
532
+ }
533
+ occurrences.push(...match.occurrences);
534
+ continue;
535
+ }
536
+ if (token.startsWith('-')) continue;
537
+
538
+ const child = positionalSeen ? null : findSubcommand(command, token);
539
+ if (child) {
540
+ command = child;
541
+ hierarchy.push(child);
542
+ selectedCommandPath.push(child.name());
543
+ positionalSeen = false;
544
+ continue;
545
+ }
546
+ if (command === program && selectedCommandPath.length === 0 && defaultCommandName) {
547
+ const defaultCommand = findSubcommand(program, defaultCommandName);
548
+ if (defaultCommand) {
549
+ command = defaultCommand;
550
+ hierarchy.push(defaultCommand);
551
+ selectedCommandPath.push(defaultCommand.name());
552
+ }
553
+ }
554
+ positionalSeen = true;
555
+ }
556
+ return { selectedCommandPath, occurrences };
557
+ }
558
+
559
+ function parsedOptionPresent(metadata, argv, longName, shortName = null) {
560
+ if (!metadata) return optionIndex(argv, longName, shortName) !== -1;
561
+ return metadata.occurrences.some((entry) => {
562
+ return entry.long === longName || (shortName && entry.short === shortName);
563
+ });
564
+ }
565
+
566
+ function parsedOptionValue(metadata, argv, longName, shortName = null) {
567
+ if (!metadata) return optionValue(argv, longName, shortName);
568
+ for (let index = metadata.occurrences.length - 1; index >= 0; index -= 1) {
569
+ const entry = metadata.occurrences[index];
570
+ if (entry.long === longName || (shortName && entry.short === shortName)) {
571
+ return entry.value;
572
+ }
366
573
  }
574
+ return null;
575
+ }
367
576
 
368
- // Check if 24 hours have passed
369
- const elapsed = Date.now() - settings.lastUpdateCheckAt;
370
- return elapsed >= CHECK_INTERVAL_MS;
577
+ function optionIndex(argv, longName, shortName = null) {
578
+ const end = argv.indexOf('--');
579
+ const limit = end === -1 ? argv.length : end;
580
+ for (let index = 0; index < limit; index += 1) {
581
+ const token = argv[index];
582
+ if (token === longName || (shortName && token === shortName)) return index;
583
+ if (token.startsWith(`${longName}=`)) return index;
584
+ }
585
+ return -1;
586
+ }
587
+
588
+ function optionValue(argv, longName, shortName = null) {
589
+ const end = argv.indexOf('--');
590
+ const limit = end === -1 ? argv.length : end;
591
+ for (let index = 0; index < limit; index += 1) {
592
+ const token = argv[index];
593
+ if (token === longName || (shortName && token === shortName)) {
594
+ return argv[index + 1] ?? null;
595
+ }
596
+ if (token.startsWith(`${longName}=`)) return token.slice(longName.length + 1);
597
+ if (shortName && token.startsWith(shortName) && token.length > shortName.length) {
598
+ return token.slice(shortName.length);
599
+ }
600
+ }
601
+ return null;
602
+ }
603
+
604
+ function commandPath(argv) {
605
+ const positional = [];
606
+ const optionsWithValues = new Set([
607
+ '--format',
608
+ '-f',
609
+ '--output-format',
610
+ '--json-schema',
611
+ '--mcp-config',
612
+ ]);
613
+ const end = argv.indexOf('--');
614
+ const limit = end === -1 ? argv.length : end;
615
+ let skipNext = false;
616
+
617
+ for (let index = 0; index < limit; index += 1) {
618
+ const token = argv[index];
619
+ if (skipNext) {
620
+ skipNext = false;
621
+ continue;
622
+ }
623
+ if (optionsWithValues.has(token)) {
624
+ skipNext = true;
625
+ continue;
626
+ }
627
+ if (!token.startsWith('-')) positional.push(token);
628
+ }
629
+ return positional;
371
630
  }
372
631
 
373
632
  /**
374
- * Main entry point - check for updates
375
- * @param {object} options
376
- * @param {boolean} options.quiet - Skip interactive prompts
377
- * @returns {Promise<void>}
633
+ * Pure classification for optional automatic update work.
378
634
  */
379
- async function checkForUpdates(options = {}) {
380
- const settings = loadSettings();
635
+ function isAutomaticUpdateEligible(options = {}) {
636
+ const argv = options.argv || process.argv.slice(2);
637
+ const env = options.env || process.env;
638
+ const stdin = options.stdin || process.stdin;
639
+ const stdout = options.stdout || process.stdout;
640
+ const stderr = options.stderr || process.stderr;
641
+ const currentVersion = options.currentVersion || getCurrentVersion();
642
+ const packageName = options.packageName || getCurrentPackageName();
381
643
 
382
- // Skip check if not due
383
- if (!shouldCheckForUpdates(settings)) {
384
- return;
644
+ if (
645
+ packageName !== NEW_PACKAGE_NAME ||
646
+ !parseStableVersion(currentVersion) ||
647
+ argv.length === 0 ||
648
+ stdin.isTTY !== true ||
649
+ stdout.isTTY !== true ||
650
+ stderr.isTTY !== true ||
651
+ (env.CI !== undefined && env.CI !== null && String(env.CI).length > 0) ||
652
+ env.ZEROSHOT_DAEMON === '1'
653
+ ) {
654
+ return false;
385
655
  }
386
656
 
387
- const currentVersion = getCurrentVersion();
388
- const latestVersion = await fetchLatestVersion();
657
+ let metadata = null;
658
+ if (options.commanderProgram) {
659
+ try {
660
+ metadata = classifyCommanderArguments(
661
+ options.commanderProgram,
662
+ argv,
663
+ options.defaultCommandName
664
+ );
665
+ } catch {
666
+ return false;
667
+ }
668
+ }
389
669
 
390
- // Update last check timestamp regardless of result
391
- settings.lastUpdateCheckAt = Date.now();
392
- saveSettings(settings);
670
+ if (
671
+ parsedOptionPresent(metadata, argv, '--quiet', '-q') ||
672
+ parsedOptionPresent(metadata, argv, '--help', '-h') ||
673
+ parsedOptionPresent(metadata, argv, '--version', '-V') ||
674
+ (!metadata && hasGroupedGlobalShortFlag(argv)) ||
675
+ optionIndex(argv, '--completion') !== -1
676
+ ) {
677
+ return false;
678
+ }
393
679
 
394
- // Network failure - silently skip
395
- if (!latestVersion) {
396
- return;
680
+ const [command, subcommand] = metadata ? metadata.selectedCommandPath : commandPath(argv);
681
+ if (!command) return false;
682
+ if (
683
+ command === 'update' ||
684
+ command === 'get-log-path' ||
685
+ (command === 'task' && subcommand === 'run') ||
686
+ (command === 'setup' && ['plan', 'apply', 'undo'].includes(subcommand)) ||
687
+ (command === 'cmdproof' && ['prove', 'verify', 'check'].includes(subcommand))
688
+ ) {
689
+ return false;
397
690
  }
398
691
 
399
- // No update available
400
- if (!isNewerVersion(currentVersion, latestVersion)) {
401
- return;
692
+ if (
693
+ parsedOptionPresent(metadata, argv, '--json') ||
694
+ parsedOptionPresent(metadata, argv, '--silent-json-output') ||
695
+ parsedOptionPresent(metadata, argv, '--json-schema')
696
+ ) {
697
+ return false;
402
698
  }
403
699
 
404
- // Already notified about this version
405
- if (settings.lastSeenVersion === latestVersion) {
406
- return;
700
+ const outputFormat = parsedOptionValue(metadata, argv, '--output-format');
701
+ if (outputFormat === 'json' || outputFormat === 'stream-json') return false;
702
+ if (command === 'export' && parsedOptionValue(metadata, argv, '--format', '-f') === 'json') {
703
+ return false;
407
704
  }
408
705
 
409
- // Update lastSeenVersion so we don't nag about the same version
410
- settings.lastSeenVersion = latestVersion;
411
- saveSettings(settings);
706
+ return true;
707
+ }
412
708
 
413
- // Check write permissions upfront
414
- const hasWriteAccess = canWriteToNpmGlobal();
709
+ function shouldCheckForUpdates(settings, now = Date.now()) {
710
+ if (settings.autoCheckUpdates !== true) return false;
711
+ const timestamp = settings.lastUpdateCheckAt;
712
+ if (!Number.isFinite(timestamp) || timestamp < 0 || timestamp > now) return true;
713
+ return now - timestamp >= CHECK_INTERVAL_MS;
714
+ }
415
715
 
416
- // Quiet mode - just inform, no prompt
417
- if (options.quiet) {
418
- console.log(`šŸ“¦ Update available: ${currentVersion} → ${latestVersion}`);
419
- if (hasWriteAccess) {
420
- console.log(` Run: ${buildManualInstallCommand(getInstallPrefix(), false)}\n`);
421
- } else {
422
- console.log(` Run: ${buildManualInstallCommand(getInstallPrefix(), true)}\n`);
423
- }
716
+ async function refreshUpdateCache(options) {
717
+ const now = options.now || Date.now;
718
+ const transaction = options.mutateSettings || mutateSettings;
719
+ const fetcher = options.fetchLatestVersion || fetchLatestVersion;
720
+ const generateClaimId = options.generateClaimId || (() => crypto.randomUUID());
721
+ const attemptAt = now();
722
+ const claimId = generateClaimId();
723
+ let claim;
724
+
725
+ try {
726
+ claim = transaction(
727
+ (settings) => {
728
+ if (!shouldCheckForUpdates(settings, attemptAt)) return null;
729
+ settings.lastUpdateCheckAt = attemptAt;
730
+ settings.lastUpdateCheckClaim = claimId;
731
+ return { attemptAt, claimId };
732
+ },
733
+ { lockTimeoutMs: 0 }
734
+ );
735
+ } catch {
424
736
  return;
425
737
  }
738
+ if (!claim) return;
426
739
 
427
- // No write permission - inform user but don't offer interactive prompt
428
- // (they'd say yes then get an error, which is frustrating UX)
429
- if (!hasWriteAccess) {
430
- console.log(`\nšŸ“¦ Update available: ${currentVersion} → ${latestVersion}`);
431
- console.log(` Run: ${buildManualInstallCommand(getInstallPrefix(), true)}\n`);
740
+ let latestVersion;
741
+ try {
742
+ latestVersion = await fetcher({ unref: true });
743
+ } catch {
432
744
  return;
433
745
  }
746
+ if (!parseStableVersion(latestVersion)) return;
747
+
748
+ try {
749
+ transaction(
750
+ (settings) => {
751
+ if (
752
+ settings.autoCheckUpdates !== true ||
753
+ settings.lastUpdateCheckAt !== attemptAt ||
754
+ settings.lastUpdateCheckClaim !== claimId
755
+ ) {
756
+ return false;
757
+ }
758
+ settings.lastSeenVersion = latestVersion;
759
+ settings.lastUpdateCheckClaim = null;
760
+ return true;
761
+ },
762
+ { lockTimeoutMs: 0 }
763
+ );
764
+ } catch {
765
+ // Optional cache persistence must never affect command dispatch or output.
766
+ }
767
+ }
434
768
 
435
- // Interactive mode - prompt for update (only if we have write access)
436
- const wantsUpdate = await promptForUpdate(currentVersion, latestVersion);
437
- if (wantsUpdate) {
438
- await runUpdate();
769
+ /**
770
+ * Synchronously render the startup cache and begin a due refresh without awaiting it.
771
+ * @returns {Promise<void>|null} the shared refresh, exposed for deterministic tests
772
+ */
773
+ function checkForUpdates(options = {}) {
774
+ if (!options.eligibilityChecked && !isAutomaticUpdateEligible(options)) return null;
775
+
776
+ const currentVersion = options.currentVersion || getCurrentVersion();
777
+ if (!parseStableVersion(currentVersion)) return null;
778
+ const readSettings = options.loadSettings || loadSettings;
779
+ let settings;
780
+ try {
781
+ settings = readSettings({ silent: true });
782
+ } catch {
783
+ return null;
439
784
  }
785
+ if (settings.autoCheckUpdates !== true) return null;
786
+
787
+ if (
788
+ parseStableVersion(settings.lastSeenVersion) &&
789
+ isNewerVersion(currentVersion, settings.lastSeenVersion)
790
+ ) {
791
+ try {
792
+ const stderr = options.stderr || process.stderr;
793
+ stderr.write(
794
+ `Update available: ${currentVersion} → ${settings.lastSeenVersion}. Run \`zeroshot update\`.\n`
795
+ );
796
+ } catch {
797
+ // Optional notification output failures are contained.
798
+ }
799
+ }
800
+
801
+ const now = options.now || Date.now;
802
+ if (!shouldCheckForUpdates(settings, now())) return null;
803
+ if (inFlightRefresh) return inFlightRefresh;
804
+
805
+ const refresh = new Promise((resolve) => {
806
+ const schedule = options.scheduleRefresh || setImmediate;
807
+ const handle = schedule(() => {
808
+ Promise.resolve(refreshUpdateCache(options))
809
+ .catch(() => {})
810
+ .then(resolve);
811
+ });
812
+ handle?.unref?.();
813
+ });
814
+ inFlightRefresh = refresh;
815
+ refresh.then(() => {
816
+ if (inFlightRefresh === refresh) inFlightRefresh = null;
817
+ });
818
+ return refresh;
440
819
  }
441
820
 
442
821
  module.exports = {
443
822
  checkForUpdates,
823
+ isAutomaticUpdateEligible,
824
+ parseStableVersion,
825
+ validatedManifestVersion,
444
826
  // Exported for testing and CLI update command
445
827
  NEW_PACKAGE_NAME,
446
828
  LEGACY_PACKAGE_NAME,
@@ -448,6 +830,7 @@ module.exports = {
448
830
  getCurrentPackageName,
449
831
  isLegacyDistro,
450
832
  printLegacyDistroNotice,
833
+ shouldForceLegacyUpdate,
451
834
  deriveInstallPrefixFromPackageRoot,
452
835
  getInstallPrefix,
453
836
  resolveNpmCommand,
@@ -456,7 +839,9 @@ module.exports = {
456
839
  isNewerVersion,
457
840
  fetchLatestVersion,
458
841
  runUpdate,
842
+ runLegacyUpdateIfRequested,
459
843
  shouldCheckForUpdates,
460
844
  canWriteToNpmGlobal,
461
845
  CHECK_INTERVAL_MS,
846
+ MAX_RESPONSE_BYTES,
462
847
  };