@fission-ai/openspec 1.11.0 → 1.13.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 +3 -14
- package/dist/cli/index.js +1 -0
- package/dist/commands/change.js +6 -5
- package/dist/commands/validate.d.ts +57 -0
- package/dist/commands/validate.js +99 -13
- package/dist/commands/workflow/instructions.js +140 -5
- package/dist/commands/workflow/shared.d.ts +8 -0
- package/dist/core/command-generation/adapters/codeassistant.d.ts +15 -0
- package/dist/core/command-generation/adapters/codeassistant.js +29 -0
- package/dist/core/command-generation/adapters/index.d.ts +1 -0
- package/dist/core/command-generation/adapters/index.js +1 -0
- package/dist/core/command-generation/registry.js +2 -0
- package/dist/core/completions/command-registry.js +6 -0
- package/dist/core/config.js +1 -0
- package/dist/core/init.d.ts +1 -0
- package/dist/core/init.js +41 -34
- package/dist/core/onboarding-commands.d.ts +11 -1
- package/dist/core/onboarding-commands.js +26 -0
- package/dist/core/openspec-root.d.ts +1 -0
- package/dist/core/openspec-root.js +10 -2
- package/dist/core/parsers/requirement-blocks.js +77 -31
- package/dist/core/shared/ide-restart.d.ts +26 -0
- package/dist/core/shared/ide-restart.js +42 -0
- package/dist/core/shared/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/tool-detection.js +28 -1
- package/dist/core/specs-apply.js +184 -8
- package/dist/core/templates/workflows/explore.js +45 -0
- package/dist/core/templates/workflows/ff-change.js +8 -0
- package/dist/core/templates/workflows/propose.js +40 -12
- package/dist/core/update.d.ts +22 -0
- package/dist/core/update.js +55 -17
- package/dist/core/validation/validator.d.ts +12 -3
- package/dist/core/validation/validator.js +74 -3
- package/dist/utils/command-references.js +7 -8
- package/package.json +3 -3
- package/schemas/spec-driven/schema.yaml +14 -2
package/dist/core/init.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ export declare class InitCommand {
|
|
|
78
78
|
private resolveToolsArg;
|
|
79
79
|
private validateTools;
|
|
80
80
|
private createDirectoryStructure;
|
|
81
|
+
private writeGitkeepFiles;
|
|
81
82
|
/**
|
|
82
83
|
* Generates skill files and slash commands for each selected tool,
|
|
83
84
|
* honoring the configured delivery mode (skills, commands, or both).
|
package/dist/core/init.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createRequire } from 'module';
|
|
|
12
12
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
13
13
|
import { classifyOpenSpecDir, MAX_CONTEXT_SIZE, readProjectConfig, storePointerProblem, } from './project-config.js';
|
|
14
14
|
import { findRepoPlanningRootSync } from './planning-home.js';
|
|
15
|
+
import { ANCHORED_OPENSPEC_DIRS, ensureDirectoryAnchor } from './openspec-root.js';
|
|
15
16
|
import { getSkillReferenceTransformer, getTransformerForTool, usesNaturalLanguageSkillReferences } from '../utils/command-references.js';
|
|
16
17
|
import { AI_TOOLS, OPENSPEC_DIR_NAME, resolveToolIdAlias, } from './config.js';
|
|
17
18
|
import { PALETTE } from './styles/palette.js';
|
|
@@ -19,10 +20,11 @@ import { isInteractive } from '../utils/interactive.js';
|
|
|
19
20
|
import { serializeConfig } from './config-prompts.js';
|
|
20
21
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
21
22
|
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDeferredGlobalPromptSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, omitGlobalLegacyPromptFiles, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
22
|
-
import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/index.js';
|
|
23
|
+
import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, formatIdeRestart, } from './shared/index.js';
|
|
23
24
|
import { getGlobalConfig } from './global-config.js';
|
|
24
25
|
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
|
|
25
26
|
import { getAvailableTools } from './available-tools.js';
|
|
27
|
+
import { formatOptionalWorkflowsNote } from './onboarding-commands.js';
|
|
26
28
|
import { resolveSharedSkillWriters, sharedSkillRootOwner, writeSharedSkillTarget, } from './shared-skill-target.js';
|
|
27
29
|
import { migrateIfNeeded, migrateLegacyToolDirs, describeLegacyMigration, keptInPlaceNotice, hasMovableContent, scanInstalledWorkflows as scanInstalledWorkflowsShared } from './migration.js';
|
|
28
30
|
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
@@ -590,7 +592,6 @@ export class InitCommand {
|
|
|
590
592
|
skillsRoot: isGlobalSkillTarget ? skillsPath : projectPath,
|
|
591
593
|
isGlobalSkillTarget,
|
|
592
594
|
wasConfigured: preState?.configured ?? false,
|
|
593
|
-
requiresIdeRestart: tool.requiresIdeRestart,
|
|
594
595
|
writesSkills: !tool.skillsDir || skillWriters.has(tool.value),
|
|
595
596
|
});
|
|
596
597
|
}
|
|
@@ -600,36 +601,37 @@ export class InitCommand {
|
|
|
600
601
|
// DIRECTORY STRUCTURE
|
|
601
602
|
// ═══════════════════════════════════════════════════════════
|
|
602
603
|
async createDirectoryStructure(openspecPath, extendMode) {
|
|
604
|
+
const directories = [
|
|
605
|
+
openspecPath,
|
|
606
|
+
path.join(openspecPath, 'specs'),
|
|
607
|
+
path.join(openspecPath, 'changes'),
|
|
608
|
+
path.join(openspecPath, 'changes', 'archive'),
|
|
609
|
+
];
|
|
603
610
|
if (extendMode) {
|
|
604
611
|
// In extend mode, just ensure directories exist without spinner
|
|
605
|
-
const directories = [
|
|
606
|
-
openspecPath,
|
|
607
|
-
path.join(openspecPath, 'specs'),
|
|
608
|
-
path.join(openspecPath, 'changes'),
|
|
609
|
-
path.join(openspecPath, 'changes', 'archive'),
|
|
610
|
-
];
|
|
611
612
|
for (const dir of directories) {
|
|
612
613
|
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
|
|
613
614
|
await FileSystemUtils.createDirectory(dir);
|
|
614
615
|
}
|
|
616
|
+
await this.writeGitkeepFiles(openspecPath);
|
|
615
617
|
return;
|
|
616
618
|
}
|
|
617
619
|
const spinner = this.startSpinner('Creating OpenSpec structure...');
|
|
618
|
-
const directories = [
|
|
619
|
-
openspecPath,
|
|
620
|
-
path.join(openspecPath, 'specs'),
|
|
621
|
-
path.join(openspecPath, 'changes'),
|
|
622
|
-
path.join(openspecPath, 'changes', 'archive'),
|
|
623
|
-
];
|
|
624
620
|
for (const dir of directories) {
|
|
625
621
|
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
|
|
626
622
|
await FileSystemUtils.createDirectory(dir);
|
|
627
623
|
}
|
|
624
|
+
await this.writeGitkeepFiles(openspecPath);
|
|
628
625
|
spinner.stopAndPersist({
|
|
629
626
|
symbol: PALETTE.white('▌'),
|
|
630
627
|
text: PALETTE.white('OpenSpec structure created'),
|
|
631
628
|
});
|
|
632
629
|
}
|
|
630
|
+
async writeGitkeepFiles(openspecPath) {
|
|
631
|
+
for (const relativeDir of ANCHORED_OPENSPEC_DIRS) {
|
|
632
|
+
await ensureDirectoryAnchor(path.dirname(openspecPath), relativeDir);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
633
635
|
// ═══════════════════════════════════════════════════════════
|
|
634
636
|
// SKILL & COMMAND GENERATION
|
|
635
637
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -1017,9 +1019,11 @@ export class InitCommand {
|
|
|
1017
1019
|
`${zeroArtifactTools.length === 1 ? 'it supports' : 'they support'} only skills. ` +
|
|
1018
1020
|
`Run 'openspec config set delivery both' to generate skills.`));
|
|
1019
1021
|
}
|
|
1022
|
+
let advertisedAnInvocation = true;
|
|
1020
1023
|
if (successfulTools.length > 0 && !commandsGenerated && !skillsGenerated) {
|
|
1021
1024
|
// Nothing was generated for any tool: the correction above is the
|
|
1022
1025
|
// whole story, so don't advertise an invocation that doesn't exist.
|
|
1026
|
+
advertisedAnInvocation = false;
|
|
1023
1027
|
}
|
|
1024
1028
|
else if (activeWorkflows.includes('propose')) {
|
|
1025
1029
|
printStartHints('/opsx:propose');
|
|
@@ -1029,32 +1033,35 @@ export class InitCommand {
|
|
|
1029
1033
|
}
|
|
1030
1034
|
else {
|
|
1031
1035
|
console.log("Done. Run 'openspec config profile' to configure your workflows.");
|
|
1036
|
+
advertisedAnInvocation = false;
|
|
1037
|
+
}
|
|
1038
|
+
// Workflows the active profile left out. Setup is the only moment a user
|
|
1039
|
+
// is told what exists, so name them here rather than let a missing
|
|
1040
|
+
// command read as a broken install (#1076). Skipped when the branch above
|
|
1041
|
+
// already pointed at `openspec config profile`, and when no tool received
|
|
1042
|
+
// a workflow surface at all (no tools selected, or none that could take
|
|
1043
|
+
// one) — there, adding workflows writes nothing, so naming them would
|
|
1044
|
+
// point at the wrong problem.
|
|
1045
|
+
if (advertisedAnInvocation && (commandsGenerated || skillsGenerated)) {
|
|
1046
|
+
const optionalWorkflowsNote = formatOptionalWorkflowsNote(activeWorkflows);
|
|
1047
|
+
if (optionalWorkflowsNote) {
|
|
1048
|
+
console.log();
|
|
1049
|
+
for (const line of optionalWorkflowsNote) {
|
|
1050
|
+
console.log(chalk.dim(line));
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1032
1053
|
}
|
|
1033
1054
|
// Links
|
|
1034
1055
|
console.log();
|
|
1035
1056
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
1036
1057
|
console.log(`Feedback: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec/issues')}`);
|
|
1037
|
-
// Restart instruction
|
|
1038
|
-
//
|
|
1039
|
-
//
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
// under the active delivery (an IDE tool that generated nothing has nothing a
|
|
1043
|
-
// restart would pick up, even if a co-configured CLI tool did generate).
|
|
1044
|
-
// Wording follows what the IDE tool itself generated, not the global
|
|
1045
|
-
// aggregate: it must not say "commands" when the IDE tool only got skills
|
|
1046
|
-
// while a co-configured CLI tool got commands. Not "slash commands" either:
|
|
1047
|
-
// Amazon Q's generated files are prompt-library entries invoked with @, so a
|
|
1048
|
-
// restart line promising slash commands would be wrong for it.
|
|
1049
|
-
const restartCommandsGenerated = successfulTools.some((tool) => tool.requiresIdeRestart &&
|
|
1050
|
-
shouldGenerateCommandsForTool(tool.value, activeDelivery));
|
|
1051
|
-
const restartSkillsGenerated = successfulTools.some((tool) => tool.requiresIdeRestart &&
|
|
1052
|
-
shouldGenerateSkillsForTool(tool.value, activeDelivery));
|
|
1053
|
-
if (restartCommandsGenerated || restartSkillsGenerated) {
|
|
1058
|
+
// Restart instruction for successfully configured IDE/editor-resident tools
|
|
1059
|
+
// with a supported surface under the active delivery. The rule and wording live in
|
|
1060
|
+
// formatIdeRestart so `update` says the same thing for the same event.
|
|
1061
|
+
const restartHint = formatIdeRestart(successfulTools.map((tool) => tool.value), activeDelivery);
|
|
1062
|
+
if (restartHint) {
|
|
1054
1063
|
console.log();
|
|
1055
|
-
console.log(chalk.white(
|
|
1056
|
-
? 'Restart your IDE for the new commands to take effect.'
|
|
1057
|
-
: 'Restart your IDE for the new skills to take effect.'));
|
|
1064
|
+
console.log(chalk.white(restartHint));
|
|
1058
1065
|
}
|
|
1059
1066
|
console.log();
|
|
1060
1067
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* tool — command, skill, or a tool-specific skill prefix — is decided by
|
|
10
10
|
* src/utils/command-references.ts at the call site.
|
|
11
11
|
*/
|
|
12
|
-
import type
|
|
12
|
+
import { type WorkflowId } from './profiles.js';
|
|
13
13
|
export type OnboardingCommand = {
|
|
14
14
|
workflow: WorkflowId;
|
|
15
15
|
command: string;
|
|
@@ -27,4 +27,14 @@ export declare const DESCRIPTION_BUDGET = 17;
|
|
|
27
27
|
* Returns an empty array when none of the onboarding workflows are installed.
|
|
28
28
|
*/
|
|
29
29
|
export declare function getOnboardingCommands(workflows: readonly string[]): OnboardingCommand[];
|
|
30
|
+
/**
|
|
31
|
+
* Returns the note telling a user which workflows their profile left out, or
|
|
32
|
+
* null when every workflow is already installed.
|
|
33
|
+
*
|
|
34
|
+
* Setup output otherwise never names the workflows that exist but were not
|
|
35
|
+
* installed, so a user on the default profile has no way to learn that
|
|
36
|
+
* `/opsx:ff` and friends are one command away. The docs say it; nobody reads
|
|
37
|
+
* the docs before typing a command that isn't there.
|
|
38
|
+
*/
|
|
39
|
+
export declare function formatOptionalWorkflowsNote(installedWorkflows: readonly string[]): string[] | null;
|
|
30
40
|
//# sourceMappingURL=onboarding-commands.d.ts.map
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* tool — command, skill, or a tool-specific skill prefix — is decided by
|
|
10
10
|
* src/utils/command-references.ts at the call site.
|
|
11
11
|
*/
|
|
12
|
+
import { ALL_WORKFLOWS } from './profiles.js';
|
|
12
13
|
/**
|
|
13
14
|
* Longest description the welcome screen can render. It shows these beside a
|
|
14
15
|
* 24-column art column and only animates at MIN_WIDTH (60) columns or wider; a
|
|
@@ -35,4 +36,29 @@ export function getOnboardingCommands(workflows) {
|
|
|
35
36
|
const installed = new Set(workflows);
|
|
36
37
|
return ONBOARDING_COMMANDS.filter((entry) => installed.has(entry.workflow));
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns the note telling a user which workflows their profile left out, or
|
|
41
|
+
* null when every workflow is already installed.
|
|
42
|
+
*
|
|
43
|
+
* Setup output otherwise never names the workflows that exist but were not
|
|
44
|
+
* installed, so a user on the default profile has no way to learn that
|
|
45
|
+
* `/opsx:ff` and friends are one command away. The docs say it; nobody reads
|
|
46
|
+
* the docs before typing a command that isn't there.
|
|
47
|
+
*/
|
|
48
|
+
export function formatOptionalWorkflowsNote(installedWorkflows) {
|
|
49
|
+
const installed = new Set(installedWorkflows);
|
|
50
|
+
const missing = ALL_WORKFLOWS.filter((workflow) => !installed.has(workflow));
|
|
51
|
+
if (missing.length === 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const label = missing.length === 1 ? 'workflow is' : 'workflows are';
|
|
55
|
+
const pronoun = missing.length === 1 ? 'it' : 'them';
|
|
56
|
+
// `openspec config profile` offers to apply to this project before it
|
|
57
|
+
// exits, and prints the `openspec update` guidance itself when declined, so
|
|
58
|
+
// naming a second command here would be one step too many.
|
|
59
|
+
return [
|
|
60
|
+
`Note: ${missing.length} more ${label} available (${missing.join(', ')}).`,
|
|
61
|
+
`Add ${pronoun} with \`openspec config profile\`.`,
|
|
62
|
+
];
|
|
63
|
+
}
|
|
38
64
|
//# sourceMappingURL=onboarding-commands.js.map
|
|
@@ -37,6 +37,7 @@ export interface EnsureOpenSpecRootResult {
|
|
|
37
37
|
createdPaths: CreatedPathLedgerEntry[];
|
|
38
38
|
}
|
|
39
39
|
export declare function inspectOpenSpecRoot(storeRoot: string): Promise<OpenSpecRootInspection>;
|
|
40
|
+
export declare function ensureDirectoryAnchor(storeRoot: string, relativeDir: string, ledger?: CreatedPathLedgerEntry[]): Promise<void>;
|
|
40
41
|
export interface EnsureOpenSpecRootOptions {
|
|
41
42
|
anchorEmptyDirectories?: boolean;
|
|
42
43
|
}
|
|
@@ -144,13 +144,21 @@ async function ensureDefaultConfig(storeRoot, ledger) {
|
|
|
144
144
|
kind: 'file',
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
|
-
async function ensureDirectoryAnchor(storeRoot, relativeDir, ledger) {
|
|
147
|
+
export async function ensureDirectoryAnchor(storeRoot, relativeDir, ledger = []) {
|
|
148
148
|
const directory = path.join(storeRoot, relativeDir);
|
|
149
149
|
if ((await fs.readdir(directory)).length > 0)
|
|
150
150
|
return;
|
|
151
151
|
const relativePath = `${relativeDir}/${DIRECTORY_ANCHOR_FILE_NAME}`;
|
|
152
152
|
const absolutePath = path.join(directory, DIRECTORY_ANCHOR_FILE_NAME);
|
|
153
|
-
|
|
153
|
+
try {
|
|
154
|
+
// A file or symlink may appear after readdir. Never replace or follow it.
|
|
155
|
+
await fs.writeFile(absolutePath, '', { encoding: 'utf-8', flag: 'wx' });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error.code === 'EEXIST')
|
|
159
|
+
return;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
154
162
|
ledger.push({
|
|
155
163
|
relativePath: relativeArtifact(relativePath, 'file'),
|
|
156
164
|
absolutePath,
|
|
@@ -99,24 +99,26 @@ export function parseDeltaSpec(content) {
|
|
|
99
99
|
const lines = normalized.split('\n');
|
|
100
100
|
const fenceMask = buildCodeFenceMask(lines);
|
|
101
101
|
const sections = splitTopLevelSections(lines, fenceMask);
|
|
102
|
-
const addedLookup =
|
|
103
|
-
const modifiedLookup =
|
|
104
|
-
const removedLookup =
|
|
105
|
-
const renamedLookup =
|
|
102
|
+
const addedLookup = getSectionsCaseInsensitive(sections, 'ADDED Requirements');
|
|
103
|
+
const modifiedLookup = getSectionsCaseInsensitive(sections, 'MODIFIED Requirements');
|
|
104
|
+
const removedLookup = getSectionsCaseInsensitive(sections, 'REMOVED Requirements');
|
|
105
|
+
const renamedLookup = getSectionsCaseInsensitive(sections, 'RENAMED Requirements');
|
|
106
106
|
const skippedHeaders = [];
|
|
107
|
-
const added =
|
|
107
|
+
const added = addedLookup.bodies.flatMap((body) => parseRequirementBlocksFromSection(body, {
|
|
108
108
|
section: addedLookup.title,
|
|
109
|
-
bodyStartLine:
|
|
109
|
+
bodyStartLine: body.bodyStartLine,
|
|
110
110
|
sink: skippedHeaders,
|
|
111
|
-
});
|
|
112
|
-
const modified =
|
|
111
|
+
}));
|
|
112
|
+
const modified = modifiedLookup.bodies.flatMap((body) => parseRequirementBlocksFromSection(body, {
|
|
113
113
|
section: modifiedLookup.title,
|
|
114
|
-
bodyStartLine:
|
|
114
|
+
bodyStartLine: body.bodyStartLine,
|
|
115
115
|
sink: skippedHeaders,
|
|
116
|
-
});
|
|
117
|
-
const removedNames =
|
|
118
|
-
const removedBlocks =
|
|
119
|
-
|
|
116
|
+
}));
|
|
117
|
+
const removedNames = removedLookup.bodies.flatMap((body) => parseRemovedNames(body));
|
|
118
|
+
const removedBlocks = removedLookup.bodies.flatMap((body) => parseRequirementBlocksFromSection(body));
|
|
119
|
+
// Pairs are read per section, so a FROM in one copy of the header can never
|
|
120
|
+
// pair with a TO in another.
|
|
121
|
+
const renamedPairs = renamedLookup.bodies.flatMap((body) => parseRenamedPairs(body));
|
|
120
122
|
skippedHeaders.sort((a, b) => a.line - b.line);
|
|
121
123
|
return {
|
|
122
124
|
added,
|
|
@@ -133,8 +135,16 @@ export function parseDeltaSpec(content) {
|
|
|
133
135
|
},
|
|
134
136
|
};
|
|
135
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Every `## ` section, as a LIST rather than a title-keyed record.
|
|
140
|
+
*
|
|
141
|
+
* Keying by title silently dropped a repeated header: a delta that wrote
|
|
142
|
+
* `## ADDED Requirements` twice kept only the last body, so every requirement
|
|
143
|
+
* under the first copy was discarded before any validation or merge rule could
|
|
144
|
+
* see it. A list keeps each occurrence, and the lookup below merges them.
|
|
145
|
+
*/
|
|
136
146
|
function splitTopLevelSections(lines, fenceMask) {
|
|
137
|
-
const
|
|
147
|
+
const sections = [];
|
|
138
148
|
const indices = [];
|
|
139
149
|
for (let i = 0; i < lines.length; i++) {
|
|
140
150
|
if (fenceMask[i])
|
|
@@ -148,23 +158,39 @@ function splitTopLevelSections(lines, fenceMask) {
|
|
|
148
158
|
const current = indices[i];
|
|
149
159
|
const next = indices[i + 1];
|
|
150
160
|
const end = next ? next.index : lines.length;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
sections.push({
|
|
162
|
+
title: current.title,
|
|
163
|
+
body: {
|
|
164
|
+
lines: lines.slice(current.index + 1, end),
|
|
165
|
+
fenceMask: fenceMask.slice(current.index + 1, end),
|
|
166
|
+
bodyStartLine: current.index + 2,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
156
169
|
}
|
|
157
|
-
return
|
|
170
|
+
return sections;
|
|
158
171
|
}
|
|
159
|
-
|
|
160
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Every section body whose title folds to `desired`, in document order.
|
|
174
|
+
*
|
|
175
|
+
* Returning all of them - rather than the first match - is what makes a
|
|
176
|
+
* repeated header (`## ADDED Requirements` twice) and a case variant
|
|
177
|
+
* (`## ADDED Requirements` + `## Added Requirements`) both apply in full. Each
|
|
178
|
+
* body keeps its own `bodyStartLine`, so reported line numbers stay correct for
|
|
179
|
+
* the copy the header actually came from.
|
|
180
|
+
*
|
|
181
|
+
* `title` is the first spelling the author used, which is what diagnostics quote.
|
|
182
|
+
*/
|
|
183
|
+
function getSectionsCaseInsensitive(sections, desired) {
|
|
161
184
|
const target = desired.toLowerCase();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
185
|
+
const matches = sections.filter((section) => section.title.toLowerCase() === target);
|
|
186
|
+
if (matches.length === 0) {
|
|
187
|
+
return { title: desired, bodies: [], found: false };
|
|
166
188
|
}
|
|
167
|
-
return {
|
|
189
|
+
return {
|
|
190
|
+
title: matches[0].title,
|
|
191
|
+
bodies: matches.map((section) => section.body),
|
|
192
|
+
found: true,
|
|
193
|
+
};
|
|
168
194
|
}
|
|
169
195
|
function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
170
196
|
const { lines, fenceMask } = sectionBody;
|
|
@@ -212,6 +238,13 @@ function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
|
212
238
|
}
|
|
213
239
|
return blocks;
|
|
214
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Requirement names listed in `## REMOVED Requirements`, in document order.
|
|
243
|
+
*
|
|
244
|
+
* Two spellings are accepted: a plain `### Requirement:` header, and a bullet
|
|
245
|
+
* carrying one. Every CommonMark bullet marker counts for the second form -
|
|
246
|
+
* see the pattern below for why that matters.
|
|
247
|
+
*/
|
|
215
248
|
function parseRemovedNames(sectionBody) {
|
|
216
249
|
const { lines, fenceMask } = sectionBody;
|
|
217
250
|
if (lines.length === 0)
|
|
@@ -226,14 +259,24 @@ function parseRemovedNames(sectionBody) {
|
|
|
226
259
|
names.push(normalizeRequirementName(m[1]));
|
|
227
260
|
continue;
|
|
228
261
|
}
|
|
229
|
-
// Also support bullet list of headers
|
|
230
|
-
|
|
262
|
+
// Also support bullet list of headers. Every CommonMark bullet marker
|
|
263
|
+
// counts: `*` and `+` open a list exactly as `-` does, so accepting only
|
|
264
|
+
// `-` turned a removal written with either of them into a silent no-op -
|
|
265
|
+
// archive reported success while the requirement stayed in the spec.
|
|
266
|
+
const bullet = line.match(/^\s*[-*+]\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
231
267
|
if (bullet) {
|
|
232
268
|
names.push(normalizeRequirementName(bullet[1]));
|
|
233
269
|
}
|
|
234
270
|
}
|
|
235
271
|
return names;
|
|
236
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* `FROM:`/`TO:` rename pairs from `## RENAMED Requirements`, in document order.
|
|
275
|
+
*
|
|
276
|
+
* The bullet is optional, and every CommonMark bullet marker is accepted: a
|
|
277
|
+
* rename written with `*` or `+` used to match nothing at all, so the rename
|
|
278
|
+
* silently never happened while archive still reported success.
|
|
279
|
+
*/
|
|
237
280
|
function parseRenamedPairs(sectionBody) {
|
|
238
281
|
const { lines, fenceMask } = sectionBody;
|
|
239
282
|
if (lines.length === 0)
|
|
@@ -244,8 +287,11 @@ function parseRenamedPairs(sectionBody) {
|
|
|
244
287
|
if (fenceMask[i])
|
|
245
288
|
continue;
|
|
246
289
|
const line = lines[i];
|
|
247
|
-
|
|
248
|
-
|
|
290
|
+
// The bullet stays optional, and any CommonMark marker is accepted: a rename
|
|
291
|
+
// written with `*` or `+` used to match nothing at all, so the rename never
|
|
292
|
+
// happened while archive still reported success.
|
|
293
|
+
const fromMatch = line.match(/^\s*[-*+]?\s*FROM:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
294
|
+
const toMatch = line.match(/^\s*[-*+]?\s*TO:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
249
295
|
if (fromMatch) {
|
|
250
296
|
current.from = normalizeRequirementName(fromMatch[1]);
|
|
251
297
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE restart hint
|
|
3
|
+
*
|
|
4
|
+
* Shared restart guidance for tools successfully configured by init or update.
|
|
5
|
+
* The wording covers additions, updates, and removals, including an empty
|
|
6
|
+
* workflow selection that removes every generated file.
|
|
7
|
+
*/
|
|
8
|
+
import type { Delivery } from '../global-config.js';
|
|
9
|
+
/** The surface a restart hint names. Absent when no hint is due. */
|
|
10
|
+
export type IdeRestartSurface = 'commands' | 'skills';
|
|
11
|
+
/**
|
|
12
|
+
* Both conditions stay coupled to the SAME tool: its surfaces are loaded by a
|
|
13
|
+
* long-running editor process (a CLI picks them up immediately, so a restart
|
|
14
|
+
* line would be wrong for it — see #1067), and it supports a generated surface
|
|
15
|
+
* under the active delivery. A CLI tool's commands must not determine the hint
|
|
16
|
+
* for an IDE tool that only supports skills. Commands take precedence when
|
|
17
|
+
* both surfaces are supported under the active delivery.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveIdeRestartSurface(toolIds: readonly string[], delivery: Delivery): IdeRestartSurface | null;
|
|
20
|
+
/**
|
|
21
|
+
* The restart line to print, or null when no restart is needed. Deliberately
|
|
22
|
+
* not "slash commands": Amazon Q's generated files are prompt-library entries
|
|
23
|
+
* invoked with `@`, so promising slash commands would be wrong for it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatIdeRestart(toolIds: readonly string[], delivery: Delivery): string | null;
|
|
26
|
+
//# sourceMappingURL=ide-restart.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE restart hint
|
|
3
|
+
*
|
|
4
|
+
* Shared restart guidance for tools successfully configured by init or update.
|
|
5
|
+
* The wording covers additions, updates, and removals, including an empty
|
|
6
|
+
* workflow selection that removes every generated file.
|
|
7
|
+
*/
|
|
8
|
+
import { AI_TOOLS } from '../config.js';
|
|
9
|
+
import { shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, } from '../command-surface.js';
|
|
10
|
+
function isIdeResident(toolId) {
|
|
11
|
+
return Boolean(AI_TOOLS.find((tool) => tool.value === toolId)?.requiresIdeRestart);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Both conditions stay coupled to the SAME tool: its surfaces are loaded by a
|
|
15
|
+
* long-running editor process (a CLI picks them up immediately, so a restart
|
|
16
|
+
* line would be wrong for it — see #1067), and it supports a generated surface
|
|
17
|
+
* under the active delivery. A CLI tool's commands must not determine the hint
|
|
18
|
+
* for an IDE tool that only supports skills. Commands take precedence when
|
|
19
|
+
* both surfaces are supported under the active delivery.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveIdeRestartSurface(toolIds, delivery) {
|
|
22
|
+
const ideTools = [...new Set(toolIds)].filter(isIdeResident);
|
|
23
|
+
if (ideTools.some((toolId) => shouldGenerateCommandsForTool(toolId, delivery))) {
|
|
24
|
+
return 'commands';
|
|
25
|
+
}
|
|
26
|
+
if (ideTools.some((toolId) => shouldGenerateSkillsForTool(toolId, delivery))) {
|
|
27
|
+
return 'skills';
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The restart line to print, or null when no restart is needed. Deliberately
|
|
33
|
+
* not "slash commands": Amazon Q's generated files are prompt-library entries
|
|
34
|
+
* invoked with `@`, so promising slash commands would be wrong for it.
|
|
35
|
+
*/
|
|
36
|
+
export function formatIdeRestart(toolIds, delivery) {
|
|
37
|
+
const surface = resolveIdeRestartSurface(toolIds, delivery);
|
|
38
|
+
return surface
|
|
39
|
+
? `Restart your IDE to refresh ${surface}.`
|
|
40
|
+
: null;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=ide-restart.js.map
|
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
export { SKILL_NAMES, type SkillName, COMMAND_IDS, type CommandId, type ToolSkillStatus, type ToolVersionStatus, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
|
|
7
7
|
export { type SkillTemplateEntry, type CommandTemplateEntry, getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js';
|
|
8
8
|
export { type SkillCapableTool, toolSupportsSkills, getSkillCapableTools, hasGlobalSkillTarget, resolveToolSkillsDir, } from './skill-paths.js';
|
|
9
|
+
export { type IdeRestartSurface, resolveIdeRestartSurface, formatIdeRestart, } from './ide-restart.js';
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
export { SKILL_NAMES, COMMAND_IDS, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
|
|
7
7
|
export { getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js';
|
|
8
8
|
export { toolSupportsSkills, getSkillCapableTools, hasGlobalSkillTarget, resolveToolSkillsDir, } from './skill-paths.js';
|
|
9
|
+
export { resolveIdeRestartSurface, formatIdeRestart, } from './ide-restart.js';
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
|
@@ -253,7 +253,34 @@ export function getToolVersionStatus(projectRoot, toolId, currentVersion, option
|
|
|
253
253
|
generatedByVersion = currentVersion;
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
-
|
|
256
|
+
// 3. A version marker in a skill file only proves the SKILL files came from
|
|
257
|
+
// this CLI. It says nothing about the command files written beside them,
|
|
258
|
+
// which a user may have hand-edited or a partial write may have truncated.
|
|
259
|
+
// Without this, `update` answered "all tools up to date" while a damaged
|
|
260
|
+
// command file sat on disk, repairable only by knowing to pass --force.
|
|
261
|
+
// The content comparison already exists; it was simply never consulted
|
|
262
|
+
// once a skill file supplied a version.
|
|
263
|
+
//
|
|
264
|
+
// Scoped to tools that have BOTH, so the commands-only path above keeps
|
|
265
|
+
// its exact behaviour, and skipped when the delivery mode generates no
|
|
266
|
+
// commands for this tool - there would be nothing to compare against, and
|
|
267
|
+
// `areCommandFilesUpToDate` reports an empty command set as "not current".
|
|
268
|
+
let commandsDrifted = false;
|
|
269
|
+
if (skillConfigured && commandConfigured) {
|
|
270
|
+
let generatesCommands = true;
|
|
271
|
+
try {
|
|
272
|
+
generatesCommands = shouldGenerateCommandsForTool(toolId, getGlobalConfig().delivery ?? 'both');
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
generatesCommands = true;
|
|
276
|
+
}
|
|
277
|
+
commandsDrifted =
|
|
278
|
+
generatesCommands && !areCommandFilesUpToDate(projectRoot, toolId, options);
|
|
279
|
+
}
|
|
280
|
+
const needsUpdate = configured &&
|
|
281
|
+
(generatedByVersion === null ||
|
|
282
|
+
generatedByVersion !== currentVersion ||
|
|
283
|
+
commandsDrifted);
|
|
257
284
|
return {
|
|
258
285
|
toolId,
|
|
259
286
|
toolName: tool.name,
|