@fission-ai/openspec 1.7.0 → 1.8.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 -1
- package/dist/cli/index.js +6 -1
- package/dist/commands/change.js +15 -1
- package/dist/commands/schema.js +89 -20
- package/dist/commands/spec.js +30 -6
- package/dist/commands/validate.js +2 -2
- package/dist/commands/workflow/instructions.js +32 -27
- package/dist/commands/workflow/status.js +2 -2
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/core/archive.d.ts +14 -0
- package/dist/core/archive.js +1265 -151
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +3 -1
- package/dist/core/artifact-graph/instruction-loader.js +15 -5
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.js +35 -9
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +7 -0
- package/dist/core/change-status-policy.js +2 -2
- package/dist/core/completions/command-registry.js +8 -0
- package/dist/core/config-schema.d.ts +3 -0
- package/dist/core/config-schema.js +28 -1
- package/dist/core/config.d.ts +3 -0
- package/dist/core/config.js +24 -2
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +11 -0
- package/dist/core/init.d.ts +23 -0
- package/dist/core/init.js +234 -39
- package/dist/core/legacy-cleanup.js +4 -2
- package/dist/core/migration.d.ts +6 -4
- package/dist/core/migration.js +78 -22
- package/dist/core/parsers/requirement-blocks.d.ts +10 -0
- package/dist/core/parsers/requirement-blocks.js +60 -0
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +17 -1
- package/dist/core/profile-sync-drift.js +40 -5
- package/dist/core/project-config.d.ts +3 -0
- package/dist/core/project-config.js +31 -1
- package/dist/core/shared/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -0
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.js +66 -35
- package/dist/core/shared-skill-target.d.ts +16 -0
- package/dist/core/shared-skill-target.js +156 -0
- package/dist/core/specs-apply.d.ts +72 -0
- package/dist/core/specs-apply.js +448 -55
- package/dist/core/templates/skill-templates.d.ts +1 -1
- package/dist/core/templates/skill-templates.js +1 -1
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +22 -182
- package/dist/core/templates/workflows/archive-change.js +8 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +30 -26
- package/dist/core/templates/workflows/continue-change.js +6 -6
- package/dist/core/templates/workflows/explore.js +42 -18
- package/dist/core/templates/workflows/onboard.js +9 -3
- package/dist/core/templates/workflows/propose.js +72 -20
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +96 -18
- package/dist/core/templates/workflows/update-change.js +10 -8
- package/dist/core/update.d.ts +1 -0
- package/dist/core/update.js +103 -24
- package/dist/core/validation/validator.d.ts +23 -3
- package/dist/core/validation/validator.js +134 -18
- package/dist/core/version-check.js +6 -11
- package/dist/telemetry/config.d.ts +3 -4
- package/dist/telemetry/index.d.ts +9 -4
- package/dist/telemetry/index.js +20 -8
- package/dist/utils/change-metadata.d.ts +17 -4
- package/dist/utils/change-metadata.js +26 -4
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +14 -2
- package/dist/utils/command-references.js +45 -3
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/interactive.d.ts +23 -0
- package/dist/utils/interactive.js +32 -0
- package/dist/utils/spec-discovery.d.ts +4 -4
- package/dist/utils/spec-discovery.js +19 -6
- package/dist/utils/task-progress.d.ts +17 -0
- package/dist/utils/task-progress.js +41 -13
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +8 -6
- package/schemas/spec-driven/templates/proposal.md +6 -4
package/dist/core/init.js
CHANGED
|
@@ -12,19 +12,21 @@ import { createRequire } from 'module';
|
|
|
12
12
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
13
13
|
import { classifyOpenSpecDir, storePointerProblem } from './project-config.js';
|
|
14
14
|
import { findRepoPlanningRootSync } from './planning-home.js';
|
|
15
|
-
import { getSkillReferenceTransformer, getTransformerForTool } from '../utils/command-references.js';
|
|
15
|
+
import { getSkillReferenceTransformer, getTransformerForTool, usesNaturalLanguageSkillReferences } from '../utils/command-references.js';
|
|
16
16
|
import { AI_TOOLS, OPENSPEC_DIR_NAME, resolveToolIdAlias, } from './config.js';
|
|
17
17
|
import { PALETTE } from './styles/palette.js';
|
|
18
18
|
import { isInteractive } from '../utils/interactive.js';
|
|
19
19
|
import { serializeConfig } from './config-prompts.js';
|
|
20
20
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
21
21
|
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDeferredGlobalPromptSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, omitGlobalLegacyPromptFiles, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
22
|
-
import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, } from './shared/index.js';
|
|
22
|
+
import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/index.js';
|
|
23
23
|
import { getGlobalConfig } from './global-config.js';
|
|
24
24
|
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
|
|
25
25
|
import { getAvailableTools } from './available-tools.js';
|
|
26
|
+
import { writeSharedSkillTarget } from './shared-skill-target.js';
|
|
26
27
|
import { migrateIfNeeded, migrateLegacyToolDirs, describeLegacyMigration, keptInPlaceNotice, hasMovableContent, scanInstalledWorkflows as scanInstalledWorkflowsShared } from './migration.js';
|
|
27
28
|
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
29
|
+
import { writeCopilotCloudFiles, readCopilotCloudOptIn, hasExistingManagedCloudFiles, persistCopilotCloudOptIn, removeCopilotCloudFiles, findUnmanagedCloudFiles, listManagedCloudFiles, } from './github-copilot/cloud-agent.js';
|
|
28
30
|
const require = createRequire(import.meta.url);
|
|
29
31
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
30
32
|
// -----------------------------------------------------------------------------
|
|
@@ -58,12 +60,14 @@ export class InitCommand {
|
|
|
58
60
|
interactiveOption;
|
|
59
61
|
profileOverride;
|
|
60
62
|
animation;
|
|
63
|
+
copilotCloudOption;
|
|
61
64
|
constructor(options = {}) {
|
|
62
65
|
this.toolsArg = options.tools;
|
|
63
66
|
this.force = options.force ?? false;
|
|
64
67
|
this.interactiveOption = options.interactive;
|
|
65
68
|
this.profileOverride = options.profile;
|
|
66
69
|
this.animation = options.animation ?? true;
|
|
70
|
+
this.copilotCloudOption = options.copilotCloud;
|
|
67
71
|
}
|
|
68
72
|
async execute(targetPath) {
|
|
69
73
|
const projectPath = path.resolve(targetPath);
|
|
@@ -120,7 +124,7 @@ export class InitCommand {
|
|
|
120
124
|
// Get tool selection (pass detected tools for pre-selection)
|
|
121
125
|
const selectedToolIds = await this.getSelectedTools(toolStates, extendMode, detectedTools, projectPath);
|
|
122
126
|
// Validate selected tools
|
|
123
|
-
const validatedTools = this.validateTools(selectedToolIds, toolStates);
|
|
127
|
+
const validatedTools = this.validateTools(selectedToolIds, toolStates, projectPath);
|
|
124
128
|
// Selecting a renamed tool is consent to leave its former directory:
|
|
125
129
|
// init is about to write the current one, and leaving OpenSpec content
|
|
126
130
|
// behind would give the user two installs of the same tool.
|
|
@@ -132,10 +136,16 @@ export class InitCommand {
|
|
|
132
136
|
if (kept)
|
|
133
137
|
console.log(chalk.dim(kept));
|
|
134
138
|
}
|
|
139
|
+
// Decide whether to generate GitHub Copilot cloud files. This is opt-in
|
|
140
|
+
// (see cloud-agent.ts): selecting the Copilot tool no longer silently
|
|
141
|
+
// writes a GitHub Actions workflow into the user's .github/. The decision
|
|
142
|
+
// is made before generation so the write can be gated, and persisted after
|
|
143
|
+
// config.yaml exists so future non-interactive updates honor it.
|
|
144
|
+
const copilotDecision = await this.resolveCopilotCloudDecision(projectPath, validatedTools);
|
|
135
145
|
// Create directory structure and config
|
|
136
146
|
await this.createDirectoryStructure(openspecPath, extendMode);
|
|
137
147
|
// Generate skills and commands for each tool
|
|
138
|
-
const results = await this.generateSkillsAndCommands(projectPath, validatedTools);
|
|
148
|
+
const results = await this.generateSkillsAndCommands(projectPath, validatedTools, copilotDecision.write);
|
|
139
149
|
// Legacy cleanup was deferred to avoid interfering with skill/command generation;
|
|
140
150
|
// now that outputs are written, finalize the cleanup (e.g. remove stale files).
|
|
141
151
|
if (deferredLegacyCleanup) {
|
|
@@ -143,8 +153,49 @@ export class InitCommand {
|
|
|
143
153
|
}
|
|
144
154
|
// Create config.yaml if needed
|
|
145
155
|
const configStatus = await this.createConfig(openspecPath, extendMode);
|
|
156
|
+
// Persist an explicit Copilot cloud decision so `openspec update` (which
|
|
157
|
+
// never prompts) honors it. Best-effort: a config-write failure must not
|
|
158
|
+
// fail an otherwise-successful init.
|
|
159
|
+
if (copilotDecision.persist !== undefined) {
|
|
160
|
+
try {
|
|
161
|
+
await persistCopilotCloudOptIn(projectPath, copilotDecision.persist);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Non-fatal: the files (if any) were still written correctly.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// An explicit opt-out means "no cloud files here": clean up any that a
|
|
168
|
+
// previous run (or an older OpenSpec) generated. Only OpenSpec-managed
|
|
169
|
+
// files are removed — a user-customized file is preserved.
|
|
170
|
+
let copilotRemoved = 0;
|
|
171
|
+
if (copilotDecision.optedOut) {
|
|
172
|
+
try {
|
|
173
|
+
copilotRemoved = await removeCopilotCloudFiles(projectPath);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Non-fatal: removal targets files from a prior run; a failure here
|
|
177
|
+
// just leaves them for the next `openspec update` to clean up.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Report the cloud outcome from what is actually on disk after the write,
|
|
181
|
+
// not from the decision alone: writing over a user-owned file is a no-op,
|
|
182
|
+
// and the alternate-agent path can remove a managed file — so list only
|
|
183
|
+
// managed files that exist, and separately flag any left-untouched ones.
|
|
184
|
+
const copilotSucceeded = [...results.createdTools, ...results.refreshedTools].some((tool) => tool.value === 'github-copilot');
|
|
185
|
+
const wroteCloud = copilotDecision.write && copilotSucceeded;
|
|
186
|
+
const copilotPresent = wroteCloud ? await listManagedCloudFiles(projectPath) : [];
|
|
187
|
+
const copilotCollisions = wroteCloud ? await findUnmanagedCloudFiles(projectPath) : [];
|
|
146
188
|
// Display success message
|
|
147
|
-
this.displaySuccessMessage(projectPath, validatedTools, results, configStatus
|
|
189
|
+
this.displaySuccessMessage(projectPath, validatedTools, results, configStatus, {
|
|
190
|
+
write: copilotDecision.write,
|
|
191
|
+
skippedUndecided: copilotDecision.skippedUndecided,
|
|
192
|
+
present: copilotPresent,
|
|
193
|
+
collisions: copilotCollisions,
|
|
194
|
+
removed: copilotRemoved,
|
|
195
|
+
});
|
|
196
|
+
if (results.failedTools.length > 0) {
|
|
197
|
+
throw new Error(`OpenSpec setup failed for: ${results.failedTools.map((tool) => tool.name).join(', ')}`);
|
|
198
|
+
}
|
|
148
199
|
}
|
|
149
200
|
// ═══════════════════════════════════════════════════════════
|
|
150
201
|
// VALIDATION & SETUP
|
|
@@ -164,6 +215,59 @@ export class InitCommand {
|
|
|
164
215
|
return false;
|
|
165
216
|
return isInteractive({ interactive: this.interactiveOption });
|
|
166
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Decide whether to generate GitHub Copilot cloud files, and whether to
|
|
220
|
+
* persist that decision. Precedence:
|
|
221
|
+
* 1. `--copilot-cloud` / `--no-copilot-cloud` flag (explicit this run)
|
|
222
|
+
* 2. persisted opt-in in config.yaml
|
|
223
|
+
* 3. managed files already present (migration for pre-opt-in projects)
|
|
224
|
+
* 4. interactive confirm (default No)
|
|
225
|
+
* 5. non-interactive with no signal: skip, and don't persist a default
|
|
226
|
+
*
|
|
227
|
+
* @returns `write` — generate the files this run; `persist` — value to write
|
|
228
|
+
* back to config (undefined = leave config untouched); `optedOut` — the user
|
|
229
|
+
* explicitly declined, so any already-generated managed files should be
|
|
230
|
+
* removed; `skippedUndecided` — selected but no signal and couldn't ask, so
|
|
231
|
+
* the caller can hint that the opt-in exists.
|
|
232
|
+
*/
|
|
233
|
+
async resolveCopilotCloudDecision(projectPath, tools) {
|
|
234
|
+
const copilotSelected = tools.some((tool) => tool.value === 'github-copilot');
|
|
235
|
+
if (!copilotSelected) {
|
|
236
|
+
// A flag that can't apply is a likely mistake — say so rather than no-op.
|
|
237
|
+
if (this.copilotCloudOption !== undefined) {
|
|
238
|
+
console.log(chalk.yellow('--copilot-cloud/--no-copilot-cloud was ignored because the github-copilot tool was not selected.'));
|
|
239
|
+
}
|
|
240
|
+
return { write: false, optedOut: false, skippedUndecided: false };
|
|
241
|
+
}
|
|
242
|
+
if (this.copilotCloudOption !== undefined) {
|
|
243
|
+
return {
|
|
244
|
+
write: this.copilotCloudOption,
|
|
245
|
+
persist: this.copilotCloudOption,
|
|
246
|
+
optedOut: !this.copilotCloudOption,
|
|
247
|
+
skippedUndecided: false,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const persistedOptIn = readCopilotCloudOptIn(projectPath);
|
|
251
|
+
if (typeof persistedOptIn === 'boolean') {
|
|
252
|
+
return { write: persistedOptIn, optedOut: !persistedOptIn, skippedUndecided: false };
|
|
253
|
+
}
|
|
254
|
+
if (await hasExistingManagedCloudFiles(projectPath)) {
|
|
255
|
+
return { write: true, optedOut: false, skippedUndecided: false };
|
|
256
|
+
}
|
|
257
|
+
if (this.canPromptInteractively()) {
|
|
258
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
259
|
+
const answer = await confirm({
|
|
260
|
+
message: 'Set up GitHub Copilot cloud coding-agent files? This is for the GitHub-hosted ' +
|
|
261
|
+
'Copilot coding agent (github.com), not Copilot in your editor. It writes two files: ' +
|
|
262
|
+
'.github/workflows/copilot-setup-steps.yml and .github/agents/openspec.agent.md.',
|
|
263
|
+
default: false,
|
|
264
|
+
});
|
|
265
|
+
return { write: answer, persist: answer, optedOut: !answer, skippedUndecided: false };
|
|
266
|
+
}
|
|
267
|
+
// Non-interactive with no explicit signal: don't write, and leave the
|
|
268
|
+
// decision unpersisted so a later interactive run can still prompt.
|
|
269
|
+
return { write: false, optedOut: false, skippedUndecided: true };
|
|
270
|
+
}
|
|
167
271
|
resolveProfileOverride() {
|
|
168
272
|
if (this.profileOverride === undefined) {
|
|
169
273
|
return undefined;
|
|
@@ -411,23 +515,34 @@ export class InitCommand {
|
|
|
411
515
|
}
|
|
412
516
|
return deduped;
|
|
413
517
|
}
|
|
414
|
-
validateTools(toolIds, toolStates) {
|
|
518
|
+
validateTools(toolIds, toolStates, projectPath) {
|
|
415
519
|
const validatedTools = [];
|
|
416
|
-
|
|
520
|
+
const reconciledToolIds = toolIds.includes('codex') && toolIds.includes('agents')
|
|
521
|
+
? toolIds.filter((toolId) => toolId !== 'agents')
|
|
522
|
+
: toolIds;
|
|
523
|
+
if (reconciledToolIds.length !== toolIds.length) {
|
|
524
|
+
console.log(chalk.dim('Codex and agents share .agents/skills; writing one tree with Codex and generic skill references.'));
|
|
525
|
+
}
|
|
526
|
+
for (const toolId of reconciledToolIds) {
|
|
417
527
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
418
528
|
if (!tool) {
|
|
419
529
|
const validToolIds = getToolsWithSkillsDir();
|
|
420
530
|
throw new Error(`Unknown tool '${toolId}'. Valid tools:\n ${validToolIds.join('\n ')}`);
|
|
421
531
|
}
|
|
422
|
-
if (!tool
|
|
532
|
+
if (!toolSupportsSkills(tool)) {
|
|
423
533
|
const validToolsWithSkills = getToolsWithSkillsDir();
|
|
424
534
|
throw new Error(`Tool '${toolId}' does not support skill generation.\nTools with skill generation support:\n ${validToolsWithSkills.join('\n ')}`);
|
|
425
535
|
}
|
|
426
536
|
const preState = toolStates.get(tool.value);
|
|
537
|
+
const skillsPath = resolveToolSkillsDir(projectPath, tool);
|
|
538
|
+
const isGlobalSkillTarget = hasGlobalSkillTarget(tool);
|
|
427
539
|
validatedTools.push({
|
|
428
540
|
value: tool.value,
|
|
429
541
|
name: tool.name,
|
|
430
542
|
skillsDir: tool.skillsDir,
|
|
543
|
+
skillsPath,
|
|
544
|
+
skillsRoot: isGlobalSkillTarget ? skillsPath : projectPath,
|
|
545
|
+
isGlobalSkillTarget,
|
|
431
546
|
wasConfigured: preState?.configured ?? false,
|
|
432
547
|
});
|
|
433
548
|
}
|
|
@@ -446,6 +561,7 @@ export class InitCommand {
|
|
|
446
561
|
path.join(openspecPath, 'changes', 'archive'),
|
|
447
562
|
];
|
|
448
563
|
for (const dir of directories) {
|
|
564
|
+
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
|
|
449
565
|
await FileSystemUtils.createDirectory(dir);
|
|
450
566
|
}
|
|
451
567
|
return;
|
|
@@ -458,6 +574,7 @@ export class InitCommand {
|
|
|
458
574
|
path.join(openspecPath, 'changes', 'archive'),
|
|
459
575
|
];
|
|
460
576
|
for (const dir of directories) {
|
|
577
|
+
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
|
|
461
578
|
await FileSystemUtils.createDirectory(dir);
|
|
462
579
|
}
|
|
463
580
|
spinner.stopAndPersist({
|
|
@@ -476,7 +593,7 @@ export class InitCommand {
|
|
|
476
593
|
* @param tools - Selected tools with their skill directory metadata
|
|
477
594
|
* @returns Created, refreshed, and failed tools plus removed artifact counts
|
|
478
595
|
*/
|
|
479
|
-
async generateSkillsAndCommands(projectPath, tools) {
|
|
596
|
+
async generateSkillsAndCommands(projectPath, tools, writeCopilotCloud) {
|
|
480
597
|
const createdTools = [];
|
|
481
598
|
const refreshedTools = [];
|
|
482
599
|
const failedTools = [];
|
|
@@ -501,22 +618,24 @@ export class InitCommand {
|
|
|
501
618
|
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
502
619
|
// Generate skill files if the selected delivery and tool capability allow skills
|
|
503
620
|
if (shouldGenerateSkills) {
|
|
504
|
-
// Use tool-specific skillsDir
|
|
505
|
-
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
506
621
|
// Create skill directories and SKILL.md files
|
|
507
622
|
for (const { template, dirName } of skillTemplates) {
|
|
508
|
-
const skillDir = path.join(
|
|
623
|
+
const skillDir = path.join(tool.skillsPath, dirName);
|
|
509
624
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
510
625
|
// Generate SKILL.md content with YAML frontmatter including generatedBy
|
|
511
626
|
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
512
627
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
513
628
|
// Write the skill file
|
|
629
|
+
FileSystemUtils.assertPathWithin(tool.skillsRoot, skillFile);
|
|
514
630
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
515
631
|
}
|
|
632
|
+
writeSharedSkillTarget(projectPath, tool.value);
|
|
516
633
|
}
|
|
517
|
-
if (shouldRemoveSkillsForTool(tool.value, delivery)) {
|
|
518
|
-
|
|
519
|
-
|
|
634
|
+
if (shouldRemoveSkillsForTool(tool.value, delivery) && !tool.isGlobalSkillTarget) {
|
|
635
|
+
removedSkillCount += await this.removeSkillDirs(tool.skillsRoot, tool.skillsPath);
|
|
636
|
+
// Retain an explicit selection even when this delivery mode produces
|
|
637
|
+
// no skills, so a divergent legacy sibling cannot reclaim ownership.
|
|
638
|
+
writeSharedSkillTarget(projectPath, tool.value);
|
|
520
639
|
}
|
|
521
640
|
// Generate commands if delivery includes commands
|
|
522
641
|
if (shouldGenerateCommands) {
|
|
@@ -524,7 +643,7 @@ export class InitCommand {
|
|
|
524
643
|
if (adapter) {
|
|
525
644
|
const generatedCommands = generateCommands(commandContents, adapter);
|
|
526
645
|
for (const cmd of generatedCommands) {
|
|
527
|
-
const commandFile =
|
|
646
|
+
const commandFile = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmd.path);
|
|
528
647
|
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
529
648
|
}
|
|
530
649
|
}
|
|
@@ -540,6 +659,9 @@ export class InitCommand {
|
|
|
540
659
|
if (shouldReconcileCommandFilesForTool(tool.value, delivery)) {
|
|
541
660
|
removedCommandCount += await this.removeCommandFiles(projectPath, tool.value);
|
|
542
661
|
}
|
|
662
|
+
if (tool.value === 'github-copilot' && writeCopilotCloud) {
|
|
663
|
+
await writeCopilotCloudFiles(projectPath);
|
|
664
|
+
}
|
|
543
665
|
spinner.succeed(`Setup complete for ${tool.name}`);
|
|
544
666
|
if (tool.wasConfigured) {
|
|
545
667
|
refreshedTools.push(tool);
|
|
@@ -553,6 +675,16 @@ export class InitCommand {
|
|
|
553
675
|
failedTools.push({ name: tool.name, error: error });
|
|
554
676
|
}
|
|
555
677
|
}
|
|
678
|
+
for (const tool of [...createdTools, ...refreshedTools]) {
|
|
679
|
+
for (const migration of migrateLegacyToolDirs(projectPath, [tool.value], 'after-generation')) {
|
|
680
|
+
if (hasMovableContent(migration)) {
|
|
681
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
682
|
+
}
|
|
683
|
+
const kept = keptInPlaceNotice(migration);
|
|
684
|
+
if (kept)
|
|
685
|
+
console.log(chalk.dim(kept));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
556
688
|
return {
|
|
557
689
|
createdTools,
|
|
558
690
|
refreshedTools,
|
|
@@ -576,6 +708,7 @@ export class InitCommand {
|
|
|
576
708
|
}
|
|
577
709
|
try {
|
|
578
710
|
const yamlContent = serializeConfig({ schema: DEFAULT_SCHEMA });
|
|
711
|
+
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), configPath);
|
|
579
712
|
await FileSystemUtils.writeFile(configPath, yamlContent);
|
|
580
713
|
return 'created';
|
|
581
714
|
}
|
|
@@ -586,9 +719,9 @@ export class InitCommand {
|
|
|
586
719
|
// ═══════════════════════════════════════════════════════════
|
|
587
720
|
// UI & OUTPUT
|
|
588
721
|
// ═══════════════════════════════════════════════════════════
|
|
589
|
-
displaySuccessMessage(projectPath, tools, results, configStatus) {
|
|
722
|
+
displaySuccessMessage(projectPath, tools, results, configStatus, copilot) {
|
|
590
723
|
console.log();
|
|
591
|
-
console.log(chalk.bold('OpenSpec Setup Complete'));
|
|
724
|
+
console.log(chalk.bold(results.failedTools.length > 0 ? 'OpenSpec Setup Incomplete' : 'OpenSpec Setup Complete'));
|
|
592
725
|
console.log();
|
|
593
726
|
// Show created vs refreshed tools
|
|
594
727
|
if (results.createdTools.length > 0) {
|
|
@@ -604,21 +737,56 @@ export class InitCommand {
|
|
|
604
737
|
const profile = this.profileOverride ?? globalConfig.profile ?? 'core';
|
|
605
738
|
const delivery = globalConfig.delivery ?? 'both';
|
|
606
739
|
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
740
|
+
const usesGlobalSkillTarget = successfulTools.some((tool) => tool.isGlobalSkillTarget);
|
|
741
|
+
if (!usesGlobalSkillTarget) {
|
|
742
|
+
const toolDirs = [
|
|
743
|
+
...new Set(successfulTools
|
|
744
|
+
.map((tool) => tool.skillsDir)
|
|
745
|
+
.filter((skillsDir) => Boolean(skillsDir))),
|
|
746
|
+
].join(', ');
|
|
747
|
+
const skillCount = successfulTools.some((tool) => shouldGenerateSkillsForTool(tool.value, delivery))
|
|
748
|
+
? getSkillTemplates(workflows).length
|
|
749
|
+
: 0;
|
|
750
|
+
const commandCount = successfulTools.some((tool) => shouldGenerateCommandsForTool(tool.value, delivery))
|
|
751
|
+
? getCommandContents(workflows).length
|
|
752
|
+
: 0;
|
|
753
|
+
if (skillCount > 0 && commandCount > 0) {
|
|
754
|
+
console.log(`${skillCount} skills and ${commandCount} commands in ${toolDirs}/`);
|
|
755
|
+
}
|
|
756
|
+
else if (skillCount > 0) {
|
|
757
|
+
console.log(`${skillCount} skills in ${toolDirs}/`);
|
|
758
|
+
}
|
|
759
|
+
else if (commandCount > 0) {
|
|
760
|
+
console.log(`${commandCount} commands in ${toolDirs}/`);
|
|
761
|
+
}
|
|
619
762
|
}
|
|
620
|
-
else
|
|
621
|
-
|
|
763
|
+
else {
|
|
764
|
+
const skillTools = successfulTools.filter((tool) => shouldGenerateSkillsForTool(tool.value, delivery));
|
|
765
|
+
const skillCount = skillTools.length * getSkillTemplates(workflows).length;
|
|
766
|
+
if (skillCount > 0) {
|
|
767
|
+
const skillDirs = [...new Set(skillTools.map((tool) => tool.skillsPath))];
|
|
768
|
+
console.log(`${skillCount} skills in ${skillDirs.join(', ')}`);
|
|
769
|
+
}
|
|
770
|
+
const commandContents = getCommandContents(workflows);
|
|
771
|
+
const commandTools = successfulTools.filter((tool) => shouldGenerateCommandsForTool(tool.value, delivery));
|
|
772
|
+
const commandCount = commandTools.length * commandContents.length;
|
|
773
|
+
if (commandCount > 0) {
|
|
774
|
+
const commandDirs = [
|
|
775
|
+
...new Set(commandTools.flatMap((tool) => {
|
|
776
|
+
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
777
|
+
if (!adapter)
|
|
778
|
+
return [];
|
|
779
|
+
return commandContents.map((command) => {
|
|
780
|
+
const commandPath = adapter.getFilePath(command.id);
|
|
781
|
+
const absolutePath = path.isAbsolute(commandPath)
|
|
782
|
+
? commandPath
|
|
783
|
+
: path.join(projectPath, commandPath);
|
|
784
|
+
return path.dirname(absolutePath);
|
|
785
|
+
});
|
|
786
|
+
})),
|
|
787
|
+
];
|
|
788
|
+
console.log(`${commandCount} commands in ${commandDirs.join(', ')}`);
|
|
789
|
+
}
|
|
622
790
|
}
|
|
623
791
|
}
|
|
624
792
|
// Show failures
|
|
@@ -638,6 +806,26 @@ export class InitCommand {
|
|
|
638
806
|
if (results.removedSkillCount > 0) {
|
|
639
807
|
console.log(chalk.dim(`Removed: ${results.removedSkillCount} skill directories (delivery: commands)`));
|
|
640
808
|
}
|
|
809
|
+
// GitHub Copilot cloud files are opt-in — report what is actually on disk:
|
|
810
|
+
// list the managed files that now exist (never files we didn't write), flag
|
|
811
|
+
// any user-owned file we left untouched, note an opt-out cleanup, or (when
|
|
812
|
+
// skipped for want of a signal) say how to turn them on.
|
|
813
|
+
const copilotSucceeded = successfulTools.some((tool) => tool.value === 'github-copilot');
|
|
814
|
+
if (copilotSucceeded && copilot.write) {
|
|
815
|
+
if (copilot.present.length > 0) {
|
|
816
|
+
console.log(`GitHub Copilot cloud files: ${copilot.present.join(', ')}`);
|
|
817
|
+
}
|
|
818
|
+
if (copilot.collisions.length > 0) {
|
|
819
|
+
console.log(chalk.dim(`Left your existing ${copilot.collisions.join(' and ')} untouched — add the OpenSpec ` +
|
|
820
|
+
`install step by hand so the Copilot cloud agent can run openspec.`));
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
else if (copilotSucceeded && copilot.removed > 0) {
|
|
824
|
+
console.log(chalk.dim(`Removed: ${copilot.removed} Copilot cloud agent file(s) (opted out of cloud files)`));
|
|
825
|
+
}
|
|
826
|
+
else if (copilotSucceeded && copilot.skippedUndecided) {
|
|
827
|
+
console.log(chalk.dim("Skipped GitHub Copilot cloud files (opt-in). Enable with 'openspec init --copilot-cloud'."));
|
|
828
|
+
}
|
|
641
829
|
// Show manual setup notes for tools that need extra configuration
|
|
642
830
|
for (const tool of successfulTools) {
|
|
643
831
|
const setupNote = AI_TOOLS.find((t) => t.value === tool.value)?.setupNote;
|
|
@@ -684,7 +872,13 @@ export class InitCommand {
|
|
|
684
872
|
hint = `Start your first change: ${transformer ? transformer(command) : command} "your idea"`;
|
|
685
873
|
}
|
|
686
874
|
else if (shouldGenerateSkillsForTool(tool.value, activeDelivery)) {
|
|
687
|
-
|
|
875
|
+
const skillReference = getSkillReferenceTransformer(tool.value)(command);
|
|
876
|
+
// Tools with no slash surface (e.g. Rovo Dev) reference skills as
|
|
877
|
+
// prose ("the openspec-propose skill"); phrase the hint so it reads
|
|
878
|
+
// as an instruction rather than a dead command with an argument.
|
|
879
|
+
hint = usesNaturalLanguageSkillReferences(tool.value)
|
|
880
|
+
? `Start your first change: ask ${tool.name} to use ${skillReference} with "your idea"`
|
|
881
|
+
: `Start your first change: ${skillReference} "your idea"`;
|
|
688
882
|
}
|
|
689
883
|
else {
|
|
690
884
|
continue;
|
|
@@ -757,18 +951,19 @@ export class InitCommand {
|
|
|
757
951
|
spinner: PROGRESS_SPINNER,
|
|
758
952
|
}).start();
|
|
759
953
|
}
|
|
760
|
-
async removeSkillDirs(skillsDir) {
|
|
954
|
+
async removeSkillDirs(skillsRoot, skillsDir) {
|
|
761
955
|
let removed = 0;
|
|
762
956
|
for (const workflow of ALL_WORKFLOWS) {
|
|
763
957
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
764
958
|
if (!dirName)
|
|
765
959
|
continue;
|
|
766
960
|
const skillDir = path.join(skillsDir, dirName);
|
|
961
|
+
if (!fs.existsSync(skillDir))
|
|
962
|
+
continue;
|
|
963
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
|
|
767
964
|
try {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
removed++;
|
|
771
|
-
}
|
|
965
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
966
|
+
removed++;
|
|
772
967
|
}
|
|
773
968
|
catch {
|
|
774
969
|
// Ignore errors
|
|
@@ -783,7 +978,7 @@ export class InitCommand {
|
|
|
783
978
|
return 0;
|
|
784
979
|
for (const workflow of ALL_WORKFLOWS) {
|
|
785
980
|
const cmdPath = adapter.getFilePath(workflow);
|
|
786
|
-
const fullPath =
|
|
981
|
+
const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
|
|
787
982
|
try {
|
|
788
983
|
if (fs.existsSync(fullPath)) {
|
|
789
984
|
await fs.promises.unlink(fullPath);
|
|
@@ -35,7 +35,6 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
|
35
35
|
'lingma': { type: 'directory', path: '.lingma/commands/openspec' },
|
|
36
36
|
'crush': { type: 'directory', path: '.crush/commands/openspec' },
|
|
37
37
|
'gemini': { type: 'directory', path: '.gemini/commands/openspec' },
|
|
38
|
-
'costrict': { type: 'directory', path: '.cospec/openspec/commands' },
|
|
39
38
|
// File-based: individual openspec-*.md files in a commands/workflows/prompts folder
|
|
40
39
|
'cursor': { type: 'files', pattern: '.cursor/commands/openspec-*.md' },
|
|
41
40
|
// Keyed by the tool id these map back to, so the pre-opsx Windsurf files
|
|
@@ -54,9 +53,12 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
|
54
53
|
'continue': { type: 'files', pattern: '.continue/prompts/openspec-*.prompt' },
|
|
55
54
|
'antigravity': { type: 'files', pattern: '.agent/workflows/openspec-*.md' },
|
|
56
55
|
'iflow': { type: 'files', pattern: '.iflow/commands/openspec-*.md' },
|
|
57
|
-
'junie': { type: 'files', pattern: ['.junie/commands/opsx-*.md', '.junie/commands/openspec-*.md'] },
|
|
58
56
|
'qwen': { type: 'files', pattern: ['.qwen/commands/opsx-*.toml', '.qwen/commands/openspec-*.toml'] },
|
|
59
57
|
'codex': { type: 'files', pattern: '.codex/prompts/openspec-*.md' },
|
|
58
|
+
// Keep this file-scoped: the CoStrict adapter writes `opsx-*.md` into the
|
|
59
|
+
// same folder, so a directory entry removes the live command files — and
|
|
60
|
+
// anything else the user keeps there — on every run.
|
|
61
|
+
'costrict': { type: 'files', pattern: '.cospec/openspec/commands/openspec-*.md' },
|
|
60
62
|
};
|
|
61
63
|
/**
|
|
62
64
|
* Final OpenSpec-managed global Codex prompt filenames mapped to the workflows
|
package/dist/core/migration.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface LegacyToolRoot {
|
|
|
14
14
|
* location may still be the live one for somebody.
|
|
15
15
|
*/
|
|
16
16
|
needsConsent: boolean;
|
|
17
|
+
/** Migrations that need a freshly generated destination run afterward. */
|
|
18
|
+
timing?: 'before-generation' | 'after-generation';
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* Former tool roots whose OpenSpec-managed content belongs under the tool's
|
|
@@ -32,8 +34,8 @@ export interface LegacyToolMigration {
|
|
|
32
34
|
commandFiles: number;
|
|
33
35
|
/**
|
|
34
36
|
* OpenSpec-managed files left under the legacy root because the copy there
|
|
35
|
-
* differs from the one that survives
|
|
36
|
-
*
|
|
37
|
+
* differs materially from the one that survives, so it is reported rather
|
|
38
|
+
* than dropped.
|
|
37
39
|
*/
|
|
38
40
|
keptInPlace: number;
|
|
39
41
|
/** Whether this move needs the user's consent first */
|
|
@@ -43,7 +45,7 @@ export interface LegacyToolMigration {
|
|
|
43
45
|
* Reports the OpenSpec content sitting under each tool's legacy root, without
|
|
44
46
|
* moving anything. Callers use this to ask before a move that needs consent.
|
|
45
47
|
*/
|
|
46
|
-
export declare function findLegacyToolMigrations(projectPath: string): LegacyToolMigration[];
|
|
48
|
+
export declare function findLegacyToolMigrations(projectPath: string, timing?: 'before-generation' | 'after-generation'): LegacyToolMigration[];
|
|
47
49
|
/**
|
|
48
50
|
* Moves OpenSpec-managed skill directories (openspec-*) and command files
|
|
49
51
|
* (opsx-*) from a tool's legacy root to its current one. When the destination
|
|
@@ -55,7 +57,7 @@ export declare function findLegacyToolMigrations(projectPath: string): LegacyToo
|
|
|
55
57
|
* @param toolIds - Restrict the move to these tools; omit to move every tool
|
|
56
58
|
* whose legacy root needs no consent
|
|
57
59
|
*/
|
|
58
|
-
export declare function migrateLegacyToolDirs(projectPath: string, toolIds?: string[]): LegacyToolMigration[];
|
|
60
|
+
export declare function migrateLegacyToolDirs(projectPath: string, toolIds?: string[], timing?: 'before-generation' | 'after-generation'): LegacyToolMigration[];
|
|
59
61
|
/**
|
|
60
62
|
* Summarizes what a migration moved, e.g. "6 skills and 6 commands".
|
|
61
63
|
*/
|