@imdeadpool/guardex 7.0.21 → 7.0.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.
@@ -1,223 +1,603 @@
1
- function createToolchainApi(deps) {
2
- const {
3
- TOOL_NAME,
4
- NPM_BIN,
5
- NPX_BIN,
6
- packageJson,
7
- OPENSPEC_PACKAGE,
8
- OPENSPEC_BIN,
9
- GLOBAL_TOOLCHAIN_PACKAGES,
10
- parseAutoApproval,
11
- isInteractiveTerminal,
12
- promptYesNoStrict,
13
- run,
14
- checkForGuardexUpdate,
15
- printUpdateAvailableBanner,
16
- readInstalledGuardexVersion,
17
- restartIntoUpdatedGuardex,
18
- checkForOpenSpecPackageUpdate,
19
- printOpenSpecUpdateAvailableBanner,
20
- resolveGlobalInstallApproval,
21
- detectGlobalToolchainPackages,
22
- detectOptionalLocalCompanionTools,
23
- formatGlobalToolchainServiceName,
24
- askGlobalInstallForMissing,
25
- } = deps;
26
-
27
- function maybeSelfUpdateBeforeStatus() {
28
- const check = checkForGuardexUpdate();
29
- if (!check.checked || !check.updateAvailable) {
30
- return;
31
- }
1
+ const {
2
+ fs,
3
+ path,
4
+ cp,
5
+ packageJson,
6
+ TOOL_NAME,
7
+ SHORT_TOOL_NAME,
8
+ OPENSPEC_PACKAGE,
9
+ NPX_BIN,
10
+ GUARDEX_HOME_DIR,
11
+ GLOBAL_TOOLCHAIN_SERVICES,
12
+ GLOBAL_TOOLCHAIN_PACKAGES,
13
+ OPTIONAL_LOCAL_COMPANION_TOOLS,
14
+ REQUIRED_SYSTEM_TOOLS,
15
+ NPM_BIN,
16
+ OPENSPEC_BIN,
17
+ envFlagIsTruthy,
18
+ } = require('../context');
19
+ const { run } = require('../core/runtime');
20
+ const {
21
+ parseVersionString,
22
+ compareParsedVersions,
23
+ isNewerVersion,
24
+ } = require('../core/versions');
25
+ const { readSingleLineFromStdin } = require('../core/stdin');
26
+ const { colorize } = require('../output');
27
+
28
+ function isInteractiveTerminal() {
29
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
30
+ }
32
31
 
33
- printUpdateAvailableBanner(check.current, check.latest);
32
+ function parseAutoApproval(name) {
33
+ const raw = process.env[name];
34
+ if (raw == null) return null;
35
+ const normalized = String(raw).trim().toLowerCase();
36
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
37
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
38
+ return null;
39
+ }
34
40
 
35
- const autoApproval = parseAutoApproval('GUARDEX_AUTO_UPDATE_APPROVAL');
36
- const interactive = isInteractiveTerminal();
41
+ function parseNpmVersionOutput(stdout) {
42
+ const trimmed = String(stdout || '').trim();
43
+ if (!trimmed) return '';
37
44
 
38
- if (!interactive && autoApproval == null) {
39
- console.log(`[${TOOL_NAME}] Non-interactive shell; skipping auto-update prompt.`);
40
- return;
45
+ try {
46
+ const parsed = JSON.parse(trimmed);
47
+ if (Array.isArray(parsed)) {
48
+ return String(parsed[parsed.length - 1] || '').trim();
41
49
  }
50
+ return String(parsed || '').trim();
51
+ } catch {
52
+ const firstLine = trimmed.split('\n').map((line) => line.trim()).find(Boolean);
53
+ return firstLine || '';
54
+ }
55
+ }
42
56
 
43
- const shouldUpdate = interactive
44
- ? promptYesNoStrict(
45
- `Update now? (${NPM_BIN} i -g ${packageJson.name}@latest)`,
46
- )
47
- : autoApproval;
57
+ function checkForGuardexUpdate() {
58
+ if (envFlagIsTruthy(process.env.GUARDEX_SKIP_UPDATE_CHECK)) {
59
+ return { checked: false, reason: 'disabled' };
60
+ }
48
61
 
49
- if (!shouldUpdate) {
50
- console.log(`[${TOOL_NAME}] Skipped update.`);
51
- return;
52
- }
62
+ const forceCheck = envFlagIsTruthy(process.env.GUARDEX_FORCE_UPDATE_CHECK);
63
+ if (!forceCheck && !isInteractiveTerminal()) {
64
+ return { checked: false, reason: 'non-interactive' };
65
+ }
53
66
 
54
- const installResult = run(NPM_BIN, ['i', '-g', `${packageJson.name}@latest`], { stdio: 'inherit' });
55
- if (installResult.status !== 0) {
56
- console.log(`[${TOOL_NAME}] ⚠️ Update failed. You can retry manually.`);
57
- return;
58
- }
67
+ const result = run(NPM_BIN, ['view', packageJson.name, 'version', '--json'], { timeout: 5000 });
68
+ if (result.status !== 0) {
69
+ return { checked: false, reason: 'lookup-failed' };
70
+ }
59
71
 
60
- const postInstallVersion = readInstalledGuardexVersion();
61
- if (postInstallVersion != null && postInstallVersion !== check.latest) {
62
- console.log(
63
- `[${TOOL_NAME}] Installed version is still ${postInstallVersion} (expected ${check.latest}). ` +
64
- `Retrying with pinned version ${check.latest}…`,
65
- );
66
- const pinnedResult = run(
67
- NPM_BIN,
68
- ['i', '-g', `${packageJson.name}@${check.latest}`],
69
- { stdio: 'inherit' },
70
- );
71
- if (pinnedResult.status !== 0) {
72
- console.log(
73
- `[${TOOL_NAME}] ⚠️ Pinned retry failed. Run manually: ${NPM_BIN} i -g ${packageJson.name}@${check.latest}`,
74
- );
75
- return;
76
- }
77
- const pinnedVersion = readInstalledGuardexVersion();
78
- if (pinnedVersion != null && pinnedVersion !== check.latest) {
79
- console.log(
80
- `[${TOOL_NAME}] ⚠️ On-disk version still ${pinnedVersion} after pinned retry. ` +
81
- `Investigate: ${NPM_BIN} root -g && ${NPM_BIN} cache verify`,
82
- );
83
- return;
72
+ const latest = parseNpmVersionOutput(result.stdout);
73
+ if (!latest) {
74
+ return { checked: false, reason: 'invalid-latest-version' };
75
+ }
76
+
77
+ return {
78
+ checked: true,
79
+ current: packageJson.version,
80
+ latest,
81
+ updateAvailable: isNewerVersion(latest, packageJson.version),
82
+ };
83
+ }
84
+
85
+ function printUpdateAvailableBanner(current, latest) {
86
+ const title = colorize('UPDATE AVAILABLE', '1;33');
87
+ console.log(`[${TOOL_NAME}] ${title}`);
88
+ console.log(`[${TOOL_NAME}] Current: ${current}`);
89
+ console.log(`[${TOOL_NAME}] Latest : ${latest}`);
90
+ console.log(`[${TOOL_NAME}] Command: ${NPM_BIN} i -g ${packageJson.name}@latest`);
91
+ }
92
+
93
+ function readInstalledGuardexVersion() {
94
+ const installInfo = readInstalledGuardexInstallInfo();
95
+ return installInfo ? installInfo.version : null;
96
+ }
97
+
98
+ function readInstalledGuardexInstallInfo() {
99
+ try {
100
+ const rootResult = run(NPM_BIN, ['root', '-g'], { timeout: 5000 });
101
+ if (rootResult.status !== 0) {
102
+ return null;
103
+ }
104
+ const globalRoot = String(rootResult.stdout || '').trim();
105
+ if (!globalRoot) {
106
+ return null;
107
+ }
108
+ const installedPkgPath = path.join(globalRoot, packageJson.name, 'package.json');
109
+ if (!fs.existsSync(installedPkgPath)) {
110
+ return null;
111
+ }
112
+ const parsed = JSON.parse(fs.readFileSync(installedPkgPath, 'utf8'));
113
+ if (parsed && typeof parsed.version === 'string') {
114
+ let binRelative = null;
115
+ if (typeof parsed.bin === 'string') {
116
+ binRelative = parsed.bin;
117
+ } else if (parsed.bin && typeof parsed.bin === 'object') {
118
+ const invokedName = path.basename(process.argv[1] || '');
119
+ binRelative =
120
+ parsed.bin[invokedName] ||
121
+ parsed.bin[SHORT_TOOL_NAME] ||
122
+ Object.values(parsed.bin).find((value) => typeof value === 'string') ||
123
+ null;
84
124
  }
125
+ const packageRoot = path.dirname(installedPkgPath);
126
+ const binPath = binRelative ? path.join(packageRoot, binRelative) : null;
127
+ return {
128
+ version: parsed.version,
129
+ packageRoot,
130
+ binPath,
131
+ };
85
132
  }
133
+ } catch {
134
+ return null;
135
+ }
136
+ return null;
137
+ }
86
138
 
87
- console.log(`[${TOOL_NAME}] ✅ Updated to latest published version.`);
88
- restartIntoUpdatedGuardex(check.latest);
139
+ function restartIntoUpdatedGuardex(expectedVersion) {
140
+ const installInfo = readInstalledGuardexInstallInfo();
141
+ if (!installInfo || installInfo.version !== expectedVersion || installInfo.version === packageJson.version) {
142
+ return;
143
+ }
144
+ if (!installInfo.binPath || !fs.existsSync(installInfo.binPath)) {
145
+ console.log(`[${TOOL_NAME}] Restart required to use ${installInfo.version}. Rerun ${SHORT_TOOL_NAME}.`);
146
+ return;
89
147
  }
90
148
 
91
- function maybeOpenSpecUpdateBeforeStatus() {
92
- const check = checkForOpenSpecPackageUpdate();
93
- if (!check.checked || !check.updateAvailable) {
94
- return;
95
- }
149
+ console.log(`[${TOOL_NAME}] Restarting into ${installInfo.version}…`);
150
+ const restartResult = cp.spawnSync(
151
+ process.execPath,
152
+ [installInfo.binPath, ...process.argv.slice(2)],
153
+ {
154
+ cwd: process.cwd(),
155
+ env: {
156
+ ...process.env,
157
+ GUARDEX_SKIP_UPDATE_CHECK: '1',
158
+ },
159
+ stdio: 'inherit',
160
+ },
161
+ );
162
+ if (restartResult.error) {
163
+ console.log(
164
+ `[${TOOL_NAME}] Restart into ${installInfo.version} failed. Rerun ${SHORT_TOOL_NAME}.`,
165
+ );
166
+ return;
167
+ }
168
+ process.exit(restartResult.status == null ? 0 : restartResult.status);
169
+ }
96
170
 
97
- printOpenSpecUpdateAvailableBanner(check.current, check.latest);
171
+ function checkForOpenSpecPackageUpdate() {
172
+ if (envFlagIsTruthy(process.env.GUARDEX_SKIP_OPENSPEC_UPDATE_CHECK)) {
173
+ return { checked: false, reason: 'disabled' };
174
+ }
98
175
 
99
- const autoApproval = parseAutoApproval('GUARDEX_AUTO_OPENSPEC_UPDATE_APPROVAL');
100
- const interactive = isInteractiveTerminal();
176
+ const forceCheck = envFlagIsTruthy(process.env.GUARDEX_FORCE_OPENSPEC_UPDATE_CHECK);
177
+ if (!forceCheck && !isInteractiveTerminal()) {
178
+ return { checked: false, reason: 'non-interactive' };
179
+ }
101
180
 
102
- if (!interactive && autoApproval == null) {
103
- console.log(`[${TOOL_NAME}] Non-interactive shell; skipping OpenSpec update prompt.`);
104
- return;
181
+ const detection = detectGlobalToolchainPackages();
182
+ if (!detection.ok) {
183
+ return { checked: false, reason: 'package-detect-failed' };
184
+ }
185
+
186
+ const current = String((detection.installedVersions || {})[OPENSPEC_PACKAGE] || '').trim();
187
+ if (!current) {
188
+ return { checked: false, reason: 'not-installed' };
189
+ }
190
+
191
+ const latestResult = run(NPM_BIN, ['view', OPENSPEC_PACKAGE, 'version', '--json'], { timeout: 5000 });
192
+ if (latestResult.status !== 0) {
193
+ return { checked: false, reason: 'lookup-failed' };
194
+ }
195
+
196
+ const latest = parseNpmVersionOutput(latestResult.stdout);
197
+ if (!latest) {
198
+ return { checked: false, reason: 'invalid-latest-version' };
199
+ }
200
+
201
+ return {
202
+ checked: true,
203
+ current,
204
+ latest,
205
+ updateAvailable: isNewerVersion(latest, current),
206
+ };
207
+ }
208
+
209
+ function printOpenSpecUpdateAvailableBanner(current, latest) {
210
+ const title = colorize('OPENSPEC UPDATE AVAILABLE', '1;33');
211
+ console.log(`[${TOOL_NAME}] ${title}`);
212
+ console.log(`[${TOOL_NAME}] Current: ${current}`);
213
+ console.log(`[${TOOL_NAME}] Latest : ${latest}`);
214
+ console.log(`[${TOOL_NAME}] Command: ${NPM_BIN} i -g ${OPENSPEC_PACKAGE}@latest`);
215
+ console.log(`[${TOOL_NAME}] Then : ${OPENSPEC_BIN} update`);
216
+ }
217
+
218
+ function promptYesNoStrict(question) {
219
+ while (true) {
220
+ process.stdout.write(`${question} [y/n] `);
221
+ const answer = readSingleLineFromStdin().trim().toLowerCase();
222
+
223
+ if (answer === 'y' || answer === 'yes') {
224
+ process.stdout.write('\n');
225
+ return true;
226
+ }
227
+ if (answer === 'n' || answer === 'no') {
228
+ process.stdout.write('\n');
229
+ return false;
105
230
  }
106
231
 
107
- const shouldUpdate = interactive
108
- ? promptYesNoStrict(
109
- `Update OpenSpec now? (${NPM_BIN} i -g ${OPENSPEC_PACKAGE}@latest && ${OPENSPEC_BIN} update)`,
110
- )
111
- : autoApproval;
232
+ process.stdout.write('Please answer with y or n.\n');
233
+ }
234
+ }
112
235
 
113
- if (!shouldUpdate) {
114
- console.log(`[${TOOL_NAME}] Skipped OpenSpec update.`);
115
- return;
236
+ function resolveGlobalInstallApproval(options) {
237
+ if (options.yesGlobalInstall && options.noGlobalInstall) {
238
+ throw new Error('Cannot use both --yes-global-install and --no-global-install');
239
+ }
240
+
241
+ if (options.yesGlobalInstall) {
242
+ return { approved: true, source: 'flag' };
243
+ }
244
+
245
+ if (options.noGlobalInstall) {
246
+ return { approved: false, source: 'flag' };
247
+ }
248
+
249
+ if (!isInteractiveTerminal()) {
250
+ return { approved: false, source: 'non-interactive-default' };
251
+ }
252
+ return { approved: true, source: 'prompt' };
253
+ }
254
+
255
+ function getGlobalToolchainService(packageName) {
256
+ const service = GLOBAL_TOOLCHAIN_SERVICES.find(
257
+ (candidate) => candidate.packageName === packageName,
258
+ );
259
+ return service || { name: packageName, packageName };
260
+ }
261
+
262
+ function formatGlobalToolchainServiceName(packageName) {
263
+ return getGlobalToolchainService(packageName).name;
264
+ }
265
+
266
+ function describeMissingGlobalDependencyWarnings(packageNames) {
267
+ return packageNames
268
+ .map((packageName) => getGlobalToolchainService(packageName))
269
+ .filter((service) => service.dependencyUrl)
270
+ .map(
271
+ (service) =>
272
+ `Guardex needs ${service.name} as a dependency: ${service.dependencyUrl}`,
273
+ );
274
+ }
275
+
276
+ function describeCompanionInstallCommands(missingPackages, missingLocalTools) {
277
+ const commands = [];
278
+ if (missingPackages.length > 0) {
279
+ commands.push(`${NPM_BIN} i -g ${missingPackages.join(' ')}`);
280
+ }
281
+ for (const tool of missingLocalTools) {
282
+ commands.push(tool.installCommand);
283
+ }
284
+ return commands;
285
+ }
286
+
287
+ function buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools) {
288
+ const dependencyWarnings = describeMissingGlobalDependencyWarnings(missingPackages);
289
+ const installCommands = describeCompanionInstallCommands(missingPackages, missingLocalTools);
290
+ const dependencyPrefix = dependencyWarnings.length > 0
291
+ ? `${dependencyWarnings.join(' ')} `
292
+ : '';
293
+ return `${dependencyPrefix}Install missing companion tools now? (${installCommands.join(' && ')})`;
294
+ }
295
+
296
+ function detectGlobalToolchainPackages() {
297
+ const result = run(NPM_BIN, ['list', '-g', '--depth=0', '--json']);
298
+ if (result.status !== 0) {
299
+ const stderr = (result.stderr || '').trim();
300
+ return {
301
+ ok: false,
302
+ error: stderr || 'Unable to detect globally installed npm packages',
303
+ };
304
+ }
305
+
306
+ let parsed;
307
+ try {
308
+ parsed = JSON.parse(result.stdout || '{}');
309
+ } catch (error) {
310
+ return {
311
+ ok: false,
312
+ error: `Failed to parse npm list output: ${error.message}`,
313
+ };
314
+ }
315
+
316
+ const dependencyMap = parsed && parsed.dependencies && typeof parsed.dependencies === 'object'
317
+ ? parsed.dependencies
318
+ : {};
319
+ const installedSet = new Set(Object.keys(dependencyMap));
320
+
321
+ const installed = [];
322
+ const missing = [];
323
+ const installedVersions = {};
324
+ for (const pkg of GLOBAL_TOOLCHAIN_PACKAGES) {
325
+ if (installedSet.has(pkg)) {
326
+ installed.push(pkg);
327
+ const rawVersion = dependencyMap[pkg] && dependencyMap[pkg].version;
328
+ const version = String(rawVersion || '').trim();
329
+ if (version) {
330
+ installedVersions[pkg] = version;
331
+ }
332
+ } else {
333
+ missing.push(pkg);
116
334
  }
335
+ }
336
+
337
+ return { ok: true, installed, missing, installedVersions };
338
+ }
339
+
340
+ function detectRequiredSystemTools() {
341
+ const services = [];
342
+ for (const tool of REQUIRED_SYSTEM_TOOLS) {
343
+ const result = run(tool.command, ['--version']);
344
+ const active = result.status === 0;
345
+ const rawReason = result.error && result.error.code
346
+ ? result.error.code
347
+ : (result.stderr || '').trim();
348
+ const reason = rawReason.split('\n')[0] || '';
349
+ services.push({
350
+ name: tool.name,
351
+ displayName: tool.displayName || tool.name,
352
+ command: tool.command,
353
+ installHint: tool.installHint,
354
+ status: active ? 'active' : 'inactive',
355
+ reason,
356
+ });
357
+ }
358
+ return services;
359
+ }
360
+
361
+ function detectOptionalLocalCompanionTools() {
362
+ return OPTIONAL_LOCAL_COMPANION_TOOLS.map((tool) => {
363
+ const detectedPath = tool.candidatePaths
364
+ .map((relativePath) => path.join(GUARDEX_HOME_DIR, relativePath))
365
+ .find((candidatePath) => fs.existsSync(candidatePath));
366
+ return {
367
+ name: tool.name,
368
+ displayName: tool.displayName || tool.name,
369
+ installCommand: tool.installCommand,
370
+ installArgs: [...tool.installArgs],
371
+ status: detectedPath ? 'active' : 'inactive',
372
+ detectedPath: detectedPath || null,
373
+ };
374
+ });
375
+ }
376
+
377
+ function askGlobalInstallForMissing(options, missingPackages, missingLocalTools) {
378
+ const approval = resolveGlobalInstallApproval(options);
379
+ if (!approval.approved) {
380
+ return approval;
381
+ }
382
+
383
+ if (approval.source === 'prompt') {
384
+ const approved = promptYesNoStrict(
385
+ buildMissingCompanionInstallPrompt(missingPackages, missingLocalTools),
386
+ );
387
+ return { approved, source: 'prompt' };
388
+ }
117
389
 
118
- const installResult = run(NPM_BIN, ['i', '-g', `${OPENSPEC_PACKAGE}@latest`], { stdio: 'inherit' });
119
- if (installResult.status !== 0) {
120
- console.log(`[${TOOL_NAME}] ⚠️ OpenSpec npm install failed. You can retry manually.`);
390
+ return approval;
391
+ }
392
+
393
+ function maybeSelfUpdateBeforeStatus() {
394
+ const check = checkForGuardexUpdate();
395
+ if (!check.checked || !check.updateAvailable) {
396
+ return;
397
+ }
398
+
399
+ printUpdateAvailableBanner(check.current, check.latest);
400
+
401
+ const autoApproval = parseAutoApproval('GUARDEX_AUTO_UPDATE_APPROVAL');
402
+ const interactive = isInteractiveTerminal();
403
+
404
+ if (!interactive && autoApproval == null) {
405
+ console.log(`[${TOOL_NAME}] Non-interactive shell; skipping auto-update prompt.`);
406
+ return;
407
+ }
408
+
409
+ const shouldUpdate = interactive
410
+ ? promptYesNoStrict(
411
+ `Update now? (${NPM_BIN} i -g ${packageJson.name}@latest)`,
412
+ )
413
+ : autoApproval;
414
+
415
+ if (!shouldUpdate) {
416
+ console.log(`[${TOOL_NAME}] Skipped update.`);
417
+ return;
418
+ }
419
+
420
+ const installResult = run(NPM_BIN, ['i', '-g', `${packageJson.name}@latest`], { stdio: 'inherit' });
421
+ if (installResult.status !== 0) {
422
+ console.log(`[${TOOL_NAME}] Update failed. You can retry manually.`);
423
+ return;
424
+ }
425
+
426
+ const postInstallVersion = readInstalledGuardexVersion();
427
+ if (postInstallVersion != null && postInstallVersion !== check.latest) {
428
+ console.log(
429
+ `[${TOOL_NAME}] Installed version is still ${postInstallVersion} (expected ${check.latest}). ` +
430
+ `Retrying with pinned version ${check.latest}...`,
431
+ );
432
+ const pinnedResult = run(
433
+ NPM_BIN,
434
+ ['i', '-g', `${packageJson.name}@${check.latest}`],
435
+ { stdio: 'inherit' },
436
+ );
437
+ if (pinnedResult.status !== 0) {
438
+ console.log(
439
+ `[${TOOL_NAME}] Pinned retry failed. Run manually: ${NPM_BIN} i -g ${packageJson.name}@${check.latest}`,
440
+ );
121
441
  return;
122
442
  }
123
-
124
- const toolUpdateResult = run(OPENSPEC_BIN, ['update'], { stdio: 'inherit' });
125
- if (toolUpdateResult.status !== 0) {
126
- console.log(`[${TOOL_NAME}] ⚠️ OpenSpec tool update failed. Run '${OPENSPEC_BIN} update' manually.`);
443
+ const pinnedVersion = readInstalledGuardexVersion();
444
+ if (pinnedVersion != null && pinnedVersion !== check.latest) {
445
+ console.log(
446
+ `[${TOOL_NAME}] On-disk version still ${pinnedVersion} after pinned retry. ` +
447
+ `Investigate: ${NPM_BIN} root -g && ${NPM_BIN} cache verify`,
448
+ );
127
449
  return;
128
450
  }
451
+ }
452
+
453
+ console.log(`[${TOOL_NAME}] Updated to latest published version.`);
454
+ restartIntoUpdatedGuardex(check.latest);
455
+ }
129
456
 
130
- console.log(`[${TOOL_NAME}] ✅ OpenSpec updated to latest package and tool plugins refreshed.`);
457
+ function maybeOpenSpecUpdateBeforeStatus() {
458
+ const check = checkForOpenSpecPackageUpdate();
459
+ if (!check.checked || !check.updateAvailable) {
460
+ return;
131
461
  }
132
462
 
133
- function installGlobalToolchain(options) {
134
- const approval = resolveGlobalInstallApproval(options);
135
- if (approval.source === 'flag' && !approval.approved) {
136
- return {
137
- status: 'skipped',
138
- reason: approval.source,
139
- missingPackages: [],
140
- missingLocalTools: [],
141
- };
142
- }
463
+ printOpenSpecUpdateAvailableBanner(check.current, check.latest);
143
464
 
144
- if (options.dryRun) {
145
- return { status: 'dry-run-skip' };
146
- }
465
+ const autoApproval = parseAutoApproval('GUARDEX_AUTO_OPENSPEC_UPDATE_APPROVAL');
466
+ const interactive = isInteractiveTerminal();
147
467
 
148
- const detection = detectGlobalToolchainPackages();
149
- const localCompanionTools = detectOptionalLocalCompanionTools();
150
- if (!detection.ok) {
151
- console.log(`[${TOOL_NAME}] ⚠️ Could not detect global packages: ${detection.error}`);
152
- } else {
153
- if (detection.installed.length > 0) {
154
- console.log(
155
- `[${TOOL_NAME}] Already installed globally: ` +
156
- `${detection.installed.map((pkg) => formatGlobalToolchainServiceName(pkg)).join(', ')}`,
157
- );
158
- }
159
- const installedLocalTools = localCompanionTools
160
- .filter((tool) => tool.status === 'active')
161
- .map((tool) => tool.name);
162
- if (installedLocalTools.length > 0) {
163
- console.log(`[${TOOL_NAME}] Already installed locally: ${installedLocalTools.join(', ')}`);
164
- }
165
- if (detection.missing.length === 0 && localCompanionTools.every((tool) => tool.status === 'active')) {
166
- return { status: 'already-installed' };
167
- }
168
- }
468
+ if (!interactive && autoApproval == null) {
469
+ console.log(`[${TOOL_NAME}] Non-interactive shell; skipping OpenSpec update prompt.`);
470
+ return;
471
+ }
169
472
 
170
- const missingPackages = detection.ok ? detection.missing : [...GLOBAL_TOOLCHAIN_PACKAGES];
171
- const missingLocalTools = localCompanionTools.filter((tool) => tool.status !== 'active');
172
- const installApproval = askGlobalInstallForMissing(options, missingPackages, missingLocalTools);
173
- if (!installApproval.approved) {
174
- return {
175
- status: 'skipped',
176
- reason: installApproval.source,
177
- missingPackages,
178
- missingLocalTools,
179
- };
180
- }
473
+ const shouldUpdate = interactive
474
+ ? promptYesNoStrict(
475
+ `Update OpenSpec now? (${NPM_BIN} i -g ${OPENSPEC_PACKAGE}@latest && ${OPENSPEC_BIN} update)`,
476
+ )
477
+ : autoApproval;
478
+
479
+ if (!shouldUpdate) {
480
+ console.log(`[${TOOL_NAME}] Skipped OpenSpec update.`);
481
+ return;
482
+ }
181
483
 
182
- const installed = [];
183
- if (missingPackages.length > 0) {
484
+ const installResult = run(NPM_BIN, ['i', '-g', `${OPENSPEC_PACKAGE}@latest`], { stdio: 'inherit' });
485
+ if (installResult.status !== 0) {
486
+ console.log(`[${TOOL_NAME}] OpenSpec npm install failed. You can retry manually.`);
487
+ return;
488
+ }
489
+
490
+ const toolUpdateResult = run(OPENSPEC_BIN, ['update'], { stdio: 'inherit' });
491
+ if (toolUpdateResult.status !== 0) {
492
+ console.log(`[${TOOL_NAME}] OpenSpec tool update failed. Run '${OPENSPEC_BIN} update' manually.`);
493
+ return;
494
+ }
495
+
496
+ console.log(`[${TOOL_NAME}] OpenSpec updated to latest package and tool plugins refreshed.`);
497
+ }
498
+
499
+ function installGlobalToolchain(options) {
500
+ const approval = resolveGlobalInstallApproval(options);
501
+ if (approval.source === 'flag' && !approval.approved) {
502
+ return {
503
+ status: 'skipped',
504
+ reason: approval.source,
505
+ missingPackages: [],
506
+ missingLocalTools: [],
507
+ };
508
+ }
509
+
510
+ if (options.dryRun) {
511
+ return { status: 'dry-run-skip' };
512
+ }
513
+
514
+ const detection = detectGlobalToolchainPackages();
515
+ const localCompanionTools = detectOptionalLocalCompanionTools();
516
+ if (!detection.ok) {
517
+ console.log(`[${TOOL_NAME}] Could not detect global packages: ${detection.error}`);
518
+ } else {
519
+ if (detection.installed.length > 0) {
184
520
  console.log(
185
- `[${TOOL_NAME}] Installing global toolchain: npm i -g ${missingPackages.join(' ')}`,
521
+ `[${TOOL_NAME}] Already installed globally: ` +
522
+ `${detection.installed.map((pkg) => formatGlobalToolchainServiceName(pkg)).join(', ')}`,
186
523
  );
187
- const result = run(NPM_BIN, ['i', '-g', ...missingPackages], { stdio: 'inherit' });
188
- if (result.status !== 0) {
189
- const stderr = (result.stderr || '').trim();
190
- return {
191
- status: 'failed',
192
- reason: stderr || 'npm global install failed',
193
- };
194
- }
195
- installed.push(...missingPackages);
196
524
  }
525
+ const installedLocalTools = localCompanionTools
526
+ .filter((tool) => tool.status === 'active')
527
+ .map((tool) => tool.name);
528
+ if (installedLocalTools.length > 0) {
529
+ console.log(`[${TOOL_NAME}] Already installed locally: ${installedLocalTools.join(', ')}`);
530
+ }
531
+ if (detection.missing.length === 0 && localCompanionTools.every((tool) => tool.status === 'active')) {
532
+ return { status: 'already-installed' };
533
+ }
534
+ }
197
535
 
198
- for (const tool of missingLocalTools) {
199
- console.log(`[${TOOL_NAME}] Installing local companion tool: ${tool.installCommand}`);
200
- const result = run(NPX_BIN, tool.installArgs, { stdio: 'inherit' });
201
- if (result.status !== 0) {
202
- const stderr = (result.stderr || '').trim();
203
- return {
204
- status: 'failed',
205
- reason: stderr || `${tool.name} install failed`,
206
- };
207
- }
208
- installed.push(tool.name);
536
+ const missingPackages = detection.ok ? detection.missing : [...GLOBAL_TOOLCHAIN_PACKAGES];
537
+ const missingLocalTools = localCompanionTools.filter((tool) => tool.status !== 'active');
538
+ const installApproval = askGlobalInstallForMissing(options, missingPackages, missingLocalTools);
539
+ if (!installApproval.approved) {
540
+ return {
541
+ status: 'skipped',
542
+ reason: installApproval.source,
543
+ missingPackages,
544
+ missingLocalTools,
545
+ };
546
+ }
547
+
548
+ const installed = [];
549
+ if (missingPackages.length > 0) {
550
+ console.log(
551
+ `[${TOOL_NAME}] Installing global toolchain: npm i -g ${missingPackages.join(' ')}`,
552
+ );
553
+ const result = run(NPM_BIN, ['i', '-g', ...missingPackages], { stdio: 'inherit' });
554
+ if (result.status !== 0) {
555
+ const stderr = (result.stderr || '').trim();
556
+ return {
557
+ status: 'failed',
558
+ reason: stderr || 'npm global install failed',
559
+ };
209
560
  }
561
+ installed.push(...missingPackages);
562
+ }
210
563
 
211
- return { status: 'installed', packages: installed };
564
+ for (const tool of missingLocalTools) {
565
+ console.log(`[${TOOL_NAME}] Installing local companion tool: ${tool.installCommand}`);
566
+ const result = run(NPX_BIN, tool.installArgs, { stdio: 'inherit' });
567
+ if (result.status !== 0) {
568
+ const stderr = (result.stderr || '').trim();
569
+ return {
570
+ status: 'failed',
571
+ reason: stderr || `${tool.name} install failed`,
572
+ };
573
+ }
574
+ installed.push(tool.name);
212
575
  }
213
576
 
214
- return {
215
- maybeSelfUpdateBeforeStatus,
216
- maybeOpenSpecUpdateBeforeStatus,
217
- installGlobalToolchain,
218
- };
577
+ return { status: 'installed', packages: installed };
219
578
  }
220
579
 
221
580
  module.exports = {
222
- createToolchainApi,
581
+ isInteractiveTerminal,
582
+ parseAutoApproval,
583
+ checkForGuardexUpdate,
584
+ printUpdateAvailableBanner,
585
+ readInstalledGuardexVersion,
586
+ readInstalledGuardexInstallInfo,
587
+ restartIntoUpdatedGuardex,
588
+ checkForOpenSpecPackageUpdate,
589
+ printOpenSpecUpdateAvailableBanner,
590
+ promptYesNoStrict,
591
+ resolveGlobalInstallApproval,
592
+ getGlobalToolchainService,
593
+ formatGlobalToolchainServiceName,
594
+ describeMissingGlobalDependencyWarnings,
595
+ describeCompanionInstallCommands,
596
+ detectGlobalToolchainPackages,
597
+ detectRequiredSystemTools,
598
+ detectOptionalLocalCompanionTools,
599
+ askGlobalInstallForMissing,
600
+ maybeSelfUpdateBeforeStatus,
601
+ maybeOpenSpecUpdateBeforeStatus,
602
+ installGlobalToolchain,
223
603
  };