@fission-ai/openspec 1.9.0 → 1.11.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/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.js +60 -9
- package/dist/commands/change.d.ts +24 -0
- package/dist/commands/change.js +223 -0
- package/dist/commands/config.js +14 -3
- package/dist/commands/feedback.js +28 -7
- package/dist/commands/schema.d.ts +5 -0
- package/dist/commands/schema.js +201 -35
- package/dist/commands/show.js +1 -1
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/status.d.ts +2 -0
- package/dist/commands/workflow/status.js +76 -12
- package/dist/core/archive.js +64 -7
- package/dist/core/artifact-graph/instruction-loader.js +2 -7
- package/dist/core/artifact-graph/outputs.d.ts +4 -0
- package/dist/core/artifact-graph/outputs.js +7 -0
- package/dist/core/artifact-graph/resolver.js +15 -9
- package/dist/core/available-tools.js +16 -1
- package/dist/core/command-generation/adapters/antigravity.d.ts +1 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -2
- package/dist/core/command-generation/adapters/opencode.d.ts +2 -1
- package/dist/core/command-generation/adapters/opencode.js +13 -2
- package/dist/core/completion-tip.d.ts +15 -0
- package/dist/core/completion-tip.js +133 -0
- package/dist/core/completions/command-registry.js +20 -0
- package/dist/core/completions/factory.d.ts +11 -0
- package/dist/core/completions/generators/fish-generator.d.ts +12 -0
- package/dist/core/completions/generators/fish-generator.js +79 -47
- package/dist/core/completions/installers/bash-installer.d.ts +7 -0
- package/dist/core/completions/installers/bash-installer.js +15 -0
- package/dist/core/completions/installers/fish-installer.d.ts +7 -0
- package/dist/core/completions/installers/fish-installer.js +15 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +7 -0
- package/dist/core/completions/installers/powershell-installer.js +15 -0
- package/dist/core/completions/templates/fish-templates.d.ts +1 -1
- package/dist/core/completions/templates/fish-templates.js +107 -10
- package/dist/core/completions/types.d.ts +4 -0
- package/dist/core/config-prompts.js +19 -10
- package/dist/core/config-schema.d.ts +1 -0
- package/dist/core/config-schema.js +2 -0
- package/dist/core/config.js +7 -1
- package/dist/core/global-config.d.ts +2 -0
- package/dist/core/init.d.ts +5 -0
- package/dist/core/init.js +117 -14
- package/dist/core/legacy-cleanup.js +6 -0
- package/dist/core/migration.js +42 -12
- package/dist/core/parsers/requirement-blocks.d.ts +1 -0
- package/dist/core/parsers/requirement-blocks.js +2 -0
- package/dist/core/profiles.d.ts +1 -1
- package/dist/core/profiles.js +11 -2
- package/dist/core/shared/tool-detection.js +9 -3
- package/dist/core/shared-skill-target.d.ts +13 -0
- package/dist/core/shared-skill-target.js +43 -1
- package/dist/core/specs-apply.js +16 -8
- package/dist/core/templates/workflows/explore.js +79 -73
- package/dist/core/templates/workflows/feedback.js +1 -0
- package/dist/core/templates/workflows/onboard.js +4 -4
- package/dist/core/update.js +39 -26
- package/dist/core/validation/constants.d.ts +3 -0
- package/dist/core/validation/constants.js +12 -0
- package/dist/core/validation/purpose-placeholder.d.ts +45 -0
- package/dist/core/validation/purpose-placeholder.js +117 -0
- package/dist/core/validation/validator.js +16 -1
- package/dist/telemetry/index.js +3 -2
- package/dist/utils/change-metadata.js +17 -13
- package/dist/utils/change-utils.js +5 -0
- package/dist/utils/requirement-diff.d.ts +42 -0
- package/dist/utils/requirement-diff.js +61 -0
- package/package.json +4 -6
- package/schemas/spec-driven/schema.yaml +17 -7
- package/scripts/postinstall.js +0 -83
package/dist/core/update.js
CHANGED
|
@@ -23,7 +23,7 @@ import { getAvailableTools } from './available-tools.js';
|
|
|
23
23
|
import { WORKFLOW_TO_SKILL_DIR, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
|
|
24
24
|
import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, findLegacyToolMigrations, migrateLegacyToolDirs, describeLegacyMigration, legacyMigrationNotice, keptInPlaceNotice, hasMovableContent, } from './migration.js';
|
|
25
25
|
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
26
|
-
import { writeSharedSkillTarget,
|
|
26
|
+
import { resolveSharedSkillWriters, writeSharedSkillTarget, } from './shared-skill-target.js';
|
|
27
27
|
import { includesGitHubCopilot, writeCopilotCloudFiles, removeCopilotCloudFiles, isCopilotCloudEnabled, readCopilotCloudOptIn, findUnmanagedCloudFiles } from './github-copilot/cloud-agent.js';
|
|
28
28
|
const require = createRequire(import.meta.url);
|
|
29
29
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
@@ -135,7 +135,7 @@ export class UpdateCommand {
|
|
|
135
135
|
// Still check for new tool directories and extra workflows
|
|
136
136
|
this.detectNewTools(resolvedProjectPath, configuredTools);
|
|
137
137
|
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredTools, desiredWorkflows);
|
|
138
|
-
this.displayMissingCoreWorkflowsNote(profile,
|
|
138
|
+
this.displayMissingCoreWorkflowsNote(profile, desiredWorkflows);
|
|
139
139
|
this.displaySetupNotes(configuredTools);
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
@@ -154,7 +154,11 @@ export class UpdateCommand {
|
|
|
154
154
|
const deliveryIncludesCommands = delivery !== 'skills';
|
|
155
155
|
// 10. Update tools (all if force, otherwise only those needing update)
|
|
156
156
|
const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet];
|
|
157
|
+
const sharedSkillWriters = resolveSharedSkillWriters(resolvedProjectPath, configuredAndNewTools
|
|
158
|
+
.map((toolId) => AI_TOOLS.find((tool) => tool.value === toolId))
|
|
159
|
+
.filter((tool) => tool !== undefined));
|
|
157
160
|
const updatedTools = [];
|
|
161
|
+
const updatedToolIds = [];
|
|
158
162
|
const failedTools = [];
|
|
159
163
|
const skillsInvocableCommandSkips = [];
|
|
160
164
|
const zeroArtifactTools = [];
|
|
@@ -172,11 +176,12 @@ export class UpdateCommand {
|
|
|
172
176
|
const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : resolvedProjectPath;
|
|
173
177
|
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
174
178
|
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
179
|
+
const writesSkills = !tool.skillsDir || sharedSkillWriters.has(tool.value);
|
|
175
180
|
const toolWorkflows = legacyWorkflowOverrides[tool.value] ?? desiredWorkflows;
|
|
176
181
|
const skillTemplates = getSkillTemplates(toolWorkflows);
|
|
177
182
|
const commandContents = getCommandContents(toolWorkflows);
|
|
178
183
|
// Generate skill files if delivery includes skills
|
|
179
|
-
if (shouldGenerateSkills) {
|
|
184
|
+
if (shouldGenerateSkills && writesSkills) {
|
|
180
185
|
for (const { template, dirName } of skillTemplates) {
|
|
181
186
|
const skillDir = path.join(skillsDir, dirName);
|
|
182
187
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
@@ -189,7 +194,9 @@ export class UpdateCommand {
|
|
|
189
194
|
removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsRoot, skillsDir, toolWorkflows);
|
|
190
195
|
}
|
|
191
196
|
// Delete skill directories if delivery is commands-only
|
|
192
|
-
if (shouldRemoveSkillsForTool(tool.value, delivery) &&
|
|
197
|
+
if (shouldRemoveSkillsForTool(tool.value, delivery) &&
|
|
198
|
+
writesSkills &&
|
|
199
|
+
!hasGlobalSkillTarget(tool)) {
|
|
193
200
|
removedSkillCount += await this.removeSkillDirs(skillsRoot, skillsDir);
|
|
194
201
|
// Persist the selected owner even when commands-only delivery leaves
|
|
195
202
|
// this target with no generated skills.
|
|
@@ -223,6 +230,7 @@ export class UpdateCommand {
|
|
|
223
230
|
}
|
|
224
231
|
spinner.succeed(`Updated ${tool.name}`);
|
|
225
232
|
updatedTools.push(tool.name);
|
|
233
|
+
updatedToolIds.push(tool.value);
|
|
226
234
|
for (const migration of migrateLegacyToolDirs(resolvedProjectPath, [tool.value], 'after-generation')) {
|
|
227
235
|
if (hasMovableContent(migration)) {
|
|
228
236
|
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
@@ -310,7 +318,7 @@ export class UpdateCommand {
|
|
|
310
318
|
this.detectNewTools(resolvedProjectPath, configuredAndNewTools);
|
|
311
319
|
// 14. Display note about extra workflows not in profile
|
|
312
320
|
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredAndNewTools, desiredWorkflows);
|
|
313
|
-
this.displayMissingCoreWorkflowsNote(profile,
|
|
321
|
+
this.displayMissingCoreWorkflowsNote(profile, desiredWorkflows);
|
|
314
322
|
this.displaySetupNotes(configuredAndNewTools);
|
|
315
323
|
// 15. List affected tools
|
|
316
324
|
if (updatedTools.length > 0) {
|
|
@@ -318,7 +326,16 @@ export class UpdateCommand {
|
|
|
318
326
|
console.log(chalk.dim(`Tools: ${toolDisplayNames.join(', ')}`));
|
|
319
327
|
}
|
|
320
328
|
console.log();
|
|
321
|
-
|
|
329
|
+
const affectedToolIds = [...new Set([...newlyConfiguredTools, ...updatedToolIds])];
|
|
330
|
+
const shouldRestartIde = affectedToolIds.some((toolId) => {
|
|
331
|
+
const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
|
|
332
|
+
return Boolean(tool?.requiresIdeRestart &&
|
|
333
|
+
(shouldGenerateCommandsForTool(toolId, delivery) ||
|
|
334
|
+
shouldGenerateSkillsForTool(toolId, delivery)));
|
|
335
|
+
});
|
|
336
|
+
if (shouldRestartIde) {
|
|
337
|
+
console.log(chalk.dim('Restart your IDE for changes to take effect.'));
|
|
338
|
+
}
|
|
322
339
|
if (failedTools.length > 0) {
|
|
323
340
|
throw new Error(`OpenSpec update failed for: ${failedTools.map((tool) => tool.name).join(', ')}`);
|
|
324
341
|
}
|
|
@@ -787,11 +804,15 @@ export class UpdateCommand {
|
|
|
787
804
|
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
788
805
|
}
|
|
789
806
|
}
|
|
790
|
-
const inferredCodexWorkflows = getLegacyWorkflowIdsForTool(detection, 'codex');
|
|
807
|
+
const inferredCodexWorkflows = getProfileWorkflows('custom', getLegacyWorkflowIdsForTool(detection, 'codex')).filter((workflow) => ALL_WORKFLOWS.includes(workflow));
|
|
791
808
|
// Create skills/commands for selected tools using effective profile+delivery.
|
|
792
809
|
const newlyConfigured = [];
|
|
793
810
|
const skippedSharedSkillTools = [];
|
|
794
811
|
const workflowOverrides = {};
|
|
812
|
+
const arbitrationTools = [...new Set([...configuredTools, ...selectedTools])]
|
|
813
|
+
.map((toolId) => AI_TOOLS.find((tool) => tool.value === toolId))
|
|
814
|
+
.filter((tool) => tool !== undefined);
|
|
815
|
+
const sharedSkillWriters = resolveSharedSkillWriters(projectPath, arbitrationTools);
|
|
795
816
|
for (const toolId of selectedTools) {
|
|
796
817
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
797
818
|
if (!tool || !toolSupportsSkills(tool))
|
|
@@ -802,6 +823,7 @@ export class UpdateCommand {
|
|
|
802
823
|
const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : projectPath;
|
|
803
824
|
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
804
825
|
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
826
|
+
const writesSkills = !tool.skillsDir || sharedSkillWriters.has(tool.value);
|
|
805
827
|
const toolWorkflows = (tool.value === 'codex' && inferredCodexWorkflows.length > 0
|
|
806
828
|
? inferredCodexWorkflows
|
|
807
829
|
: desiredWorkflows);
|
|
@@ -810,32 +832,23 @@ export class UpdateCommand {
|
|
|
810
832
|
}
|
|
811
833
|
const skillTemplates = getSkillTemplates(toolWorkflows);
|
|
812
834
|
const commandContents = getCommandContents(toolWorkflows);
|
|
813
|
-
//
|
|
814
|
-
//
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
// Skipping here means the tool is never recorded as configured, so a
|
|
822
|
-
// persistent legacy signal re-offers it on later runs. Because no
|
|
823
|
-
// replacement is written, this tool is also exempted from immediate
|
|
824
|
-
// legacy cleanup (see skippedSharedSkillTools) — otherwise a repo-local
|
|
825
|
-
// `.codex/prompts` would be deleted with nothing put in its place. That
|
|
826
|
-
// repeat is idempotent and harmless — the alternative is the silent
|
|
827
|
-
// hijack this prevents.
|
|
828
|
-
const sharedOwner = shouldGenerateSkills
|
|
829
|
-
? sharedSkillRootOwner(projectPath, tool.value)
|
|
835
|
+
// Never overwrite a shared skills root owned by another tool. A tool
|
|
836
|
+
// with its own command surface can still install those commands: this
|
|
837
|
+
// is how a legacy Antigravity install gains `.agents/workflows` beside
|
|
838
|
+
// Codex-owned `.agents/skills`. A skills-only tool has no safe artifact
|
|
839
|
+
// to install, so preserve its legacy files and re-offer it later.
|
|
840
|
+
const sharedOwner = shouldGenerateSkills && !writesSkills
|
|
841
|
+
? arbitrationTools.find((candidate) => candidate.skillsDir === tool.skillsDir &&
|
|
842
|
+
sharedSkillWriters.has(candidate.value))?.value
|
|
830
843
|
: undefined;
|
|
831
|
-
if (sharedOwner) {
|
|
844
|
+
if (sharedOwner && !shouldGenerateCommands) {
|
|
832
845
|
const ownerName = AI_TOOLS.find((candidate) => candidate.value === sharedOwner)?.name ?? sharedOwner;
|
|
833
846
|
spinner.info(`Skipped ${tool.name}: ${tool.skillsDir}/skills is already managed by another tool (${ownerName}).`);
|
|
834
847
|
skippedSharedSkillTools.push(tool.value);
|
|
835
848
|
continue;
|
|
836
849
|
}
|
|
837
850
|
// Create skill files when delivery includes skills
|
|
838
|
-
if (shouldGenerateSkills) {
|
|
851
|
+
if (shouldGenerateSkills && writesSkills) {
|
|
839
852
|
for (const { template, dirName } of skillTemplates) {
|
|
840
853
|
const skillDir = path.join(skillsDir, dirName);
|
|
841
854
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
@@ -6,6 +6,8 @@ export declare const MIN_PURPOSE_LENGTH = 50;
|
|
|
6
6
|
export declare const MAX_WHY_SECTION_LENGTH = 1000;
|
|
7
7
|
export declare const MAX_REQUIREMENT_TEXT_LENGTH = 500;
|
|
8
8
|
export declare const MAX_DELTAS_PER_CHANGE = 10;
|
|
9
|
+
export declare const PURPOSE_PLACEHOLDER_PREFIX = "TBD - created by archiving change ";
|
|
10
|
+
export declare const PURPOSE_PLACEHOLDER_SUFFIX = ". Update Purpose after archive.";
|
|
9
11
|
export declare const VALIDATION_MESSAGES: {
|
|
10
12
|
readonly SCENARIO_EMPTY: "Scenario text cannot be empty";
|
|
11
13
|
readonly REQUIREMENT_EMPTY: "Requirement text cannot be empty";
|
|
@@ -26,6 +28,7 @@ export declare const VALIDATION_MESSAGES: {
|
|
|
26
28
|
readonly DELTA_SPEC_EMPTY: "Spec name cannot be empty";
|
|
27
29
|
readonly DELTA_DESCRIPTION_EMPTY: "Delta description cannot be empty";
|
|
28
30
|
readonly PURPOSE_TOO_BRIEF: "Purpose section is too brief (less than 50 characters)";
|
|
31
|
+
readonly PURPOSE_IS_PLACEHOLDER: string;
|
|
29
32
|
readonly REQUIREMENT_TOO_LONG: "Requirement text is very long (>500 characters). Consider breaking it down.";
|
|
30
33
|
readonly DELTA_DESCRIPTION_TOO_BRIEF: "Delta description is too brief";
|
|
31
34
|
readonly DELTA_MISSING_REQUIREMENTS: "Delta should include requirements";
|
|
@@ -8,6 +8,14 @@ export const MIN_PURPOSE_LENGTH = 50;
|
|
|
8
8
|
export const MAX_WHY_SECTION_LENGTH = 1000;
|
|
9
9
|
export const MAX_REQUIREMENT_TEXT_LENGTH = 500;
|
|
10
10
|
export const MAX_DELTAS_PER_CHANGE = 10;
|
|
11
|
+
// The Purpose `openspec archive` writes into a main spec it creates when the
|
|
12
|
+
// delta introduced the capability without a usable `## Purpose`. Named here, and
|
|
13
|
+
// composed from these two halves at the write site, so validation recognises the
|
|
14
|
+
// placeholder through the same definition that produces it: a second, hand-copied
|
|
15
|
+
// spelling would stop matching the day the wording changed, and a check that
|
|
16
|
+
// matches nothing looks exactly like a check that found nothing.
|
|
17
|
+
export const PURPOSE_PLACEHOLDER_PREFIX = 'TBD - created by archiving change ';
|
|
18
|
+
export const PURPOSE_PLACEHOLDER_SUFFIX = '. Update Purpose after archive.';
|
|
11
19
|
// Validation messages
|
|
12
20
|
export const VALIDATION_MESSAGES = {
|
|
13
21
|
// Required content
|
|
@@ -31,6 +39,10 @@ export const VALIDATION_MESSAGES = {
|
|
|
31
39
|
DELTA_DESCRIPTION_EMPTY: 'Delta description cannot be empty',
|
|
32
40
|
// Warnings
|
|
33
41
|
PURPOSE_TOO_BRIEF: `Purpose section is too brief (less than ${MIN_PURPOSE_LENGTH} characters)`,
|
|
42
|
+
PURPOSE_IS_PLACEHOLDER: 'Purpose section is still a placeholder rather than a Purpose anyone wrote (the sentence `openspec archive` ' +
|
|
43
|
+
'writes for a new capability, or a `TBD`/`TODO` marker left in its place). Replace it with what this ' +
|
|
44
|
+
'capability is for, editing the main spec directly: a `## Purpose` in a delta is read only when the ' +
|
|
45
|
+
'capability is created, so it cannot replace this one.',
|
|
34
46
|
REQUIREMENT_TOO_LONG: `Requirement text is very long (>${MAX_REQUIREMENT_TEXT_LENGTH} characters). Consider breaking it down.`,
|
|
35
47
|
DELTA_DESCRIPTION_TOO_BRIEF: 'Delta description is too brief',
|
|
36
48
|
DELTA_MISSING_REQUIREMENTS: 'Delta should include requirements',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects a `## Purpose` that is still a placeholder, rather than one somebody
|
|
3
|
+
* wrote.
|
|
4
|
+
*
|
|
5
|
+
* When a delta introduces a capability with no usable `## Purpose`, archive
|
|
6
|
+
* stamps the placeholder into the new main spec. That text is over
|
|
7
|
+
* `MIN_PURPOSE_LENGTH`, so the brevity check cannot reach it: the one rule that
|
|
8
|
+
* exists to catch a Purpose nobody wrote is satisfied by the exact string
|
|
9
|
+
* meaning nobody wrote one. Nothing else reads it afterwards, so the capability
|
|
10
|
+
* keeps a to-do in it while every command reports success.
|
|
11
|
+
*
|
|
12
|
+
* Two things count, and deliberately nothing else:
|
|
13
|
+
*
|
|
14
|
+
* - the placeholder this tool generates, recognised through the same constants
|
|
15
|
+
* the writer composes it from, wherever it sits in the Purpose - nobody types
|
|
16
|
+
* that sentence by accident;
|
|
17
|
+
* - a `TBD` or `TODO` **opening** the Purpose, which is the marker left behind
|
|
18
|
+
* when someone is told to leave "a brief TBD placeholder" and never comes
|
|
19
|
+
* back. Which of the two words got typed says nothing about whether the
|
|
20
|
+
* Purpose was written, so both are read the same way.
|
|
21
|
+
*
|
|
22
|
+
* A marker inside a sentence is left alone. "The retry budget is TBD pending
|
|
23
|
+
* benchmarks" is a real Purpose with an open question in it, and reporting it
|
|
24
|
+
* would teach people to ignore the warning - which costs more than the findings
|
|
25
|
+
* it would add.
|
|
26
|
+
*
|
|
27
|
+
* Fenced code inside the Purpose is quoted material rather than the Purpose
|
|
28
|
+
* speaking, so it is read out first. Without that, a Purpose documenting the
|
|
29
|
+
* sentence archive writes is reported as being that sentence: a document about
|
|
30
|
+
* the placeholder, failing for carrying one.
|
|
31
|
+
*/
|
|
32
|
+
export interface PurposePlaceholderIssue {
|
|
33
|
+
/** 1-based line of the placeholder text, when it can be located. */
|
|
34
|
+
line?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Reports the Purpose of a main spec as an unwritten placeholder, or null when
|
|
38
|
+
* it reads as authored content.
|
|
39
|
+
*
|
|
40
|
+
* An empty Purpose is not reported here - `SPEC_PURPOSE_EMPTY` already covers
|
|
41
|
+
* it, and reporting both would put two findings on one line. That falls out of
|
|
42
|
+
* the two rules rather than needing a case of its own.
|
|
43
|
+
*/
|
|
44
|
+
export declare function findPurposePlaceholderIssue(overview: string, content?: string): PurposePlaceholderIssue | null;
|
|
45
|
+
//# sourceMappingURL=purpose-placeholder.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { buildCodeFenceMask } from '../parsers/code-fence.js';
|
|
2
|
+
import { PURPOSE_PLACEHOLDER_PREFIX, PURPOSE_PLACEHOLDER_SUFFIX } from './constants.js';
|
|
3
|
+
/**
|
|
4
|
+
* A `TBD` or `TODO` opening the Purpose. The lookahead keeps it off a longer
|
|
5
|
+
* word that merely begins with those letters, like "TBDs" or "TODOs", while
|
|
6
|
+
* still allowing the punctuation a marker is usually written with: `TODO:`,
|
|
7
|
+
* `TBD -`. It rejects any letter, digit or combining mark rather than only the
|
|
8
|
+
* ASCII ones `\b` knows about, because a Purpose is prose and prose is not
|
|
9
|
+
* always written in Latin script - `TBD` followed by an Arabic-Indic digit is
|
|
10
|
+
* as much a longer word as `TBDs` is.
|
|
11
|
+
*/
|
|
12
|
+
const LEADING_MARKER = /^(?:TBD|TODO)(?![\p{L}\p{N}\p{M}_])/iu;
|
|
13
|
+
const PURPOSE_HEADER = /^ {0,3}##(?!#)[ \t]+Purpose[ \t]*$/i;
|
|
14
|
+
const TOP_LEVEL_HEADER = /^ {0,3}#{1,2}(?!#)[ \t]+/;
|
|
15
|
+
/**
|
|
16
|
+
* The lines of `text` that sit outside a fenced code block, with line endings
|
|
17
|
+
* normalised first.
|
|
18
|
+
*
|
|
19
|
+
* `buildCodeFenceMask` is the masker the requirement and structure parsers
|
|
20
|
+
* already share, and its own reason for existing is that a second, private
|
|
21
|
+
* notion of what a fence is drifts from the first. This check reads Markdown
|
|
22
|
+
* for the same purpose they do, so it reads fences the same way they do.
|
|
23
|
+
*/
|
|
24
|
+
function unfencedLines(text) {
|
|
25
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
26
|
+
const fenced = buildCodeFenceMask(lines);
|
|
27
|
+
return lines.filter((_, index) => !fenced[index]);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* True when the text carries the sentence archive writes. Matched as its two
|
|
31
|
+
* fixed halves in order, because the change name between them varies - so the
|
|
32
|
+
* check follows the writer's own definition instead of a second copy of it.
|
|
33
|
+
*/
|
|
34
|
+
function generatedPlaceholderPrefixIndex(text) {
|
|
35
|
+
let suffixAt = text.indexOf(PURPOSE_PLACEHOLDER_SUFFIX);
|
|
36
|
+
while (suffixAt !== -1) {
|
|
37
|
+
// Use the closest prefix before this suffix. An authored explanation can
|
|
38
|
+
// mention the prefix above the real placeholder; choosing the first prefix
|
|
39
|
+
// would then point the diagnostic at the explanation instead of the text
|
|
40
|
+
// the user needs to replace.
|
|
41
|
+
const prefixAt = text.lastIndexOf(PURPOSE_PLACEHOLDER_PREFIX, suffixAt);
|
|
42
|
+
if (prefixAt !== -1)
|
|
43
|
+
return prefixAt;
|
|
44
|
+
suffixAt = text.indexOf(PURPOSE_PLACEHOLDER_SUFFIX, suffixAt + 1);
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reports the Purpose of a main spec as an unwritten placeholder, or null when
|
|
50
|
+
* it reads as authored content.
|
|
51
|
+
*
|
|
52
|
+
* An empty Purpose is not reported here - `SPEC_PURPOSE_EMPTY` already covers
|
|
53
|
+
* it, and reporting both would put two findings on one line. That falls out of
|
|
54
|
+
* the two rules rather than needing a case of its own.
|
|
55
|
+
*/
|
|
56
|
+
export function findPurposePlaceholderIssue(overview, content) {
|
|
57
|
+
// An empty Purpose needs no branch of its own: neither rule matches empty
|
|
58
|
+
// text, so it falls through to null on the line below. An early return for it
|
|
59
|
+
// would be a guard no test could hold, which is worse than none. A Purpose
|
|
60
|
+
// that is nothing but a fenced block reduces to the same empty text here, and
|
|
61
|
+
// is left to the brevity and empty-Purpose rules for the same reason.
|
|
62
|
+
const prose = unfencedLines(overview).join('\n').trim();
|
|
63
|
+
const leading = LEADING_MARKER.test(prose);
|
|
64
|
+
if (!leading && generatedPlaceholderPrefixIndex(prose) === undefined)
|
|
65
|
+
return null;
|
|
66
|
+
// Which rule matched decides where the placeholder is, so the locator is told.
|
|
67
|
+
// When both match the leading marker wins: it sits at or above the generated
|
|
68
|
+
// sentence, and the earliest marker is the one a reader scanning down meets.
|
|
69
|
+
return { line: content === undefined ? undefined : findPlaceholderLine(content, leading) };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The line inside the `## Purpose` section carrying the placeholder, so the
|
|
73
|
+
* warning points at the text to replace rather than at the file.
|
|
74
|
+
*
|
|
75
|
+
* Which line that is depends on the rule that matched. A leading marker is the
|
|
76
|
+
* section's first non-blank line by definition. The generated sentence is not:
|
|
77
|
+
* it can follow prose somebody wrote, and naming the first non-blank line then
|
|
78
|
+
* points at that prose - a line the reader can see is fine, which reads as the
|
|
79
|
+
* check being wrong rather than the Purpose being unwritten.
|
|
80
|
+
*
|
|
81
|
+
* Fenced lines are skipped on the way, for the reason detection skips them, and
|
|
82
|
+
* so a `## Requirements` quoted inside a fence cannot end the section early.
|
|
83
|
+
*
|
|
84
|
+
* Undefined when the placeholder cannot be located - no section header, or a
|
|
85
|
+
* generated sentence no single line carries. The caller then reports the finding
|
|
86
|
+
* without a line rather than with a guessed one, since a wrong line number is
|
|
87
|
+
* worse than none.
|
|
88
|
+
*
|
|
89
|
+
* Line endings are normalised first, so the same spec reports the same line
|
|
90
|
+
* whether it was saved on Windows or on macOS/Linux.
|
|
91
|
+
*/
|
|
92
|
+
function findPlaceholderLine(content, leading) {
|
|
93
|
+
const lines = content.replace(/\r\n?/g, '\n').split('\n');
|
|
94
|
+
const fenced = buildCodeFenceMask(lines);
|
|
95
|
+
const headerIndex = lines.findIndex((line, index) => !fenced[index] && PURPOSE_HEADER.test(line));
|
|
96
|
+
if (headerIndex === -1)
|
|
97
|
+
return undefined;
|
|
98
|
+
const purposeLines = [];
|
|
99
|
+
for (let i = headerIndex + 1; i < lines.length; i++) {
|
|
100
|
+
if (fenced[i])
|
|
101
|
+
continue;
|
|
102
|
+
if (TOP_LEVEL_HEADER.test(lines[i]))
|
|
103
|
+
break;
|
|
104
|
+
if (leading && lines[i].trim())
|
|
105
|
+
return i + 1;
|
|
106
|
+
purposeLines.push({ line: i + 1, text: lines[i] });
|
|
107
|
+
}
|
|
108
|
+
if (leading)
|
|
109
|
+
return undefined;
|
|
110
|
+
const purpose = purposeLines.map(({ text }) => text).join('\n');
|
|
111
|
+
const prefixAt = generatedPlaceholderPrefixIndex(purpose);
|
|
112
|
+
if (prefixAt === undefined)
|
|
113
|
+
return undefined;
|
|
114
|
+
const lineOffset = purpose.slice(0, prefixAt).split('\n').length - 1;
|
|
115
|
+
return purposeLines[lineOffset]?.line;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=purpose-placeholder.js.map
|
|
@@ -12,6 +12,7 @@ import { discoverSpecFiles, hasAnyFileUnder } from '../../utils/spec-discovery.j
|
|
|
12
12
|
import { METADATA_FILENAME, readSkipSpecsMarker, resolveSchemaForChange, } from '../../utils/change-metadata.js';
|
|
13
13
|
import { resolveTaskFilesForChange } from '../../utils/task-progress.js';
|
|
14
14
|
import { findTaskNumberingIssues } from './task-numbering.js';
|
|
15
|
+
import { findPurposePlaceholderIssue } from './purpose-placeholder.js';
|
|
15
16
|
import { getPackageSchemasDir, getSchemaDir } from '../artifact-graph/index.js';
|
|
16
17
|
export class Validator {
|
|
17
18
|
strictMode;
|
|
@@ -557,7 +558,21 @@ export class Validator {
|
|
|
557
558
|
message: structuralIssue.message,
|
|
558
559
|
});
|
|
559
560
|
}
|
|
560
|
-
|
|
561
|
+
// The placeholder is longer than MIN_PURPOSE_LENGTH, so the brevity check
|
|
562
|
+
// below cannot reach it; it is reported on its own terms instead. Checked
|
|
563
|
+
// first because a hand-written "TBD" is both a placeholder and too brief,
|
|
564
|
+
// and only one of those two tells the author what to do. (A "TODO" opening
|
|
565
|
+
// the Purpose reads the same way, so it is the same finding.)
|
|
566
|
+
const placeholder = findPurposePlaceholderIssue(spec.overview, content);
|
|
567
|
+
if (placeholder) {
|
|
568
|
+
issues.push({
|
|
569
|
+
level: 'WARNING',
|
|
570
|
+
path: 'overview',
|
|
571
|
+
line: placeholder.line,
|
|
572
|
+
message: VALIDATION_MESSAGES.PURPOSE_IS_PLACEHOLDER,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
else if (spec.overview.length < MIN_PURPOSE_LENGTH) {
|
|
561
576
|
issues.push({
|
|
562
577
|
level: 'WARNING',
|
|
563
578
|
path: 'overview',
|
package/dist/telemetry/index.js
CHANGED
|
@@ -174,8 +174,9 @@ export async function maybeShowTelemetryNotice(options = {}) {
|
|
|
174
174
|
if (options.silent) {
|
|
175
175
|
return;
|
|
176
176
|
}
|
|
177
|
-
// Display notice
|
|
178
|
-
|
|
177
|
+
// Display notice on stderr, not stdout: stdout is reserved for command
|
|
178
|
+
// output (raw passthrough text, JSON, etc.) and must stay parser/pipe-safe.
|
|
179
|
+
console.error('Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0 or openspec config set telemetry.enabled false');
|
|
179
180
|
// Mark as seen
|
|
180
181
|
await updateTelemetryConfig({ noticeSeen: true });
|
|
181
182
|
}
|
|
@@ -179,6 +179,18 @@ export function readRetireCapabilitiesMarker(changeDir) {
|
|
|
179
179
|
* name. One body rather than two, so a marker can never drift into honoring
|
|
180
180
|
* metadata the other rejects - the whole point of the contract described above.
|
|
181
181
|
*/
|
|
182
|
+
/**
|
|
183
|
+
* A marker that cannot be honored, with its reason made safe to print.
|
|
184
|
+
*
|
|
185
|
+
* Every reason quotes something the author wrote - a schema name, a parser
|
|
186
|
+
* message carrying one, a filesystem error carrying a path - and callers print
|
|
187
|
+
* it straight to a terminal (`openspec archive`, `openspec validate`). A raw CR
|
|
188
|
+
* could forge a line of its own and an ESC could redraw the screen, so control
|
|
189
|
+
* characters never leave this function.
|
|
190
|
+
*/
|
|
191
|
+
function unhonorable(reason) {
|
|
192
|
+
return { declared: false, invalidReason: reason.replace(/[\u0000-\u001f\u007f]/g, '?') };
|
|
193
|
+
}
|
|
182
194
|
function readBooleanMarker(changeDir, key) {
|
|
183
195
|
let raw;
|
|
184
196
|
try {
|
|
@@ -193,10 +205,7 @@ function readBooleanMarker(changeDir, key) {
|
|
|
193
205
|
// set cannot be determined - fail closed rather than let archive treat
|
|
194
206
|
// the change as unmarked while every metadata-reading surface errors.
|
|
195
207
|
const message = err instanceof Error ? err.message : String(err);
|
|
196
|
-
return {
|
|
197
|
-
declared: false,
|
|
198
|
-
invalidReason: `the metadata file cannot be read (${message})`,
|
|
199
|
-
};
|
|
208
|
+
return unhonorable(`the metadata file cannot be read (${message})`);
|
|
200
209
|
}
|
|
201
210
|
let parsed;
|
|
202
211
|
try {
|
|
@@ -206,9 +215,7 @@ function readBooleanMarker(changeDir, key) {
|
|
|
206
215
|
// Anchored so a comment like "# maybe add skip_specs later" does not
|
|
207
216
|
// claim the marker was set.
|
|
208
217
|
const mentioned = new RegExp(`^\\s*(['"]?)${key}\\1\\s*:`, 'm').test(raw);
|
|
209
|
-
return mentioned
|
|
210
|
-
? { declared: false, invalidReason: 'the file is not valid YAML' }
|
|
211
|
-
: { declared: false };
|
|
218
|
+
return mentioned ? unhonorable('the file is not valid YAML') : { declared: false };
|
|
212
219
|
}
|
|
213
220
|
const result = ChangeMetadataSchema.safeParse(parsed);
|
|
214
221
|
if (result.success) {
|
|
@@ -225,16 +232,13 @@ function readBooleanMarker(changeDir, key) {
|
|
|
225
232
|
try {
|
|
226
233
|
const projectRoot = path.resolve(changeDir, '../../..');
|
|
227
234
|
if (!listSchemas(projectRoot).includes(result.data.schema)) {
|
|
228
|
-
return {
|
|
229
|
-
declared: false,
|
|
230
|
-
invalidReason: `schema: unknown schema '${result.data.schema}'`,
|
|
231
|
-
};
|
|
235
|
+
return unhonorable(`schema: unknown schema '${result.data.schema}'`);
|
|
232
236
|
}
|
|
233
237
|
resolveSchema(result.data.schema, projectRoot);
|
|
234
238
|
}
|
|
235
239
|
catch (err) {
|
|
236
240
|
const message = err instanceof Error ? err.message : String(err);
|
|
237
|
-
return
|
|
241
|
+
return unhonorable(message);
|
|
238
242
|
}
|
|
239
243
|
return { declared: true };
|
|
240
244
|
}
|
|
@@ -250,7 +254,7 @@ function readBooleanMarker(changeDir, key) {
|
|
|
250
254
|
if (markerMentioned) {
|
|
251
255
|
const first = result.error.issues[0];
|
|
252
256
|
const where = first.path.length > 0 ? `${first.path.join('.')}: ` : '';
|
|
253
|
-
return
|
|
257
|
+
return unhonorable(`${where}${first.message}`);
|
|
254
258
|
}
|
|
255
259
|
return { declared: false };
|
|
256
260
|
}
|
|
@@ -4,6 +4,8 @@ import { writeChangeMetadata, validateSchemaName } from './change-metadata.js';
|
|
|
4
4
|
import { formatLocalDate } from './date.js';
|
|
5
5
|
import { readProjectConfig } from '../core/project-config.js';
|
|
6
6
|
import { isKebabId } from '../core/id.js';
|
|
7
|
+
import { resolveSchema } from '../core/artifact-graph/resolver.js';
|
|
8
|
+
import { isSpecsArtifactPath } from '../core/artifact-graph/outputs.js';
|
|
7
9
|
const DEFAULT_SCHEMA = 'spec-driven';
|
|
8
10
|
/**
|
|
9
11
|
* Validates that a change name follows kebab-case conventions.
|
|
@@ -117,6 +119,8 @@ export async function createChange(projectRoot, name, options = {}) {
|
|
|
117
119
|
if (await FileSystemUtils.directoryExists(changeDir)) {
|
|
118
120
|
throw new Error(`Change '${name}' already exists at ${changeDir}`);
|
|
119
121
|
}
|
|
122
|
+
const schema = resolveSchema(schemaName, projectRoot);
|
|
123
|
+
const skipsSpecs = !schema.artifacts.some(artifact => isSpecsArtifactPath(artifact.generates));
|
|
120
124
|
// Creating a change may scaffold or complete the root itself (an
|
|
121
125
|
// implicit root, or a config-only/incomplete clone). Never leave a
|
|
122
126
|
// half-root behind that doctor immediately calls unhealthy: ensure
|
|
@@ -138,6 +142,7 @@ export async function createChange(projectRoot, name, options = {}) {
|
|
|
138
142
|
writeChangeMetadata(changeDir, {
|
|
139
143
|
schema: schemaName,
|
|
140
144
|
created: formatLocalDate(),
|
|
145
|
+
...(skipsSpecs ? { skip_specs: true } : {}),
|
|
141
146
|
...options.metadata,
|
|
142
147
|
}, projectRoot);
|
|
143
148
|
return { schema: schemaName, changeDir };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** A main-spec requirement block, and how its header matched the delta's. */
|
|
2
|
+
export interface MatchedRequirementBlock {
|
|
3
|
+
raw: string;
|
|
4
|
+
/** The header name as the main spec spells it. */
|
|
5
|
+
name: string;
|
|
6
|
+
/**
|
|
7
|
+
* False when the two headers differ only in case or interior spacing. Archive
|
|
8
|
+
* matches names exactly, so an inexact match is a real problem to report even
|
|
9
|
+
* though the diff below it is still the one the author meant.
|
|
10
|
+
*/
|
|
11
|
+
exact: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Find the raw markdown block for a requirement by name in a spec file.
|
|
15
|
+
*
|
|
16
|
+
* Delegates to extractRequirementsSection() which already handles code fences,
|
|
17
|
+
* section boundaries, and requirement header parsing. We just look up by name:
|
|
18
|
+
* exactly first, then falling back to the shared case/whitespace fold so a
|
|
19
|
+
* header that differs only in spelling still shows its diff, flagged inexact.
|
|
20
|
+
*
|
|
21
|
+
* Returns null if no requirement header matches either way.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractRequirementBlock(specContent: string, requirementName: string): MatchedRequirementBlock | null;
|
|
24
|
+
/**
|
|
25
|
+
* Compute a unified diff between a main-spec requirement block and the delta
|
|
26
|
+
* block that replaces it. A null main block (new capability) diffs against the
|
|
27
|
+
* empty string, so every line reads as an addition.
|
|
28
|
+
*
|
|
29
|
+
* Uses structuredPatch to retain unified-diff hunk ranges while omitting the
|
|
30
|
+
* synthetic file headers, since the caller provides its own labeling.
|
|
31
|
+
*/
|
|
32
|
+
export declare function diffRequirementBlock(baseBlock: string | null, deltaBlock: string, label: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Build a map from folded RENAMED TO name to the original main-spec name.
|
|
35
|
+
* Renames apply in source order, so a chain such as A -> B -> C maps C back to
|
|
36
|
+
* A. That is the block a later MODIFIED C replaces.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildRenameMap(renames: Array<{
|
|
39
|
+
from: string;
|
|
40
|
+
to: string;
|
|
41
|
+
}>): Map<string, string>;
|
|
42
|
+
//# sourceMappingURL=requirement-diff.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { formatPatch, OMIT_HEADERS, structuredPatch } from 'diff';
|
|
2
|
+
import { extractRequirementsSection, foldRequirementName, normalizeRequirementName, } from '../core/parsers/requirement-blocks.js';
|
|
3
|
+
/**
|
|
4
|
+
* Find the raw markdown block for a requirement by name in a spec file.
|
|
5
|
+
*
|
|
6
|
+
* Delegates to extractRequirementsSection() which already handles code fences,
|
|
7
|
+
* section boundaries, and requirement header parsing. We just look up by name:
|
|
8
|
+
* exactly first, then falling back to the shared case/whitespace fold so a
|
|
9
|
+
* header that differs only in spelling still shows its diff, flagged inexact.
|
|
10
|
+
*
|
|
11
|
+
* Returns null if no requirement header matches either way.
|
|
12
|
+
*/
|
|
13
|
+
export function extractRequirementBlock(specContent, requirementName) {
|
|
14
|
+
const parts = extractRequirementsSection(specContent);
|
|
15
|
+
const targetName = normalizeRequirementName(requirementName);
|
|
16
|
+
const foldedTarget = foldRequirementName(targetName);
|
|
17
|
+
let folded = null;
|
|
18
|
+
for (const block of parts.bodyBlocks) {
|
|
19
|
+
const name = normalizeRequirementName(block.name);
|
|
20
|
+
if (name === targetName) {
|
|
21
|
+
return { raw: block.raw, name, exact: true };
|
|
22
|
+
}
|
|
23
|
+
if (!folded && foldRequirementName(name) === foldedTarget) {
|
|
24
|
+
folded = { raw: block.raw, name, exact: false };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return folded;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compute a unified diff between a main-spec requirement block and the delta
|
|
31
|
+
* block that replaces it. A null main block (new capability) diffs against the
|
|
32
|
+
* empty string, so every line reads as an addition.
|
|
33
|
+
*
|
|
34
|
+
* Uses structuredPatch to retain unified-diff hunk ranges while omitting the
|
|
35
|
+
* synthetic file headers, since the caller provides its own labeling.
|
|
36
|
+
*/
|
|
37
|
+
export function diffRequirementBlock(baseBlock, deltaBlock, label) {
|
|
38
|
+
const base = ensureTrailingNewline(baseBlock ?? '');
|
|
39
|
+
const delta = ensureTrailingNewline(deltaBlock);
|
|
40
|
+
const patch = structuredPatch(label, label, base, delta);
|
|
41
|
+
return formatPatch(patch, OMIT_HEADERS).trimEnd();
|
|
42
|
+
}
|
|
43
|
+
function ensureTrailingNewline(s) {
|
|
44
|
+
return s.endsWith('\n') ? s : s + '\n';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build a map from folded RENAMED TO name to the original main-spec name.
|
|
48
|
+
* Renames apply in source order, so a chain such as A -> B -> C maps C back to
|
|
49
|
+
* A. That is the block a later MODIFIED C replaces.
|
|
50
|
+
*/
|
|
51
|
+
export function buildRenameMap(renames) {
|
|
52
|
+
const map = new Map();
|
|
53
|
+
for (const r of renames) {
|
|
54
|
+
const from = foldRequirementName(r.from);
|
|
55
|
+
const original = map.get(from) ?? normalizeRequirementName(r.from);
|
|
56
|
+
map.delete(from);
|
|
57
|
+
map.set(foldRequirementName(r.to), original);
|
|
58
|
+
}
|
|
59
|
+
return map;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=requirement-diff.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fission-ai/openspec",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openspec",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"dist",
|
|
34
34
|
"bin",
|
|
35
35
|
"schemas",
|
|
36
|
-
"scripts/postinstall.js",
|
|
37
36
|
"!dist/**/*.test.js",
|
|
38
37
|
"!dist/**/__tests__",
|
|
39
38
|
"!dist/**/*.map"
|
|
@@ -53,10 +52,11 @@
|
|
|
53
52
|
"vitest": "^3.2.6"
|
|
54
53
|
},
|
|
55
54
|
"dependencies": {
|
|
56
|
-
"@inquirer/core": "^
|
|
57
|
-
"@inquirer/prompts": "^
|
|
55
|
+
"@inquirer/core": "^11.2.1",
|
|
56
|
+
"@inquirer/prompts": "^8.5.2",
|
|
58
57
|
"chalk": "^5.6.2",
|
|
59
58
|
"commander": "^14.0.0",
|
|
59
|
+
"diff": "^9.0.0",
|
|
60
60
|
"cross-spawn": "7.0.6",
|
|
61
61
|
"fast-glob": "^3.3.3",
|
|
62
62
|
"ora": "^9.4.1",
|
|
@@ -74,8 +74,6 @@
|
|
|
74
74
|
"test:watch": "vitest",
|
|
75
75
|
"test:ui": "vitest --ui",
|
|
76
76
|
"test:coverage": "vitest --coverage",
|
|
77
|
-
"test:postinstall": "node scripts/postinstall.js",
|
|
78
|
-
"postinstall": "node scripts/postinstall.js",
|
|
79
77
|
"check:pack-version": "node scripts/pack-version-check.mjs",
|
|
80
78
|
"release": "pnpm run release:ci",
|
|
81
79
|
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|