@fission-ai/openspec 1.14.3 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -10
- package/dist/cli/index.js +47 -0
- package/dist/commands/completion.js +4 -2
- package/dist/commands/feedback.js +33 -20
- package/dist/commands/install-standalone.d.ts +27 -0
- package/dist/commands/install-standalone.js +150 -0
- package/dist/commands/schema.js +10 -3
- package/dist/commands/validate.js +33 -3
- package/dist/commands/workflow/instructions.js +15 -4
- package/dist/commands/workflow/new-change.js +12 -4
- package/dist/commands/workflow/status.js +5 -2
- package/dist/commands/workflow/templates.js +5 -2
- package/dist/core/artifact-graph/resolver.d.ts +6 -0
- package/dist/core/artifact-graph/resolver.js +11 -0
- package/dist/core/available-tools.d.ts +2 -1
- package/dist/core/available-tools.js +8 -28
- package/dist/core/completion-tip.d.ts +23 -0
- package/dist/core/completion-tip.js +4 -4
- package/dist/core/completions/command-registry.js +15 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -1
- package/dist/core/completions/installers/zsh-installer.js +64 -19
- package/dist/core/config-schema.d.ts +1 -0
- package/dist/core/config-schema.js +1 -0
- package/dist/core/file-view.d.ts +13 -0
- package/dist/core/file-view.js +14 -0
- package/dist/core/github-copilot/cloud-agent.d.ts +10 -2
- package/dist/core/github-copilot/cloud-agent.js +13 -9
- package/dist/core/global-config.d.ts +4 -0
- package/dist/core/global-config.js +5 -1
- package/dist/core/id.d.ts +8 -0
- package/dist/core/id.js +15 -0
- package/dist/core/legacy-cleanup.d.ts +11 -1
- package/dist/core/legacy-cleanup.js +15 -18
- package/dist/core/migration.d.ts +27 -1
- package/dist/core/migration.js +77 -29
- package/dist/core/parsers/code-fence.js +9 -1
- package/dist/core/parsers/requirement-blocks.d.ts +30 -0
- package/dist/core/parsers/requirement-blocks.js +22 -9
- package/dist/core/parsers/requirement-text.d.ts +14 -0
- package/dist/core/parsers/requirement-text.js +39 -0
- package/dist/core/parsers/spec-structure.js +1 -1
- package/dist/core/profile-sync-drift.d.ts +4 -3
- package/dist/core/profile-sync-drift.js +19 -16
- package/dist/core/project-config.d.ts +10 -0
- package/dist/core/project-config.js +26 -0
- package/dist/core/root-selection.d.ts +6 -0
- package/dist/core/root-selection.js +37 -0
- package/dist/core/shared/tool-detection.d.ts +12 -4
- package/dist/core/shared/tool-detection.js +29 -27
- package/dist/core/shared-skill-target.d.ts +12 -5
- package/dist/core/shared-skill-target.js +34 -23
- package/dist/core/standalone-tip.d.ts +6 -0
- package/dist/core/standalone-tip.js +38 -0
- package/dist/core/store/git.d.ts +22 -1
- package/dist/core/store/git.js +59 -4
- package/dist/core/templates/workflows/verify-change.js +4 -4
- package/dist/core/update-plan.d.ts +127 -0
- package/dist/core/update-plan.js +390 -0
- package/dist/core/update.d.ts +45 -1
- package/dist/core/update.js +279 -137
- package/dist/core/validation/validator.d.ts +27 -2
- package/dist/core/validation/validator.js +58 -4
- package/dist/core/version-check.d.ts +8 -1
- package/dist/core/version-check.js +48 -5
- package/dist/utils/executable-path.d.ts +5 -0
- package/dist/utils/executable-path.js +44 -0
- package/package.json +5 -4
- package/schemas/spec-driven/schema.yaml +1 -0
|
@@ -12,8 +12,10 @@ export declare class Validator {
|
|
|
12
12
|
* Validate delta-formatted spec files under a change directory.
|
|
13
13
|
* Enforces:
|
|
14
14
|
* - At least one delta across all files
|
|
15
|
-
* - ADDED/MODIFIED: each requirement has at least one scenario;
|
|
16
|
-
*
|
|
15
|
+
* - ADDED/MODIFIED: each requirement has at least one scenario; a scenario
|
|
16
|
+
* with no WHEN/GIVEN or THEN step is guidance (WARNING) unless strict mode
|
|
17
|
+
* is enabled; missing English SHALL/MUST keywords are guidance unless
|
|
18
|
+
* strict mode is enabled
|
|
17
19
|
* - REMOVED: names only; no scenario/description required
|
|
18
20
|
* - RENAMED: pairs well-formed
|
|
19
21
|
* - No duplicates within sections; no cross-section conflicts per spec
|
|
@@ -90,6 +92,14 @@ export declare class Validator {
|
|
|
90
92
|
private findScenarioLossIssues;
|
|
91
93
|
private formatInvalidMarkerMessage;
|
|
92
94
|
private convertZodErrors;
|
|
95
|
+
/**
|
|
96
|
+
* Every guidance and error the main-spec path emits about one spec file
|
|
97
|
+
* (#1156, #243): structure, a placeholder or too-brief Purpose, requirement
|
|
98
|
+
* length, a requirement with no scenario, a missing English SHALL/MUST in the
|
|
99
|
+
* body, and a counted scenario with no trigger or outcome. The step check is
|
|
100
|
+
* the same one the delta path runs, on the same `#### ` blocks, so a scenario
|
|
101
|
+
* that asserts nothing is reported wherever it lives.
|
|
102
|
+
*/
|
|
93
103
|
private applySpecRules;
|
|
94
104
|
private applyChangeRules;
|
|
95
105
|
private enrichTopLevelError;
|
|
@@ -124,6 +134,21 @@ export declare class Validator {
|
|
|
124
134
|
* not count it. Empty when the block has no bare scenario header.
|
|
125
135
|
*/
|
|
126
136
|
private emptyScenarioHint;
|
|
137
|
+
/**
|
|
138
|
+
* A counted scenario whose body carries neither a **WHEN** (or **GIVEN**)
|
|
139
|
+
* step nor a **THEN** step can never fail: nothing triggers it and nothing
|
|
140
|
+
* is asserted (#2078). Read from the same blocks the scenario-loss comparison
|
|
141
|
+
* walks, so the two checks cannot disagree about what a scenario is.
|
|
142
|
+
* Only `Scenario:`-labeled blocks are checked: any level-4 child counts as a
|
|
143
|
+
* scenario for loss purposes, but an unlabeled `#### Notes` child is prose
|
|
144
|
+
* on purpose, and a missing WHEN/THEN there is noise, not a finding.
|
|
145
|
+
*
|
|
146
|
+
* A WARNING, like the missing SHALL/MUST keyword: plain `validate` and
|
|
147
|
+
* `archive` keep agreeing, and `--strict` — which already fails on any
|
|
148
|
+
* warning — is the bar that enforces it.
|
|
149
|
+
*/
|
|
150
|
+
private findScenarioStepIssues;
|
|
151
|
+
/** Render a list of section names as prose: `a`, `a and b`, `a, b and c`. */
|
|
127
152
|
private formatSectionList;
|
|
128
153
|
}
|
|
129
154
|
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -5,8 +5,8 @@ import { MarkdownParser } from '../parsers/markdown-parser.js';
|
|
|
5
5
|
import { ChangeParser } from '../parsers/change-parser.js';
|
|
6
6
|
import { findSpecUpdates, buildUpdatedSpec } from '../specs-apply.js';
|
|
7
7
|
import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
|
|
8
|
-
import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection, diffScenarioNames, describeScenarioBalance, describeUnappliedContent, } from '../parsers/requirement-blocks.js';
|
|
9
|
-
import { extractRequirementBody as extractRequirementBodyShared, containsShallOrMust as containsShallOrMustShared, countScenarios as countScenariosShared, countEmptyScenarios, } from '../parsers/requirement-text.js';
|
|
8
|
+
import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection, diffScenarioNames, describeScenarioBalance, parseScenarioBlocks, isScenarioLabeledHeader, describeUnappliedContent, } from '../parsers/requirement-blocks.js';
|
|
9
|
+
import { extractRequirementBody as extractRequirementBodyShared, containsShallOrMust as containsShallOrMustShared, countScenarios as countScenariosShared, countEmptyScenarios, hasScenarioBody, scenarioStepCoverage, } from '../parsers/requirement-text.js';
|
|
10
10
|
import { findMainSpecStructureIssues } from '../parsers/spec-structure.js';
|
|
11
11
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
12
12
|
import { assertDiscoveredSpecFile } from '../../utils/spec-path-containment.js';
|
|
@@ -108,8 +108,10 @@ export class Validator {
|
|
|
108
108
|
* Validate delta-formatted spec files under a change directory.
|
|
109
109
|
* Enforces:
|
|
110
110
|
* - At least one delta across all files
|
|
111
|
-
* - ADDED/MODIFIED: each requirement has at least one scenario;
|
|
112
|
-
*
|
|
111
|
+
* - ADDED/MODIFIED: each requirement has at least one scenario; a scenario
|
|
112
|
+
* with no WHEN/GIVEN or THEN step is guidance (WARNING) unless strict mode
|
|
113
|
+
* is enabled; missing English SHALL/MUST keywords are guidance unless
|
|
114
|
+
* strict mode is enabled
|
|
113
115
|
* - REMOVED: names only; no scenario/description required
|
|
114
116
|
* - RENAMED: pairs well-formed
|
|
115
117
|
* - No duplicates within sections; no cross-section conflicts per spec
|
|
@@ -278,6 +280,7 @@ export class Validator {
|
|
|
278
280
|
if (scenarioCount < 1) {
|
|
279
281
|
issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must include at least one scenario${this.emptyScenarioHint(block.raw)}` });
|
|
280
282
|
}
|
|
283
|
+
issues.push(...this.findScenarioStepIssues(block.raw, `ADDED "${block.name}"`, entryPath));
|
|
281
284
|
}
|
|
282
285
|
// Validate MODIFIED
|
|
283
286
|
for (const block of plan.modified) {
|
|
@@ -310,6 +313,7 @@ export class Validator {
|
|
|
310
313
|
if (scenarioCount < 1) {
|
|
311
314
|
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" must include at least one scenario${this.emptyScenarioHint(block.raw)}` });
|
|
312
315
|
}
|
|
316
|
+
issues.push(...this.findScenarioStepIssues(block.raw, `MODIFIED "${block.name}"`, entryPath));
|
|
313
317
|
}
|
|
314
318
|
// Run archive's scenario-loss check here too, so the change fails at
|
|
315
319
|
// authoring time instead of days later at archive time (#1477).
|
|
@@ -698,6 +702,14 @@ export class Validator {
|
|
|
698
702
|
};
|
|
699
703
|
});
|
|
700
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* Every guidance and error the main-spec path emits about one spec file
|
|
707
|
+
* (#1156, #243): structure, a placeholder or too-brief Purpose, requirement
|
|
708
|
+
* length, a requirement with no scenario, a missing English SHALL/MUST in the
|
|
709
|
+
* body, and a counted scenario with no trigger or outcome. The step check is
|
|
710
|
+
* the same one the delta path runs, on the same `#### ` blocks, so a scenario
|
|
711
|
+
* that asserts nothing is reported wherever it lives.
|
|
712
|
+
*/
|
|
701
713
|
applySpecRules(spec, content) {
|
|
702
714
|
const issues = [];
|
|
703
715
|
for (const structuralIssue of findMainSpecStructureIssues(content)) {
|
|
@@ -767,6 +779,9 @@ export class Validator {
|
|
|
767
779
|
message: this.buildMissingShallOrMustMessage(`Requirement "${block.name}"`, block.name, true),
|
|
768
780
|
});
|
|
769
781
|
}
|
|
782
|
+
// Same step check the delta path runs, on the same blocks, so a scenario
|
|
783
|
+
// that asserts nothing is reported wherever it lives.
|
|
784
|
+
issues.push(...this.findScenarioStepIssues(block.raw, `Requirement "${block.name}"`, `requirements[${index}].scenarios`));
|
|
770
785
|
});
|
|
771
786
|
return issues;
|
|
772
787
|
}
|
|
@@ -941,6 +956,45 @@ export class Validator {
|
|
|
941
956
|
return '';
|
|
942
957
|
return ' (a scenario header with no body under it does not count; add its steps, e.g. "- **WHEN** ..." and "- **THEN** ...")';
|
|
943
958
|
}
|
|
959
|
+
/**
|
|
960
|
+
* A counted scenario whose body carries neither a **WHEN** (or **GIVEN**)
|
|
961
|
+
* step nor a **THEN** step can never fail: nothing triggers it and nothing
|
|
962
|
+
* is asserted (#2078). Read from the same blocks the scenario-loss comparison
|
|
963
|
+
* walks, so the two checks cannot disagree about what a scenario is.
|
|
964
|
+
* Only `Scenario:`-labeled blocks are checked: any level-4 child counts as a
|
|
965
|
+
* scenario for loss purposes, but an unlabeled `#### Notes` child is prose
|
|
966
|
+
* on purpose, and a missing WHEN/THEN there is noise, not a finding.
|
|
967
|
+
*
|
|
968
|
+
* A WARNING, like the missing SHALL/MUST keyword: plain `validate` and
|
|
969
|
+
* `archive` keep agreeing, and `--strict` — which already fails on any
|
|
970
|
+
* warning — is the bar that enforces it.
|
|
971
|
+
*/
|
|
972
|
+
findScenarioStepIssues(blockRaw, label, issuePath) {
|
|
973
|
+
const issues = [];
|
|
974
|
+
for (const scenario of parseScenarioBlocks(blockRaw)) {
|
|
975
|
+
// A header with no body is already an ERROR with its own hint; a step
|
|
976
|
+
// complaint on top of it would say nothing new. parseScenarioBlocks keeps
|
|
977
|
+
// the header line in `raw`, and hasScenarioBody takes the body alone.
|
|
978
|
+
const bodyLines = scenario.raw.split('\n').slice(1).join('\n');
|
|
979
|
+
if (!hasScenarioBody(bodyLines))
|
|
980
|
+
continue;
|
|
981
|
+
if (!isScenarioLabeledHeader(scenario.raw.split('\n')[0]))
|
|
982
|
+
continue;
|
|
983
|
+
const { hasTrigger, hasOutcome } = scenarioStepCoverage(scenario.raw);
|
|
984
|
+
if (hasTrigger && hasOutcome)
|
|
985
|
+
continue;
|
|
986
|
+
const missing = [!hasTrigger && 'a WHEN/GIVEN step', !hasOutcome && 'a THEN step']
|
|
987
|
+
.filter((part) => part !== false)
|
|
988
|
+
.join(' and ');
|
|
989
|
+
issues.push({
|
|
990
|
+
level: 'WARNING',
|
|
991
|
+
path: issuePath,
|
|
992
|
+
message: `${label}: scenario "${scenario.name}" is missing ${missing}. A scenario needs a trigger and an outcome, e.g. "- **WHEN** an invoice is voided" and "- **THEN** a credit note is issued".`,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
return issues;
|
|
996
|
+
}
|
|
997
|
+
/** Render a list of section names as prose: `a`, `a and b`, `a, b and c`. */
|
|
944
998
|
formatSectionList(sections) {
|
|
945
999
|
if (sections.length === 0)
|
|
946
1000
|
return '';
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Called by the standalone binary's entry point. That copy has no package
|
|
3
|
+
* manager behind it, so a package-manager command is the wrong upgrade advice.
|
|
4
|
+
*/
|
|
5
|
+
export declare function markStandaloneBinary(executablePath: string): void;
|
|
6
|
+
/** The standalone binary's own path, or null when running from a package. */
|
|
7
|
+
export declare function getStandaloneBinaryPath(): string | null;
|
|
1
8
|
/**
|
|
2
9
|
* The registry to ask. A configured registry is honored only over TLS: a
|
|
3
10
|
* cleartext hop would aim the request at a link-local or internal address and
|
|
@@ -79,7 +86,7 @@ export declare function isNpmGlobalInstall(installDir: string | null, roots?: st
|
|
|
79
86
|
*/
|
|
80
87
|
export declare function isSourceCheckout(installDir: string | null): boolean;
|
|
81
88
|
export type PackageManager = 'npm' | 'pnpm' | 'bun' | 'yarn' | 'volta';
|
|
82
|
-
export type InstallScope = 'global' | 'project' | 'temporary' | 'source';
|
|
89
|
+
export type InstallScope = 'global' | 'project' | 'temporary' | 'source' | 'standalone';
|
|
83
90
|
export interface CliInstallInfo {
|
|
84
91
|
location: string | null;
|
|
85
92
|
packageManager: PackageManager | null;
|
|
@@ -3,11 +3,13 @@ import http from 'http';
|
|
|
3
3
|
import https from 'https';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { createRequire } from 'module';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
6
7
|
import chalk from 'chalk';
|
|
7
8
|
import { isCiEnvironment } from '../utils/ci.js';
|
|
8
9
|
import { isTelemetryOptedOutByEnv } from '../telemetry/opt-out.js';
|
|
9
10
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
10
11
|
import { getGlobalConfig, isGlobalConfigUnreadable } from './global-config.js';
|
|
12
|
+
import { findCopiesOnPath, samePath } from '../utils/executable-path.js';
|
|
11
13
|
const require = createRequire(import.meta.url);
|
|
12
14
|
const { name: PACKAGE_NAME, version: OPENSPEC_VERSION } = require('../../package.json');
|
|
13
15
|
const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
@@ -22,6 +24,34 @@ const MAX_REDIRECTS = 3;
|
|
|
22
24
|
* one could smuggle ANSI cursor controls and repaint the lines around it.
|
|
23
25
|
*/
|
|
24
26
|
const SAFE_VERSION = /^\d{1,10}\.\d{1,10}\.\d{1,10}(?:-[0-9A-Za-z.-]{1,64})?(?:\+[0-9A-Za-z.-]{1,64})?$/;
|
|
27
|
+
let standaloneBinaryPath = null;
|
|
28
|
+
/**
|
|
29
|
+
* Called by the standalone binary's entry point. That copy has no package
|
|
30
|
+
* manager behind it, so a package-manager command is the wrong upgrade advice.
|
|
31
|
+
*/
|
|
32
|
+
export function markStandaloneBinary(executablePath) {
|
|
33
|
+
standaloneBinaryPath = executablePath;
|
|
34
|
+
}
|
|
35
|
+
/** The standalone binary's own path, or null when running from a package. */
|
|
36
|
+
export function getStandaloneBinaryPath() {
|
|
37
|
+
return standaloneBinaryPath;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `openspec install` reinstalls the standalone binary where it already is,
|
|
41
|
+
* under its own name, so the new copy cannot end up shadowed by the old one.
|
|
42
|
+
* Spelled with the full path when `openspec` on PATH is some other copy.
|
|
43
|
+
* Null when this is not the binary.
|
|
44
|
+
*/
|
|
45
|
+
function standaloneUpgradeCommand() {
|
|
46
|
+
if (!standaloneBinaryPath)
|
|
47
|
+
return null;
|
|
48
|
+
const first = findCopiesOnPath()[0];
|
|
49
|
+
if (first && samePath(first, standaloneBinaryPath))
|
|
50
|
+
return 'openspec install';
|
|
51
|
+
return process.platform === 'win32'
|
|
52
|
+
? `& '${standaloneBinaryPath.replace(/'/g, "''")}' install`
|
|
53
|
+
: `'${standaloneBinaryPath.replace(/'/g, `'\\''`)}' install`;
|
|
54
|
+
}
|
|
25
55
|
/**
|
|
26
56
|
* The check is opt-out and must never get in the way: no network in CI or
|
|
27
57
|
* tests, an explicit escape hatch for anyone offline or air-gapped, and the
|
|
@@ -304,12 +334,14 @@ export async function getAvailableCliUpdate() {
|
|
|
304
334
|
* which copy is actually answering.
|
|
305
335
|
*/
|
|
306
336
|
export function getInstallDir() {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
337
|
+
// The binary is not a package install, so no package-manager logic applies.
|
|
338
|
+
if (standaloneBinaryPath)
|
|
311
339
|
return null;
|
|
312
|
-
|
|
340
|
+
// Resolved at runtime from this module's own (symlink-resolved) location.
|
|
341
|
+
// require.resolve would be resolved by the binary's bundler at build time,
|
|
342
|
+
// baking the build machine's path into it.
|
|
343
|
+
const packageJson = fileURLToPath(new URL('../../package.json', import.meta.url));
|
|
344
|
+
return fs.existsSync(packageJson) ? path.dirname(packageJson) : null;
|
|
313
345
|
}
|
|
314
346
|
/**
|
|
315
347
|
* True when the running CLI resolves from a `node_modules` belonging to the
|
|
@@ -508,6 +540,9 @@ function detectGlobalPackageManager(installDir) {
|
|
|
508
540
|
}
|
|
509
541
|
/** Describes the running copy without guessing when its owner is ambiguous. */
|
|
510
542
|
export function getCliInstallInfo(installDir = getInstallDir(), projectPath = '.') {
|
|
543
|
+
if (standaloneBinaryPath) {
|
|
544
|
+
return { location: path.dirname(standaloneBinaryPath), packageManager: null, scope: 'standalone' };
|
|
545
|
+
}
|
|
511
546
|
if (!installDir) {
|
|
512
547
|
return { location: null, packageManager: null, scope: null };
|
|
513
548
|
}
|
|
@@ -537,6 +572,8 @@ export function getCliInstallInfo(installDir = getInstallDir(), projectPath = '.
|
|
|
537
572
|
}
|
|
538
573
|
/** Returns a safe update command only for an install with known global ownership. */
|
|
539
574
|
export function getCliUpdateCommand(install) {
|
|
575
|
+
if (install.scope === 'standalone')
|
|
576
|
+
return standaloneUpgradeCommand();
|
|
540
577
|
if (install.scope !== 'global' || install.packageManager === null)
|
|
541
578
|
return null;
|
|
542
579
|
return GLOBAL_UPGRADE_COMMANDS[install.packageManager];
|
|
@@ -585,6 +622,12 @@ export function buildCliUpdateLines(latestVersion, installDir, projectPath, opti
|
|
|
585
622
|
*/
|
|
586
623
|
export function buildUpgradeCommandLines(installDir, projectPath) {
|
|
587
624
|
const lines = [];
|
|
625
|
+
const standalone = standaloneUpgradeCommand();
|
|
626
|
+
if (standalone) {
|
|
627
|
+
lines.push(` ${standalone}`);
|
|
628
|
+
lines.push(' Then run "openspec update" again to pick up new workflows.');
|
|
629
|
+
return lines;
|
|
630
|
+
}
|
|
588
631
|
if (isEphemeralRunnerInstall(installDir)) {
|
|
589
632
|
// That command *is* the update, so there is nothing to run afterwards.
|
|
590
633
|
lines.push(` npx ${PACKAGE_NAME}@latest update`);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Every `openspec` on PATH, in the order a shell would find them. */
|
|
2
|
+
export declare function findCopiesOnPath(pathValue?: string, platform?: NodeJS.Platform): string[];
|
|
3
|
+
/** Whether two paths name the same file once symlinks are resolved. */
|
|
4
|
+
export declare function samePath(a: string, b: string): boolean;
|
|
5
|
+
//# sourceMappingURL=executable-path.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/** Every `openspec` on PATH, in the order a shell would find them. */
|
|
4
|
+
export function findCopiesOnPath(pathValue = process.env.PATH ?? '', platform = process.platform) {
|
|
5
|
+
const names = platform === 'win32' ? ['openspec.exe', 'openspec.cmd'] : ['openspec'];
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
const copies = [];
|
|
8
|
+
for (const dir of pathValue.split(path.delimiter)) {
|
|
9
|
+
if (!dir)
|
|
10
|
+
continue;
|
|
11
|
+
for (const name of names) {
|
|
12
|
+
const candidate = path.join(dir, name);
|
|
13
|
+
try {
|
|
14
|
+
if (!fs.statSync(candidate).isFile())
|
|
15
|
+
continue;
|
|
16
|
+
if (platform !== 'win32')
|
|
17
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
18
|
+
const key = platform === 'win32' ? candidate.toLowerCase() : candidate;
|
|
19
|
+
if (!seen.has(key)) {
|
|
20
|
+
seen.add(key);
|
|
21
|
+
copies.push(candidate);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Not there, or not runnable: not a copy a shell would run.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return copies;
|
|
30
|
+
}
|
|
31
|
+
/** Whether two paths name the same file once symlinks are resolved. */
|
|
32
|
+
export function samePath(a, b) {
|
|
33
|
+
const real = (value) => {
|
|
34
|
+
try {
|
|
35
|
+
return fs.realpathSync(value);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return path.resolve(value);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const [x, y] = [real(a), real(b)];
|
|
42
|
+
return process.platform === 'win32' ? x.toLowerCase() === y.toLowerCase() : x === y;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=executable-path.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fission-ai/openspec",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openspec",
|
|
@@ -38,19 +38,20 @@
|
|
|
38
38
|
"!dist/**/*.map"
|
|
39
39
|
],
|
|
40
40
|
"engines": {
|
|
41
|
-
"node": ">=
|
|
41
|
+
"node": ">=22.12.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@changesets/changelog-github": "^1.0.0",
|
|
45
45
|
"@changesets/cli": "^3.0.1",
|
|
46
46
|
"@types/node": "^20.19.43",
|
|
47
47
|
"@types/picomatch": "^4.0.3",
|
|
48
|
-
"@vitest/ui": "^4.1.
|
|
48
|
+
"@vitest/ui": "^4.1.12",
|
|
49
49
|
"eslint": "^10.5.0",
|
|
50
50
|
"smol-toml": "^1.7.1",
|
|
51
51
|
"typescript": "^6.0.3",
|
|
52
52
|
"typescript-eslint": "^8.65.0",
|
|
53
|
-
"
|
|
53
|
+
"vite": "^8.3.5",
|
|
54
|
+
"vitest": "^4.1.12"
|
|
54
55
|
},
|
|
55
56
|
"dependencies": {
|
|
56
57
|
"@inquirer/core": "^12.0.0",
|
|
@@ -105,6 +105,7 @@ artifacts:
|
|
|
105
105
|
- Each scenario: `#### Scenario: <name>` with WHEN/THEN format
|
|
106
106
|
- **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently.
|
|
107
107
|
- Every requirement MUST have at least one scenario.
|
|
108
|
+
- Every scenario MUST have a WHEN/GIVEN step (its trigger) and a THEN step (its outcome). A scenario with only AND lines or plain prose still counts as a scenario, but `openspec validate` flags it as a warning: normal validation still passes, but `openspec validate --strict` fails on it.
|
|
108
109
|
- Keep each requirement's description (the text between `### Requirement:` and its first scenario) to 500 characters or fewer. `openspec validate` flags longer descriptions in ADDED requirements and in the main spec. This is a warning: normal validation still passes, but `openspec validate --strict` fails on it. When writing a new requirement, state one behavior per requirement: move examples and edge cases into scenarios, and split a requirement that covers several behaviors into separate `### Requirement:` blocks, each with its own scenarios. Under MODIFIED, keep the existing requirement block whole; never split, trim or rewrite existing text just to meet the length. Split an existing long requirement only when the user asks for it, in a change made for that purpose: under MODIFIED, keep its header and every scenario and cut its description down to one behavior without changing its meaning, then add each behavior you removed as its own ADDED requirement with its own scenarios.
|
|
109
110
|
|
|
110
111
|
New capabilities only: the delta spec's first section is `## Purpose` -
|