@fission-ai/openspec 1.6.0 → 1.7.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 (175) hide show
  1. package/README.md +28 -1
  2. package/dist/cli/index.js +73 -7
  3. package/dist/commands/change.d.ts +0 -1
  4. package/dist/commands/change.js +90 -50
  5. package/dist/commands/config.js +14 -8
  6. package/dist/commands/doctor.js +9 -4
  7. package/dist/commands/feedback.js +74 -23
  8. package/dist/commands/schema.js +21 -11
  9. package/dist/commands/show.js +5 -2
  10. package/dist/commands/spec.js +20 -24
  11. package/dist/commands/validate.js +18 -3
  12. package/dist/commands/workflow/index.d.ts +1 -1
  13. package/dist/commands/workflow/index.js +1 -1
  14. package/dist/commands/workflow/instructions.d.ts +7 -1
  15. package/dist/commands/workflow/instructions.js +89 -6
  16. package/dist/commands/workflow/shared.d.ts +13 -2
  17. package/dist/commands/workflow/shared.js +34 -4
  18. package/dist/commands/workflow/status.js +7 -2
  19. package/dist/core/archive.d.ts +0 -1
  20. package/dist/core/archive.js +96 -33
  21. package/dist/core/artifact-graph/graph.d.ts +16 -0
  22. package/dist/core/artifact-graph/graph.js +30 -6
  23. package/dist/core/artifact-graph/instruction-loader.d.ts +27 -2
  24. package/dist/core/artifact-graph/instruction-loader.js +55 -7
  25. package/dist/core/artifact-graph/resolver.d.ts +14 -0
  26. package/dist/core/artifact-graph/resolver.js +34 -6
  27. package/dist/core/change-metadata/schema.d.ts +1 -0
  28. package/dist/core/change-metadata/schema.js +6 -0
  29. package/dist/core/change-status-policy.d.ts +1 -1
  30. package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
  31. package/dist/core/command-generation/adapters/amazon-q.js +7 -1
  32. package/dist/core/command-generation/adapters/antigravity.js +2 -1
  33. package/dist/core/command-generation/adapters/auggie.js +2 -1
  34. package/dist/core/command-generation/adapters/bob.d.ts +5 -1
  35. package/dist/core/command-generation/adapters/bob.js +6 -5
  36. package/dist/core/command-generation/adapters/claude.js +1 -8
  37. package/dist/core/command-generation/adapters/codebuddy.js +3 -2
  38. package/dist/core/command-generation/adapters/continue.js +3 -2
  39. package/dist/core/command-generation/adapters/costrict.js +2 -1
  40. package/dist/core/command-generation/adapters/crush.js +5 -5
  41. package/dist/core/command-generation/adapters/cursor.js +2 -2
  42. package/dist/core/command-generation/adapters/devin.d.ts +19 -0
  43. package/dist/core/command-generation/adapters/devin.js +36 -0
  44. package/dist/core/command-generation/adapters/factory.js +2 -1
  45. package/dist/core/command-generation/adapters/gemini.js +37 -2
  46. package/dist/core/command-generation/adapters/github-copilot.js +2 -1
  47. package/dist/core/command-generation/adapters/iflow.js +5 -4
  48. package/dist/core/command-generation/adapters/index.d.ts +2 -2
  49. package/dist/core/command-generation/adapters/index.js +2 -2
  50. package/dist/core/command-generation/adapters/junie.js +2 -1
  51. package/dist/core/command-generation/adapters/kiro.js +2 -1
  52. package/dist/core/command-generation/adapters/lingma.js +5 -5
  53. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
  54. package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
  55. package/dist/core/command-generation/adapters/opencode.js +3 -5
  56. package/dist/core/command-generation/adapters/pi.d.ts +2 -2
  57. package/dist/core/command-generation/adapters/pi.js +3 -6
  58. package/dist/core/command-generation/adapters/qoder.js +5 -5
  59. package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
  60. package/dist/core/command-generation/adapters/qwen.js +12 -7
  61. package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
  62. package/dist/core/command-generation/adapters/roocode.js +4 -4
  63. package/dist/core/command-generation/adapters/trae.js +1 -21
  64. package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
  65. package/dist/core/command-generation/adapters/zcode.js +33 -0
  66. package/dist/core/command-generation/generator.d.ts +8 -0
  67. package/dist/core/command-generation/generator.js +15 -1
  68. package/dist/core/command-generation/index.d.ts +1 -1
  69. package/dist/core/command-generation/index.js +1 -1
  70. package/dist/core/command-generation/invocation.d.ts +73 -0
  71. package/dist/core/command-generation/invocation.js +77 -0
  72. package/dist/core/command-generation/registry.js +4 -4
  73. package/dist/core/command-generation/types.d.ts +9 -2
  74. package/dist/core/command-generation/yaml.d.ts +20 -6
  75. package/dist/core/command-generation/yaml.js +31 -21
  76. package/dist/core/command-surface.d.ts +15 -0
  77. package/dist/core/command-surface.js +33 -0
  78. package/dist/core/completions/command-registry.js +8 -2
  79. package/dist/core/completions/generators/powershell-generator.js +15 -8
  80. package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
  81. package/dist/core/completions/installers/zsh-installer.js +20 -4
  82. package/dist/core/config-prompts.d.ts +1 -1
  83. package/dist/core/config-prompts.js +14 -1
  84. package/dist/core/config-schema.d.ts +7 -1
  85. package/dist/core/config-schema.js +40 -1
  86. package/dist/core/config.d.ts +12 -0
  87. package/dist/core/config.js +21 -3
  88. package/dist/core/global-config.d.ts +5 -0
  89. package/dist/core/init.d.ts +35 -0
  90. package/dist/core/init.js +225 -39
  91. package/dist/core/legacy-cleanup.d.ts +59 -0
  92. package/dist/core/legacy-cleanup.js +249 -22
  93. package/dist/core/list.js +5 -7
  94. package/dist/core/migration.d.ts +71 -1
  95. package/dist/core/migration.js +294 -1
  96. package/dist/core/onboarding-commands.d.ts +30 -0
  97. package/dist/core/onboarding-commands.js +38 -0
  98. package/dist/core/parsers/change-parser.d.ts +17 -2
  99. package/dist/core/parsers/change-parser.js +33 -20
  100. package/dist/core/parsers/code-fence.d.ts +15 -0
  101. package/dist/core/parsers/code-fence.js +49 -0
  102. package/dist/core/parsers/markdown-parser.js +2 -1
  103. package/dist/core/parsers/requirement-blocks.d.ts +8 -0
  104. package/dist/core/parsers/requirement-blocks.js +60 -35
  105. package/dist/core/parsers/requirement-text.d.ts +1 -8
  106. package/dist/core/parsers/requirement-text.js +4 -42
  107. package/dist/core/parsers/spec-structure.js +3 -29
  108. package/dist/core/profile-sync-drift.d.ts +0 -8
  109. package/dist/core/profile-sync-drift.js +9 -45
  110. package/dist/core/project-config.d.ts +25 -6
  111. package/dist/core/project-config.js +77 -7
  112. package/dist/core/references.js +42 -4
  113. package/dist/core/relationship-health.d.ts +8 -0
  114. package/dist/core/relationship-health.js +13 -0
  115. package/dist/core/root-selection.d.ts +9 -6
  116. package/dist/core/root-selection.js +42 -5
  117. package/dist/core/shared/tool-detection.d.ts +25 -5
  118. package/dist/core/shared/tool-detection.js +105 -6
  119. package/dist/core/specs-apply.d.ts +8 -33
  120. package/dist/core/specs-apply.js +252 -157
  121. package/dist/core/store/git.d.ts +13 -0
  122. package/dist/core/store/git.js +24 -0
  123. package/dist/core/templates/workflows/apply-change.js +54 -4
  124. package/dist/core/templates/workflows/archive-change.js +166 -42
  125. package/dist/core/templates/workflows/bulk-archive-change.js +240 -63
  126. package/dist/core/templates/workflows/continue-change.js +32 -40
  127. package/dist/core/templates/workflows/explore.js +12 -0
  128. package/dist/core/templates/workflows/ff-change.js +46 -30
  129. package/dist/core/templates/workflows/new-change.js +2 -2
  130. package/dist/core/templates/workflows/onboard.js +4 -4
  131. package/dist/core/templates/workflows/propose.js +50 -32
  132. package/dist/core/templates/workflows/store-selection.d.ts +1 -1
  133. package/dist/core/templates/workflows/store-selection.js +1 -1
  134. package/dist/core/templates/workflows/sync-specs.js +172 -22
  135. package/dist/core/templates/workflows/update-change.js +20 -12
  136. package/dist/core/templates/workflows/verify-change.js +16 -10
  137. package/dist/core/update.d.ts +39 -5
  138. package/dist/core/update.js +288 -71
  139. package/dist/core/validation/constants.d.ts +4 -1
  140. package/dist/core/validation/constants.js +4 -1
  141. package/dist/core/validation/validator.d.ts +1 -7
  142. package/dist/core/validation/validator.js +92 -36
  143. package/dist/core/version-check.d.ts +152 -0
  144. package/dist/core/version-check.js +736 -0
  145. package/dist/core/view.js +12 -17
  146. package/dist/prompts/searchable-multi-select.js +1 -1
  147. package/dist/telemetry/index.d.ts +1 -1
  148. package/dist/telemetry/index.js +55 -33
  149. package/dist/ui/welcome-screen.d.ts +9 -1
  150. package/dist/ui/welcome-screen.js +91 -38
  151. package/dist/utils/change-metadata.d.ts +30 -0
  152. package/dist/utils/change-metadata.js +101 -7
  153. package/dist/utils/change-utils.d.ts +10 -4
  154. package/dist/utils/change-utils.js +20 -13
  155. package/dist/utils/command-references.d.ts +80 -6
  156. package/dist/utils/command-references.js +138 -7
  157. package/dist/utils/date.d.ts +8 -0
  158. package/dist/utils/date.js +13 -0
  159. package/dist/utils/index.d.ts +1 -1
  160. package/dist/utils/index.js +1 -1
  161. package/dist/utils/item-discovery.d.ts +19 -0
  162. package/dist/utils/item-discovery.js +30 -48
  163. package/dist/utils/shell-detection.d.ts +1 -1
  164. package/dist/utils/shell-detection.js +64 -11
  165. package/dist/utils/spec-discovery.d.ts +40 -0
  166. package/dist/utils/spec-discovery.js +102 -0
  167. package/package.json +14 -12
  168. package/schemas/spec-driven/schema.yaml +60 -5
  169. package/schemas/spec-driven/templates/design.md +2 -2
  170. package/schemas/spec-driven/templates/proposal.md +5 -1
  171. package/schemas/spec-driven/templates/spec.md +3 -0
  172. package/dist/core/command-generation/adapters/codex.d.ts +0 -16
  173. package/dist/core/command-generation/adapters/codex.js +0 -39
  174. package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
  175. package/dist/core/command-generation/adapters/windsurf.js +0 -38
@@ -18,13 +18,7 @@ export declare class Validator {
18
18
  * - No duplicates within sections; no cross-section conflicts per spec
19
19
  */
20
20
  validateChangeDeltaSpecs(changeDir: string): Promise<ValidationReport>;
21
- /**
22
- * Recursively collect every delta `spec.md` under a change's specs directory,
23
- * so both the one-level (specs/<capability>/spec.md) and nested multi-area
24
- * (specs/<area>/<capability>/spec.md) layouts are discovered (#1182b).
25
- * Returns absolute paths, sorted for deterministic issue ordering.
26
- */
27
- private findDeltaSpecFiles;
21
+ private formatInvalidMarkerMessage;
28
22
  private convertZodErrors;
29
23
  private applySpecRules;
30
24
  private applyChangeRules;
@@ -4,10 +4,12 @@ import { SpecSchema, ChangeSchema } from '../schemas/index.js';
4
4
  import { MarkdownParser } from '../parsers/markdown-parser.js';
5
5
  import { ChangeParser } from '../parsers/change-parser.js';
6
6
  import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
7
- import { parseDeltaSpec, normalizeRequirementName, extractRequirementsSection } from '../parsers/requirement-blocks.js';
7
+ import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection } from '../parsers/requirement-blocks.js';
8
8
  import { extractRequirementBody as extractRequirementBodyShared, containsShallOrMust as containsShallOrMustShared, countScenarios as countScenariosShared, } from '../parsers/requirement-text.js';
9
9
  import { findMainSpecStructureIssues } from '../parsers/spec-structure.js';
10
10
  import { FileSystemUtils } from '../../utils/file-system.js';
11
+ import { discoverSpecFiles, hasAnyFileUnder } from '../../utils/spec-discovery.js';
12
+ import { METADATA_FILENAME, readSkipSpecsMarker } from '../../utils/change-metadata.js';
11
13
  export class Validator {
12
14
  strictMode;
13
15
  constructor(strictMode = false) {
@@ -67,8 +69,20 @@ export class Validator {
67
69
  const parser = new ChangeParser(content, changeDir);
68
70
  const change = await parser.parseChangeWithDeltas(changeName);
69
71
  const result = ChangeSchema.safeParse(change);
72
+ const marker = readSkipSpecsMarker(changeDir);
73
+ if (marker.invalidReason) {
74
+ issues.push({ level: 'ERROR', path: METADATA_FILENAME, message: this.formatInvalidMarkerMessage(marker.invalidReason) });
75
+ }
70
76
  if (!result.success) {
71
- issues.push(...this.convertZodErrors(result.error));
77
+ let zodIssues = this.convertZodErrors(result.error);
78
+ // Only the no-deltas error is marker-aware here: the marker+files
79
+ // conflict is validateChangeDeltaSpecs's job, and every caller of
80
+ // this proposal-level pass (archive's non-blocking warnings) pairs
81
+ // it with that gate.
82
+ if (marker.declared) {
83
+ zodIssues = zodIssues.filter(issue => !issue.message.startsWith(VALIDATION_MESSAGES.CHANGE_NO_DELTAS));
84
+ }
85
+ issues.push(...zodIssues);
72
86
  }
73
87
  issues.push(...this.applyChangeRules(change, content));
74
88
  }
@@ -96,14 +110,30 @@ export class Validator {
96
110
  const issues = [];
97
111
  const specsDir = path.join(changeDir, 'specs');
98
112
  let totalDeltas = 0;
113
+ let hasRootLevelSpec = false;
99
114
  const missingHeaderSpecs = [];
100
115
  const emptySectionSpecs = [];
101
116
  try {
102
- // Discover delta specs at any depth so the nested multi-area layout
103
- // (specs/<area>/<capability>/spec.md) is validated, not just the
104
- // one-level specs/<capability>/spec.md layout (#1182b). The spec-driven
105
- // specs glob is specs/**/*.md; delta files are always named spec.md.
106
- const specFiles = await this.findDeltaSpecFiles(specsDir);
117
+ // Discover delta specs through the same helper the change parser, show,
118
+ // apply, and archive use, so validate never accepts a layout the merge
119
+ // path silently skips (#1385). It finds spec.md at any depth, covering
120
+ // both specs/<capability>/spec.md and the nested multi-area
121
+ // specs/<area>/<capability>/spec.md layout (#1182b).
122
+ const specFiles = (await discoverSpecFiles(specsDir)).map(spec => spec.specFile);
123
+ // A spec.md directly at the specs/ root has no capability folder, so the
124
+ // merge path drops it: without this error the change validates clean and
125
+ // archives while its requirements never reach openspec/specs/ (#1385).
126
+ // Only a regular file counts — a *directory* named spec.md is a capability
127
+ // folder like any other, and discoverSpecFiles reads it normally.
128
+ const rootSpecStat = await fs.stat(path.join(specsDir, 'spec.md')).catch(() => null);
129
+ hasRootLevelSpec = rootSpecStat?.isFile() === true;
130
+ if (hasRootLevelSpec) {
131
+ issues.push({
132
+ level: 'ERROR',
133
+ path: 'spec.md',
134
+ message: 'Delta spec found at specs/spec.md. Delta specs must live in a capability folder (e.g. specs/<capability>/spec.md) — a file at the specs/ root is ignored when the change is applied or archived.',
135
+ });
136
+ }
107
137
  for (const specFile of specFiles) {
108
138
  let content;
109
139
  try {
@@ -261,11 +291,30 @@ export class Validator {
261
291
  if (addedNames.has(toKey)) {
262
292
  issues.push({ level: 'ERROR', path: entryPath, message: `RENAMED TO collides with ADDED for "${to}"` });
263
293
  }
294
+ // Folded comparison: a case/whitespace variant of the FROM header
295
+ // in REMOVED is the same contradiction, not a different name.
296
+ const removedFoldMatch = [...removedNames].find((r) => foldRequirementName(r) === foldRequirementName(fromKey));
297
+ if (removedFoldMatch !== undefined) {
298
+ issues.push({
299
+ level: 'ERROR',
300
+ path: entryPath,
301
+ message: `Requirement present in both RENAMED and REMOVED: "${from}"` +
302
+ (removedFoldMatch === fromKey ? '' : ` (REMOVED spells it "${removedFoldMatch}")`),
303
+ });
304
+ }
264
305
  }
265
306
  }
266
307
  }
267
- catch {
268
- // If no specs dir, treat as no deltas
308
+ catch (error) {
309
+ // A missing specs dir (or a stray `specs` file) means no deltas;
310
+ // anything else (EACCES, EIO) must stay loud — discoverSpecFiles
311
+ // documents that silently dropping an unreadable capability recreates
312
+ // the data-loss class it prevents, and archive lets the same error
313
+ // propagate.
314
+ const code = error?.code;
315
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') {
316
+ throw error;
317
+ }
269
318
  }
270
319
  for (const { path: specPath, sections } of emptySectionSpecs) {
271
320
  issues.push({
@@ -281,39 +330,46 @@ export class Validator {
281
330
  message: 'No delta sections found. Add headers such as "## ADDED Requirements" or move non-delta notes outside specs/.',
282
331
  });
283
332
  }
284
- if (totalDeltas === 0) {
285
- issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
333
+ const marker = readSkipSpecsMarker(changeDir);
334
+ if (marker.invalidReason) {
335
+ issues.push({ level: 'ERROR', path: METADATA_FILENAME, message: this.formatInvalidMarkerMessage(marker.invalidReason) });
286
336
  }
287
- return this.createReport(issues);
288
- }
289
- /**
290
- * Recursively collect every delta `spec.md` under a change's specs directory,
291
- * so both the one-level (specs/<capability>/spec.md) and nested multi-area
292
- * (specs/<area>/<capability>/spec.md) layouts are discovered (#1182b).
293
- * Returns absolute paths, sorted for deterministic issue ordering.
294
- */
295
- async findDeltaSpecFiles(specsDir) {
296
- const results = [];
297
- const walk = async (dir) => {
298
- let entries;
337
+ // ANY file under specs/ contradicts the marker - not just parsed deltas.
338
+ // Headerless or stray files would be silently dropped at archive time (and
339
+ // some still satisfy the artifact graph's specs/** glob) while the change
340
+ // claims to have nothing, so they must surface as an explicit conflict.
341
+ // Probed only when the marker is declared, and unreadable specs/ (a stray
342
+ // `specs` file, permission errors) fails closed as a conflict: the marker
343
+ // claims nothing is there, and validate must not crash where the
344
+ // historical path degraded to "no deltas".
345
+ const skipSpecs = marker.declared;
346
+ let specsDirHasFiles = false;
347
+ if (skipSpecs) {
299
348
  try {
300
- entries = await fs.readdir(dir, { withFileTypes: true });
349
+ specsDirHasFiles = await hasAnyFileUnder(specsDir);
301
350
  }
302
351
  catch {
303
- return;
352
+ specsDirHasFiles = true;
304
353
  }
305
- for (const entry of entries) {
306
- const full = path.join(dir, entry.name);
307
- if (entry.isDirectory()) {
308
- await walk(full);
309
- }
310
- else if (entry.isFile() && entry.name === 'spec.md') {
311
- results.push(full);
312
- }
354
+ }
355
+ if (skipSpecs && specsDirHasFiles) {
356
+ issues.push({ level: 'ERROR', path: 'file', message: VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_CONFLICT });
357
+ }
358
+ // The root-level error already names the file and the fix; adding "No
359
+ // deltas found" on top would contradict it, since the deltas are sitting in
360
+ // the file just reported.
361
+ if (totalDeltas === 0 && !hasRootLevelSpec) {
362
+ if (skipSpecs && !specsDirHasFiles) {
363
+ issues.push({ level: 'INFO', path: 'file', message: VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_ACCEPTED });
313
364
  }
314
- };
315
- await walk(specsDir);
316
- return results.sort();
365
+ else if (!skipSpecs) {
366
+ issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
367
+ }
368
+ }
369
+ return this.createReport(issues);
370
+ }
371
+ formatInvalidMarkerMessage(invalidReason) {
372
+ return `${VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_INVALID_METADATA} (${invalidReason})`;
317
373
  }
318
374
  convertZodErrors(error) {
319
375
  return error.issues.map(err => {
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The registry to ask: only the environment variable npm exports (under
3
+ * `npm run`, or an explicit export). Deliberately not a `registry=` line from
4
+ * any .npmrc — letting file contents choose the destination of an outbound
5
+ * request is a flow worth avoiding for a convenience this small, and a project
6
+ * file would travel with a cloned repository. Anyone on a private mirror can
7
+ * export `npm_config_registry`, or turn the check off entirely.
8
+ */
9
+ export declare function registryUrl(): string;
10
+ /**
11
+ * Compares two semver-ish versions. Returns 1 when a > b, -1 when a < b, 0
12
+ * otherwise. Prereleases sort below their release (1.7.0-beta.1 < 1.7.0).
13
+ */
14
+ export declare function compareVersions(a: string, b: string): number;
15
+ /**
16
+ * Returns the published version when the installed CLI is behind it, otherwise
17
+ * null. Never throws and never blocks for longer than the request timeout.
18
+ */
19
+ export declare function getAvailableCliUpdate(): Promise<string | null>;
20
+ /**
21
+ * Directory the running CLI was loaded from, or null when it cannot be
22
+ * resolved. Shown in the upgrade hint so anyone who upgraded but still runs an
23
+ * old binary — a stale pnpm/volta/npx shim, or two installs on PATH — can see
24
+ * which copy is actually answering.
25
+ */
26
+ export declare function getInstallDir(): string | null;
27
+ /**
28
+ * True when the running CLI resolves from a `node_modules` belonging to the
29
+ * project being updated or any ancestor of it — the hoisted-root layout npm and
30
+ * pnpm workspaces produce. Anchored on the target path rather than the working
31
+ * directory, since `openspec update <path>` and running from a sub-package are
32
+ * both normal. Never throws: process.cwd() fails when the directory has been
33
+ * deleted, and a wrong upgrade hint must not take down a successful update.
34
+ */
35
+ export declare function isProjectLocalInstall(installDir: string | null, projectPath?: string): boolean;
36
+ /**
37
+ * True for the throwaway caches npx/pnpm dlx/bunx unpack into. Telling those
38
+ * users to install globally would create the second copy on PATH they were
39
+ * deliberately avoiding.
40
+ */
41
+ export declare function isEphemeralRunnerInstall(installDir: string | null): boolean;
42
+ /**
43
+ * Directories npm installs global packages into. Derived from the running node
44
+ * rather than by shelling out to `npm prefix -g`, which would cost more than
45
+ * the version check itself. Only a hint: `process.execPath` is realpath'd, so
46
+ * on Homebrew it lands in the Cellar rather than the brew prefix — which is
47
+ * why the install's own layout is the primary signal below.
48
+ */
49
+ export declare function npmGlobalRoots(): string[];
50
+ /**
51
+ * The prefix of an npm global install, read from the install's own shape:
52
+ * `<prefix>/lib/node_modules/<pkg>` on POSIX, `<prefix>/node_modules/<pkg>` on
53
+ * Windows. Self-describing, so it holds for Homebrew, nvm, Debian and anywhere
54
+ * else npm's prefix is not derivable from the node binary. Null when the
55
+ * layout does not match.
56
+ */
57
+ export declare function npmPrefixFromInstallDir(installDir: string | null): string | null;
58
+ /**
59
+ * True only when npm itself owns this copy. Everything else — a pnpm, bun,
60
+ * yarn or volta global — would be made worse by `npm install -g`, which adds a
61
+ * second copy that may not even be the one on PATH.
62
+ */
63
+ export declare function isNpmGlobalInstall(installDir: string | null, roots?: string[]): boolean;
64
+ /**
65
+ * True when the CLI is running from a clone rather than an install. Upgrade
66
+ * advice is meaningless there: the version is whatever the branch says.
67
+ */
68
+ export declare function isSourceCheckout(installDir: string | null): boolean;
69
+ export type PackageManager = 'npm' | 'pnpm' | 'bun' | 'yarn' | 'volta';
70
+ /**
71
+ * The package manager that owns this copy, so the printed command is one the
72
+ * user's setup will actually honor.
73
+ */
74
+ export declare function detectPackageManager(installDir: string | null): PackageManager;
75
+ /**
76
+ * Builds the hint, with the upgrade command chosen for how this copy of the CLI
77
+ * was installed. Pure so every branch is assertable.
78
+ */
79
+ export declare function buildCliUpdateLines(latestVersion: string, installDir: string | null, projectPath: string, options?: {
80
+ withCommand?: boolean;
81
+ }): string[];
82
+ /**
83
+ * The upgrade command for however this copy was installed, plus the reminder
84
+ * that instruction files come from the CLI and so need a second pass.
85
+ */
86
+ export declare function buildUpgradeCommandLines(installDir: string | null, projectPath: string): string[];
87
+ /**
88
+ * Whether we can run the upgrade for the user instead of only printing it.
89
+ *
90
+ * Only an npm-owned global install qualifies, because `npm install -g` is the
91
+ * only command we run: a pnpm/bun/yarn/volta global would get a second copy
92
+ * that may not be the one on PATH, a project dependency belongs to that
93
+ * project's package manager, an npx/dlx cache has nothing to upgrade, and a
94
+ * source checkout is not an install at all.
95
+ */
96
+ export declare function canSelfUpgrade(installDir: string | null, projectPath: string): boolean;
97
+ /**
98
+ * Whether to offer the upgrade rather than just print the command. Kept here,
99
+ * as a pure function of the environment, because the interesting mistakes live
100
+ * in this decision: offering where `npm install -g` cannot help, or asking a
101
+ * question no one can answer.
102
+ */
103
+ export declare function shouldOfferUpgrade(params: {
104
+ installDir: string | null;
105
+ projectPath: string;
106
+ interactive: boolean;
107
+ stdoutIsTty: boolean;
108
+ }): boolean;
109
+ /**
110
+ * The `openspec` npm installs alongside its global package, so the upgrade can
111
+ * be handed to the copy npm just wrote rather than to whatever PATH resolves.
112
+ * Null when it cannot be found, in which case PATH is the only option left.
113
+ */
114
+ export declare function upgradedBinPath(roots?: string[], installDir?: string | null): string | null;
115
+ /**
116
+ * Asks a CLI binary its version. Used to confirm an upgrade actually landed:
117
+ * `npm install -g` exits 0 even when it installed nothing, so its exit code
118
+ * alone cannot justify telling the user they are on a new version.
119
+ */
120
+ export declare function readCliVersion(binPath: string): Promise<string | null>;
121
+ export type UpgradeOutcome = 'upgraded' | 'declined' | 'failed' | 'cancelled' | 'not-on-path';
122
+ /**
123
+ * Offers to run the upgrade and reports what actually happened. The version is
124
+ * read back from the installed binary rather than assumed, so "upgraded" is a
125
+ * fact and a PATH that still answers with the old copy is caught here instead
126
+ * of silently doing nothing.
127
+ */
128
+ export declare function offerCliUpgrade(latestVersion: string): Promise<UpgradeOutcome>;
129
+ /**
130
+ * Runs `openspec update` again with the CLI that was just installed — this
131
+ * process is still the old code, so it cannot write the new workflows itself.
132
+ * Resolves the exit code to pass along; when no `openspec` is on PATH the
133
+ * upgrade still landed but nothing was regenerated, so it says so and
134
+ * resolves 0 rather than reporting a failure the upgrade did not have.
135
+ */
136
+ export declare function rerunUpdateWithUpgradedCli(projectPath: string, options?: {
137
+ force?: boolean;
138
+ binPath?: string;
139
+ }): Promise<number>;
140
+ /**
141
+ * Prints the upgrade hint. Instruction files are generated by the installed
142
+ * CLI, so "up to date" only ever means "matches this CLI" — without this note
143
+ * a stale install looks like a successful update.
144
+ */
145
+ export declare function displayCliUpdateNote(latestVersion: string, projectPath?: string, options?: {
146
+ withCommand?: boolean;
147
+ }): void;
148
+ /**
149
+ * Prints just the manual command, for when the offer was declined or failed.
150
+ */
151
+ export declare function displayUpgradeCommand(projectPath?: string): void;
152
+ //# sourceMappingURL=version-check.d.ts.map