@fission-ai/openspec 1.6.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 +31 -2
- package/dist/cli/index.js +79 -8
- package/dist/commands/change.d.ts +0 -1
- package/dist/commands/change.js +105 -51
- package/dist/commands/config.js +14 -8
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +74 -23
- package/dist/commands/schema.js +110 -31
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +49 -29
- package/dist/commands/validate.js +20 -5
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.js +121 -33
- package/dist/commands/workflow/shared.d.ts +13 -2
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +9 -4
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/core/archive.d.ts +14 -1
- package/dist/core/archive.js +1332 -155
- package/dist/core/artifact-graph/graph.d.ts +16 -0
- package/dist/core/artifact-graph/graph.js +30 -6
- 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 +30 -3
- package/dist/core/artifact-graph/instruction-loader.js +70 -12
- 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.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +69 -15
- 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 +2 -0
- package/dist/core/change-metadata/schema.js +13 -0
- package/dist/core/change-status-policy.d.ts +1 -1
- package/dist/core/change-status-policy.js +2 -2
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +7 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -1
- package/dist/core/command-generation/adapters/auggie.js +2 -1
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +6 -5
- package/dist/core/command-generation/adapters/claude.js +1 -8
- package/dist/core/command-generation/adapters/codebuddy.js +3 -2
- package/dist/core/command-generation/adapters/continue.js +3 -2
- package/dist/core/command-generation/adapters/costrict.js +2 -1
- package/dist/core/command-generation/adapters/crush.js +5 -5
- package/dist/core/command-generation/adapters/cursor.js +2 -2
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/factory.js +2 -1
- package/dist/core/command-generation/adapters/gemini.js +37 -2
- package/dist/core/command-generation/adapters/github-copilot.js +2 -1
- package/dist/core/command-generation/adapters/iflow.js +5 -4
- package/dist/core/command-generation/adapters/index.d.ts +2 -2
- package/dist/core/command-generation/adapters/index.js +2 -2
- package/dist/core/command-generation/adapters/junie.js +2 -1
- package/dist/core/command-generation/adapters/kiro.js +2 -1
- package/dist/core/command-generation/adapters/lingma.js +5 -5
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
- package/dist/core/command-generation/adapters/opencode.js +3 -5
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +3 -6
- package/dist/core/command-generation/adapters/qoder.js +5 -5
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +12 -7
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +4 -4
- package/dist/core/command-generation/adapters/trae.js +1 -21
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +8 -0
- package/dist/core/command-generation/generator.js +15 -1
- package/dist/core/command-generation/index.d.ts +1 -1
- package/dist/core/command-generation/index.js +1 -1
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.js +4 -4
- package/dist/core/command-generation/types.d.ts +9 -2
- package/dist/core/command-generation/yaml.d.ts +20 -6
- package/dist/core/command-generation/yaml.js +31 -21
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/completions/command-registry.js +16 -2
- package/dist/core/completions/generators/powershell-generator.js +15 -8
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +10 -1
- package/dist/core/config-schema.js +67 -1
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +45 -5
- 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 +16 -0
- package/dist/core/init.d.ts +58 -0
- package/dist/core/init.js +450 -69
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +253 -24
- package/dist/core/list.js +5 -7
- package/dist/core/migration.d.ts +73 -1
- package/dist/core/migration.js +360 -11
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +33 -20
- package/dist/core/parsers/code-fence.d.ts +15 -0
- package/dist/core/parsers/code-fence.js +49 -0
- package/dist/core/parsers/markdown-parser.js +2 -1
- package/dist/core/parsers/requirement-blocks.d.ts +18 -0
- package/dist/core/parsers/requirement-blocks.js +120 -35
- package/dist/core/parsers/requirement-text.d.ts +1 -8
- package/dist/core/parsers/requirement-text.js +4 -42
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +20 -30
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +48 -49
- package/dist/core/project-config.d.ts +28 -6
- package/dist/core/project-config.js +108 -8
- package/dist/core/references.js +42 -4
- package/dist/core/relationship-health.d.ts +8 -0
- package/dist/core/relationship-health.js +13 -0
- package/dist/core/root-selection.d.ts +9 -6
- package/dist/core/root-selection.js +42 -5
- 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.d.ts +25 -5
- package/dist/core/shared/tool-detection.js +166 -36
- 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 +80 -33
- package/dist/core/specs-apply.js +659 -171
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- 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 +48 -158
- package/dist/core/templates/workflows/archive-change.js +170 -42
- package/dist/core/templates/workflows/bulk-archive-change.js +256 -75
- package/dist/core/templates/workflows/continue-change.js +38 -46
- package/dist/core/templates/workflows/explore.js +54 -18
- package/dist/core/templates/workflows/ff-change.js +46 -30
- package/dist/core/templates/workflows/new-change.js +2 -2
- package/dist/core/templates/workflows/onboard.js +13 -7
- package/dist/core/templates/workflows/propose.js +116 -46
- 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 +260 -32
- package/dist/core/templates/workflows/update-change.js +26 -16
- package/dist/core/templates/workflows/verify-change.js +16 -10
- package/dist/core/update.d.ts +40 -5
- package/dist/core/update.js +389 -93
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/validator.d.ts +22 -8
- package/dist/core/validation/validator.js +222 -50
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +731 -0
- package/dist/core/view.js +12 -17
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/config.d.ts +3 -4
- package/dist/telemetry/index.d.ts +10 -5
- package/dist/telemetry/index.js +75 -41
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +43 -0
- package/dist/utils/change-metadata.js +123 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +92 -6
- package/dist/utils/command-references.js +180 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/interactive.d.ts +23 -0
- package/dist/utils/interactive.js +32 -0
- package/dist/utils/item-discovery.d.ts +19 -0
- package/dist/utils/item-discovery.js +30 -48
- package/dist/utils/shell-detection.d.ts +1 -1
- package/dist/utils/shell-detection.js +64 -11
- package/dist/utils/spec-discovery.d.ts +40 -0
- package/dist/utils/spec-discovery.js +115 -0
- package/dist/utils/task-progress.d.ts +17 -0
- package/dist/utils/task-progress.js +41 -13
- package/package.json +14 -12
- package/schemas/spec-driven/schema.yaml +67 -10
- package/schemas/spec-driven/templates/design.md +2 -2
- package/schemas/spec-driven/templates/proposal.md +10 -4
- package/schemas/spec-driven/templates/spec.md +3 -0
- package/dist/core/command-generation/adapters/codex.d.ts +0 -16
- package/dist/core/command-generation/adapters/codex.js +0 -39
- package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
- package/dist/core/command-generation/adapters/windsurf.js +0 -38
package/dist/core/init.js
CHANGED
|
@@ -12,18 +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 {
|
|
16
|
-
import { AI_TOOLS, OPENSPEC_DIR_NAME, } from './config.js';
|
|
15
|
+
import { getSkillReferenceTransformer, getTransformerForTool, usesNaturalLanguageSkillReferences } from '../utils/command-references.js';
|
|
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
|
-
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, } from './legacy-cleanup.js';
|
|
22
|
-
import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, } from './shared/index.js';
|
|
21
|
+
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
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 {
|
|
26
|
+
import { writeSharedSkillTarget } from './shared-skill-target.js';
|
|
27
|
+
import { migrateIfNeeded, migrateLegacyToolDirs, describeLegacyMigration, keptInPlaceNotice, hasMovableContent, scanInstalledWorkflows as scanInstalledWorkflowsShared } from './migration.js';
|
|
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';
|
|
27
30
|
const require = createRequire(import.meta.url);
|
|
28
31
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
29
32
|
// -----------------------------------------------------------------------------
|
|
@@ -56,11 +59,15 @@ export class InitCommand {
|
|
|
56
59
|
force;
|
|
57
60
|
interactiveOption;
|
|
58
61
|
profileOverride;
|
|
62
|
+
animation;
|
|
63
|
+
copilotCloudOption;
|
|
59
64
|
constructor(options = {}) {
|
|
60
65
|
this.toolsArg = options.tools;
|
|
61
66
|
this.force = options.force ?? false;
|
|
62
67
|
this.interactiveOption = options.interactive;
|
|
63
68
|
this.profileOverride = options.profile;
|
|
69
|
+
this.animation = options.animation ?? true;
|
|
70
|
+
this.copilotCloudOption = options.copilotCloud;
|
|
64
71
|
}
|
|
65
72
|
async execute(targetPath) {
|
|
66
73
|
const projectPath = path.resolve(targetPath);
|
|
@@ -91,36 +98,104 @@ export class InitCommand {
|
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
// Check for legacy artifacts and handle cleanup
|
|
94
|
-
await this.handleLegacyCleanup(projectPath, extendMode);
|
|
101
|
+
const deferredLegacyCleanup = await this.handleLegacyCleanup(projectPath, extendMode);
|
|
102
|
+
// Migrate OpenSpec-managed skills left in renamed tool directories
|
|
103
|
+
// (e.g. .kimi -> .kimi-code) before detection so they stay recognized.
|
|
104
|
+
migrateLegacyToolDirs(projectPath);
|
|
95
105
|
// Detect available tools in the project (task 7.1)
|
|
96
106
|
const detectedTools = getAvailableTools(projectPath);
|
|
97
107
|
// Migration check: migrate existing projects to profile system (task 7.3)
|
|
98
108
|
if (extendMode) {
|
|
99
109
|
migrateIfNeeded(projectPath, detectedTools);
|
|
100
110
|
}
|
|
111
|
+
// Validate profile override early so invalid values fail before tool setup.
|
|
112
|
+
// The resolved value is consumed later when generation reads effective config.
|
|
113
|
+
// This runs ahead of the welcome screen so an invalid --profile does not make
|
|
114
|
+
// the user press Enter before seeing the error.
|
|
115
|
+
this.resolveProfileOverride();
|
|
101
116
|
// Show animated welcome screen (interactive mode only)
|
|
102
117
|
const canPrompt = this.canPromptInteractively();
|
|
103
118
|
if (canPrompt) {
|
|
104
119
|
const { showWelcomeScreen } = await import('../ui/welcome-screen.js');
|
|
105
|
-
await showWelcomeScreen();
|
|
120
|
+
await showWelcomeScreen(this.getActiveWorkflows(), { animate: this.animation });
|
|
106
121
|
}
|
|
107
|
-
// Validate profile override early so invalid values fail before tool setup.
|
|
108
|
-
// The resolved value is consumed later when generation reads effective config.
|
|
109
|
-
this.resolveProfileOverride();
|
|
110
122
|
// Get tool states before processing
|
|
111
123
|
const toolStates = getToolStates(projectPath);
|
|
112
124
|
// Get tool selection (pass detected tools for pre-selection)
|
|
113
125
|
const selectedToolIds = await this.getSelectedTools(toolStates, extendMode, detectedTools, projectPath);
|
|
114
126
|
// Validate selected tools
|
|
115
|
-
const validatedTools = this.validateTools(selectedToolIds, toolStates);
|
|
127
|
+
const validatedTools = this.validateTools(selectedToolIds, toolStates, projectPath);
|
|
128
|
+
// Selecting a renamed tool is consent to leave its former directory:
|
|
129
|
+
// init is about to write the current one, and leaving OpenSpec content
|
|
130
|
+
// behind would give the user two installs of the same tool.
|
|
131
|
+
for (const migration of migrateLegacyToolDirs(projectPath, validatedTools.map((tool) => tool.value))) {
|
|
132
|
+
if (hasMovableContent(migration)) {
|
|
133
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
134
|
+
}
|
|
135
|
+
const kept = keptInPlaceNotice(migration);
|
|
136
|
+
if (kept)
|
|
137
|
+
console.log(chalk.dim(kept));
|
|
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);
|
|
116
145
|
// Create directory structure and config
|
|
117
146
|
await this.createDirectoryStructure(openspecPath, extendMode);
|
|
118
147
|
// Generate skills and commands for each tool
|
|
119
|
-
const results = await this.generateSkillsAndCommands(projectPath, validatedTools);
|
|
148
|
+
const results = await this.generateSkillsAndCommands(projectPath, validatedTools, copilotDecision.write);
|
|
149
|
+
// Legacy cleanup was deferred to avoid interfering with skill/command generation;
|
|
150
|
+
// now that outputs are written, finalize the cleanup (e.g. remove stale files).
|
|
151
|
+
if (deferredLegacyCleanup) {
|
|
152
|
+
await this.finalizeDeferredLegacyCleanup(projectPath, deferredLegacyCleanup);
|
|
153
|
+
}
|
|
120
154
|
// Create config.yaml if needed
|
|
121
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) : [];
|
|
122
188
|
// Display success message
|
|
123
|
-
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
|
+
}
|
|
124
199
|
}
|
|
125
200
|
// ═══════════════════════════════════════════════════════════
|
|
126
201
|
// VALIDATION & SETUP
|
|
@@ -140,6 +215,59 @@ export class InitCommand {
|
|
|
140
215
|
return false;
|
|
141
216
|
return isInteractive({ interactive: this.interactiveOption });
|
|
142
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
|
+
}
|
|
143
271
|
resolveProfileOverride() {
|
|
144
272
|
if (this.profileOverride === undefined) {
|
|
145
273
|
return undefined;
|
|
@@ -149,26 +277,50 @@ export class InitCommand {
|
|
|
149
277
|
}
|
|
150
278
|
throw new Error(`Invalid profile "${this.profileOverride}". Available profiles: core, custom`);
|
|
151
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Resolves the workflows the effective profile installs, so onboarding output
|
|
282
|
+
* only mentions commands that will actually exist.
|
|
283
|
+
*/
|
|
284
|
+
getActiveWorkflows() {
|
|
285
|
+
const globalCfg = getGlobalConfig();
|
|
286
|
+
const activeProfile = this.resolveProfileOverride() ?? globalCfg.profile ?? 'core';
|
|
287
|
+
return [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
|
|
288
|
+
}
|
|
152
289
|
// ═══════════════════════════════════════════════════════════
|
|
153
290
|
// LEGACY CLEANUP
|
|
154
291
|
// ═══════════════════════════════════════════════════════════
|
|
292
|
+
/**
|
|
293
|
+
* Cleans repo-local legacy artifacts immediately and defers global Codex prompt
|
|
294
|
+
* cleanup until replacement skills have been installed.
|
|
295
|
+
*/
|
|
155
296
|
async handleLegacyCleanup(projectPath, extendMode) {
|
|
156
297
|
// Detect legacy artifacts
|
|
157
298
|
const detection = await detectLegacyArtifacts(projectPath);
|
|
158
299
|
if (!detection.hasLegacyArtifacts) {
|
|
159
|
-
return; // No legacy artifacts found
|
|
300
|
+
return null; // No legacy artifacts found
|
|
160
301
|
}
|
|
302
|
+
const immediateDetection = omitGlobalLegacyPromptFiles(detection);
|
|
161
303
|
// Show what was detected
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
304
|
+
const immediateSummary = formatDetectionSummary(immediateDetection);
|
|
305
|
+
if (immediateSummary) {
|
|
306
|
+
console.log();
|
|
307
|
+
console.log(immediateSummary);
|
|
308
|
+
console.log();
|
|
309
|
+
}
|
|
310
|
+
// Show which global prompts are deferred — they'll only be removed once
|
|
311
|
+
// the corresponding replacement skills are installed during generation.
|
|
312
|
+
const deferredSummary = formatDeferredGlobalPromptSummary(detection);
|
|
313
|
+
if (deferredSummary) {
|
|
314
|
+
console.log(deferredSummary);
|
|
315
|
+
console.log();
|
|
316
|
+
}
|
|
165
317
|
const canPrompt = this.canPromptInteractively();
|
|
166
318
|
if (this.force || !canPrompt) {
|
|
167
319
|
// --force flag or non-interactive mode: proceed with cleanup automatically.
|
|
168
320
|
// Legacy slash commands are 100% OpenSpec-managed, and config file cleanup
|
|
169
321
|
// only removes markers (never deletes files), so auto-cleanup is safe.
|
|
170
|
-
await this.
|
|
171
|
-
return;
|
|
322
|
+
await this.performImmediateLegacyCleanup(projectPath, detection);
|
|
323
|
+
return detection.globalSlashCommandFiles.length > 0 ? { detection } : null;
|
|
172
324
|
}
|
|
173
325
|
// Interactive mode: prompt for confirmation
|
|
174
326
|
const { confirm } = await import('@inquirer/prompts');
|
|
@@ -181,7 +333,51 @@ export class InitCommand {
|
|
|
181
333
|
console.log(chalk.dim('Run with --force to skip this prompt, or manually remove legacy files.'));
|
|
182
334
|
process.exit(0);
|
|
183
335
|
}
|
|
184
|
-
await this.
|
|
336
|
+
await this.performImmediateLegacyCleanup(projectPath, detection);
|
|
337
|
+
return detection.globalSlashCommandFiles.length > 0 ? { detection } : null;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Applies the safe subset of legacy cleanup that does not depend on newly
|
|
341
|
+
* generated Codex skills.
|
|
342
|
+
*/
|
|
343
|
+
async performImmediateLegacyCleanup(projectPath, detection) {
|
|
344
|
+
const immediateDetection = omitGlobalLegacyPromptFiles(detection);
|
|
345
|
+
if (!immediateDetection.hasLegacyArtifacts) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
await this.performLegacyCleanup(projectPath, immediateDetection);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Removes only the legacy global Codex prompts whose workflows now have
|
|
352
|
+
* replacement skills in the project.
|
|
353
|
+
*/
|
|
354
|
+
async finalizeDeferredLegacyCleanup(projectPath, deferredCleanup) {
|
|
355
|
+
const availableCodexWorkflows = await this.getInstalledWorkflowsForTool(projectPath, 'codex');
|
|
356
|
+
const removableMatches = getLegacyGlobalPromptMatches(deferredCleanup.detection)
|
|
357
|
+
.filter((prompt) => prompt.workflowIds.every((workflowId) => availableCodexWorkflows.has(workflowId)));
|
|
358
|
+
if (removableMatches.length > 0) {
|
|
359
|
+
await this.performLegacyCleanup(projectPath, pickGlobalLegacyPromptFiles(deferredCleanup.detection, removableMatches.map((prompt) => prompt.path)));
|
|
360
|
+
}
|
|
361
|
+
const blockedMatches = getLegacyGlobalPromptMatches(deferredCleanup.detection)
|
|
362
|
+
.filter((prompt) => !removableMatches.some((match) => match.path === prompt.path));
|
|
363
|
+
if (blockedMatches.length > 0) {
|
|
364
|
+
console.log(chalk.yellow('Preserved deferred global prompts without replacement skills:'));
|
|
365
|
+
for (const prompt of blockedMatches) {
|
|
366
|
+
console.log(chalk.dim(` - ${prompt.toolId}: ${prompt.path}`));
|
|
367
|
+
}
|
|
368
|
+
console.log();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Reads the currently installed workflow IDs for a single tool from the
|
|
373
|
+
* generated skill layout on disk.
|
|
374
|
+
*/
|
|
375
|
+
async getInstalledWorkflowsForTool(projectPath, toolId) {
|
|
376
|
+
const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
|
|
377
|
+
if (!tool) {
|
|
378
|
+
return new Set();
|
|
379
|
+
}
|
|
380
|
+
return new Set(scanInstalledWorkflowsShared(projectPath, [tool]));
|
|
185
381
|
}
|
|
186
382
|
async performLegacyCleanup(projectPath, detection) {
|
|
187
383
|
const spinner = ora('Cleaning up legacy files...').start();
|
|
@@ -300,7 +496,9 @@ export class InitCommand {
|
|
|
300
496
|
if (tokens.length === 0) {
|
|
301
497
|
throw new Error('The --tools option requires at least one tool ID when not using "all" or "none".');
|
|
302
498
|
}
|
|
303
|
-
|
|
499
|
+
// Retired ids resolve to their current tool, so a rebrand does not break
|
|
500
|
+
// an existing `--tools windsurf` in someone's setup script.
|
|
501
|
+
const normalizedTokens = tokens.map((token) => resolveToolIdAlias(token.toLowerCase()));
|
|
304
502
|
if (normalizedTokens.some((token) => token === 'all' || token === 'none')) {
|
|
305
503
|
throw new Error('Cannot combine reserved values "all" or "none" with specific tool IDs.');
|
|
306
504
|
}
|
|
@@ -317,23 +515,34 @@ export class InitCommand {
|
|
|
317
515
|
}
|
|
318
516
|
return deduped;
|
|
319
517
|
}
|
|
320
|
-
validateTools(toolIds, toolStates) {
|
|
518
|
+
validateTools(toolIds, toolStates, projectPath) {
|
|
321
519
|
const validatedTools = [];
|
|
322
|
-
|
|
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) {
|
|
323
527
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
324
528
|
if (!tool) {
|
|
325
529
|
const validToolIds = getToolsWithSkillsDir();
|
|
326
530
|
throw new Error(`Unknown tool '${toolId}'. Valid tools:\n ${validToolIds.join('\n ')}`);
|
|
327
531
|
}
|
|
328
|
-
if (!tool
|
|
532
|
+
if (!toolSupportsSkills(tool)) {
|
|
329
533
|
const validToolsWithSkills = getToolsWithSkillsDir();
|
|
330
534
|
throw new Error(`Tool '${toolId}' does not support skill generation.\nTools with skill generation support:\n ${validToolsWithSkills.join('\n ')}`);
|
|
331
535
|
}
|
|
332
536
|
const preState = toolStates.get(tool.value);
|
|
537
|
+
const skillsPath = resolveToolSkillsDir(projectPath, tool);
|
|
538
|
+
const isGlobalSkillTarget = hasGlobalSkillTarget(tool);
|
|
333
539
|
validatedTools.push({
|
|
334
540
|
value: tool.value,
|
|
335
541
|
name: tool.name,
|
|
336
542
|
skillsDir: tool.skillsDir,
|
|
543
|
+
skillsPath,
|
|
544
|
+
skillsRoot: isGlobalSkillTarget ? skillsPath : projectPath,
|
|
545
|
+
isGlobalSkillTarget,
|
|
337
546
|
wasConfigured: preState?.configured ?? false,
|
|
338
547
|
});
|
|
339
548
|
}
|
|
@@ -352,6 +561,7 @@ export class InitCommand {
|
|
|
352
561
|
path.join(openspecPath, 'changes', 'archive'),
|
|
353
562
|
];
|
|
354
563
|
for (const dir of directories) {
|
|
564
|
+
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
|
|
355
565
|
await FileSystemUtils.createDirectory(dir);
|
|
356
566
|
}
|
|
357
567
|
return;
|
|
@@ -364,6 +574,7 @@ export class InitCommand {
|
|
|
364
574
|
path.join(openspecPath, 'changes', 'archive'),
|
|
365
575
|
];
|
|
366
576
|
for (const dir of directories) {
|
|
577
|
+
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
|
|
367
578
|
await FileSystemUtils.createDirectory(dir);
|
|
368
579
|
}
|
|
369
580
|
spinner.stopAndPersist({
|
|
@@ -374,11 +585,20 @@ export class InitCommand {
|
|
|
374
585
|
// ═══════════════════════════════════════════════════════════
|
|
375
586
|
// SKILL & COMMAND GENERATION
|
|
376
587
|
// ═══════════════════════════════════════════════════════════
|
|
377
|
-
|
|
588
|
+
/**
|
|
589
|
+
* Generates skill files and slash commands for each selected tool,
|
|
590
|
+
* honoring the configured delivery mode (skills, commands, or both).
|
|
591
|
+
*
|
|
592
|
+
* @param projectPath - Absolute path to the project root
|
|
593
|
+
* @param tools - Selected tools with their skill directory metadata
|
|
594
|
+
* @returns Created, refreshed, and failed tools plus removed artifact counts
|
|
595
|
+
*/
|
|
596
|
+
async generateSkillsAndCommands(projectPath, tools, writeCopilotCloud) {
|
|
378
597
|
const createdTools = [];
|
|
379
598
|
const refreshedTools = [];
|
|
380
599
|
const failedTools = [];
|
|
381
600
|
const commandsSkipped = [];
|
|
601
|
+
const skillsInvocableCommandSkips = [];
|
|
382
602
|
let removedCommandCount = 0;
|
|
383
603
|
let removedSkillCount = 0;
|
|
384
604
|
// Read global config for profile and delivery settings (use --profile override if set)
|
|
@@ -387,33 +607,35 @@ export class InitCommand {
|
|
|
387
607
|
const delivery = globalConfig.delivery ?? 'both';
|
|
388
608
|
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
389
609
|
// Get skill and command templates filtered by profile workflows
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
const commandContents = shouldGenerateCommands ? getCommandContents(workflows) : [];
|
|
610
|
+
const deliveryIncludesCommands = delivery !== 'skills';
|
|
611
|
+
const skillTemplates = getSkillTemplates(workflows);
|
|
612
|
+
const commandContents = getCommandContents(workflows);
|
|
394
613
|
// Process each tool
|
|
395
614
|
for (const tool of tools) {
|
|
396
615
|
const spinner = ora(`Setting up ${tool.name}...`).start();
|
|
397
616
|
try {
|
|
398
|
-
|
|
617
|
+
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
618
|
+
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
619
|
+
// Generate skill files if the selected delivery and tool capability allow skills
|
|
399
620
|
if (shouldGenerateSkills) {
|
|
400
|
-
// Use tool-specific skillsDir
|
|
401
|
-
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
402
621
|
// Create skill directories and SKILL.md files
|
|
403
622
|
for (const { template, dirName } of skillTemplates) {
|
|
404
|
-
const skillDir = path.join(
|
|
623
|
+
const skillDir = path.join(tool.skillsPath, dirName);
|
|
405
624
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
406
625
|
// Generate SKILL.md content with YAML frontmatter including generatedBy
|
|
407
|
-
|
|
408
|
-
const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined;
|
|
626
|
+
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
409
627
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
410
628
|
// Write the skill file
|
|
629
|
+
FileSystemUtils.assertPathWithin(tool.skillsRoot, skillFile);
|
|
411
630
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
412
631
|
}
|
|
632
|
+
writeSharedSkillTarget(projectPath, tool.value);
|
|
413
633
|
}
|
|
414
|
-
if (!
|
|
415
|
-
|
|
416
|
-
|
|
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);
|
|
417
639
|
}
|
|
418
640
|
// Generate commands if delivery includes commands
|
|
419
641
|
if (shouldGenerateCommands) {
|
|
@@ -421,17 +643,25 @@ export class InitCommand {
|
|
|
421
643
|
if (adapter) {
|
|
422
644
|
const generatedCommands = generateCommands(commandContents, adapter);
|
|
423
645
|
for (const cmd of generatedCommands) {
|
|
424
|
-
const commandFile =
|
|
646
|
+
const commandFile = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmd.path);
|
|
425
647
|
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
426
648
|
}
|
|
427
649
|
}
|
|
650
|
+
}
|
|
651
|
+
else if (deliveryIncludesCommands) {
|
|
652
|
+
if (resolveCommandSurfaceCapability(tool.value) === 'skills-invocable') {
|
|
653
|
+
skillsInvocableCommandSkips.push(tool.value);
|
|
654
|
+
}
|
|
428
655
|
else {
|
|
429
656
|
commandsSkipped.push(tool.value);
|
|
430
657
|
}
|
|
431
658
|
}
|
|
432
|
-
if (
|
|
659
|
+
if (shouldReconcileCommandFilesForTool(tool.value, delivery)) {
|
|
433
660
|
removedCommandCount += await this.removeCommandFiles(projectPath, tool.value);
|
|
434
661
|
}
|
|
662
|
+
if (tool.value === 'github-copilot' && writeCopilotCloud) {
|
|
663
|
+
await writeCopilotCloudFiles(projectPath);
|
|
664
|
+
}
|
|
435
665
|
spinner.succeed(`Setup complete for ${tool.name}`);
|
|
436
666
|
if (tool.wasConfigured) {
|
|
437
667
|
refreshedTools.push(tool);
|
|
@@ -445,11 +675,22 @@ export class InitCommand {
|
|
|
445
675
|
failedTools.push({ name: tool.name, error: error });
|
|
446
676
|
}
|
|
447
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
|
+
}
|
|
448
688
|
return {
|
|
449
689
|
createdTools,
|
|
450
690
|
refreshedTools,
|
|
451
691
|
failedTools,
|
|
452
692
|
commandsSkipped,
|
|
693
|
+
skillsInvocableCommandSkips,
|
|
453
694
|
removedCommandCount,
|
|
454
695
|
removedSkillCount,
|
|
455
696
|
};
|
|
@@ -467,6 +708,7 @@ export class InitCommand {
|
|
|
467
708
|
}
|
|
468
709
|
try {
|
|
469
710
|
const yamlContent = serializeConfig({ schema: DEFAULT_SCHEMA });
|
|
711
|
+
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), configPath);
|
|
470
712
|
await FileSystemUtils.writeFile(configPath, yamlContent);
|
|
471
713
|
return 'created';
|
|
472
714
|
}
|
|
@@ -477,9 +719,9 @@ export class InitCommand {
|
|
|
477
719
|
// ═══════════════════════════════════════════════════════════
|
|
478
720
|
// UI & OUTPUT
|
|
479
721
|
// ═══════════════════════════════════════════════════════════
|
|
480
|
-
displaySuccessMessage(projectPath, tools, results, configStatus) {
|
|
722
|
+
displaySuccessMessage(projectPath, tools, results, configStatus, copilot) {
|
|
481
723
|
console.log();
|
|
482
|
-
console.log(chalk.bold('OpenSpec Setup Complete'));
|
|
724
|
+
console.log(chalk.bold(results.failedTools.length > 0 ? 'OpenSpec Setup Incomplete' : 'OpenSpec Setup Complete'));
|
|
483
725
|
console.log();
|
|
484
726
|
// Show created vs refreshed tools
|
|
485
727
|
if (results.createdTools.length > 0) {
|
|
@@ -495,17 +737,56 @@ export class InitCommand {
|
|
|
495
737
|
const profile = this.profileOverride ?? globalConfig.profile ?? 'core';
|
|
496
738
|
const delivery = globalConfig.delivery ?? 'both';
|
|
497
739
|
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
+
}
|
|
506
762
|
}
|
|
507
|
-
else
|
|
508
|
-
|
|
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
|
+
}
|
|
509
790
|
}
|
|
510
791
|
}
|
|
511
792
|
// Show failures
|
|
@@ -516,12 +797,42 @@ export class InitCommand {
|
|
|
516
797
|
if (results.commandsSkipped.length > 0) {
|
|
517
798
|
console.log(chalk.dim(`Commands skipped for: ${results.commandsSkipped.join(', ')} (no adapter)`));
|
|
518
799
|
}
|
|
800
|
+
if (results.skillsInvocableCommandSkips.length > 0) {
|
|
801
|
+
console.log(chalk.dim(`Commands skipped for: ${results.skillsInvocableCommandSkips.join(', ')} (uses skills)`));
|
|
802
|
+
}
|
|
519
803
|
if (results.removedCommandCount > 0) {
|
|
520
804
|
console.log(chalk.dim(`Removed: ${results.removedCommandCount} command files (delivery: skills)`));
|
|
521
805
|
}
|
|
522
806
|
if (results.removedSkillCount > 0) {
|
|
523
807
|
console.log(chalk.dim(`Removed: ${results.removedSkillCount} skill directories (delivery: commands)`));
|
|
524
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
|
+
}
|
|
829
|
+
// Show manual setup notes for tools that need extra configuration
|
|
830
|
+
for (const tool of successfulTools) {
|
|
831
|
+
const setupNote = AI_TOOLS.find((t) => t.value === tool.value)?.setupNote;
|
|
832
|
+
if (setupNote) {
|
|
833
|
+
console.log(chalk.yellow(`Setup required for ${tool.name}: ${setupNote}`));
|
|
834
|
+
}
|
|
835
|
+
}
|
|
525
836
|
// Config status
|
|
526
837
|
if (configStatus === 'created') {
|
|
527
838
|
console.log(`Config: openspec/config.yaml (schema: ${DEFAULT_SCHEMA})`);
|
|
@@ -537,17 +848,80 @@ export class InitCommand {
|
|
|
537
848
|
console.log(chalk.dim(`Config: skipped (non-interactive mode)`));
|
|
538
849
|
}
|
|
539
850
|
// Getting started (task 7.6: show propose if in profile)
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
851
|
+
const activeWorkflows = this.getActiveWorkflows();
|
|
852
|
+
// When no tool got /opsx:* commands, point at the skill instead of a
|
|
853
|
+
// command that does not exist.
|
|
854
|
+
const activeDelivery = getGlobalConfig().delivery ?? 'both';
|
|
855
|
+
const commandsGenerated = successfulTools.some((tool) => shouldGenerateCommandsForTool(tool.value, activeDelivery));
|
|
856
|
+
const skillsGenerated = successfulTools.some((tool) => shouldGenerateSkillsForTool(tool.value, activeDelivery));
|
|
857
|
+
// Each hint line must be a usable instruction for the tool it serves.
|
|
858
|
+
// Tools that generated commands are told the command name their files
|
|
859
|
+
// answer to (/opsx:* when namespaced under opsx/, /opsx-* when the
|
|
860
|
+
// filename is the command); tools that only got skills are told their
|
|
861
|
+
// documented skill invocation (Kimi Code: /skill:openspec-*; Codex CLI:
|
|
862
|
+
// $openspec-*; others: /openspec-*). Tools that got no artifacts are
|
|
863
|
+
// covered by the configuration correction instead. When the selection
|
|
864
|
+
// disagrees, print one line per distinct instruction, labeled with the
|
|
865
|
+
// tools it applies to.
|
|
866
|
+
const startHintLines = (command) => {
|
|
867
|
+
const hintToTools = new Map();
|
|
868
|
+
for (const tool of successfulTools) {
|
|
869
|
+
let hint;
|
|
870
|
+
if (shouldGenerateCommandsForTool(tool.value, activeDelivery)) {
|
|
871
|
+
const transformer = getTransformerForTool(tool.value, activeDelivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
872
|
+
hint = `Start your first change: ${transformer ? transformer(command) : command} "your idea"`;
|
|
873
|
+
}
|
|
874
|
+
else if (shouldGenerateSkillsForTool(tool.value, activeDelivery)) {
|
|
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"`;
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
hintToTools.set(hint, [...(hintToTools.get(hint) ?? []), tool.name]);
|
|
887
|
+
}
|
|
888
|
+
if (hintToTools.size === 0) {
|
|
889
|
+
// No successful tools: keep the generic command hint
|
|
890
|
+
return [`Start your first change: ${command} "your idea"`];
|
|
891
|
+
}
|
|
892
|
+
if (hintToTools.size === 1) {
|
|
893
|
+
return [[...hintToTools.keys()][0]];
|
|
894
|
+
}
|
|
895
|
+
return [...hintToTools.entries()].map(([hint, toolNames]) => `${hint} (${toolNames.join(', ')})`);
|
|
896
|
+
};
|
|
897
|
+
const printStartHints = (command) => {
|
|
545
898
|
console.log(chalk.bold('Getting started:'));
|
|
546
|
-
|
|
899
|
+
for (const line of startHintLines(command)) {
|
|
900
|
+
console.log(` ${line}`);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
console.log();
|
|
904
|
+
// delivery=commands with tools that only support skills: those tools get
|
|
905
|
+
// no artifacts at all, so print a per-tool configuration correction
|
|
906
|
+
// rather than leave them with a dead (or missing) instruction — even
|
|
907
|
+
// when other selected tools did get commands or skills.
|
|
908
|
+
const zeroArtifactTools = successfulTools.filter((tool) => !shouldGenerateSkillsForTool(tool.value, activeDelivery) &&
|
|
909
|
+
!shouldGenerateCommandsForTool(tool.value, activeDelivery));
|
|
910
|
+
if (zeroArtifactTools.length > 0) {
|
|
911
|
+
const names = zeroArtifactTools.map((tool) => tool.name).join(', ');
|
|
912
|
+
console.log(chalk.yellow(`No skills or commands were generated for ${names}: delivery is set to 'commands' but ` +
|
|
913
|
+
`${zeroArtifactTools.length === 1 ? 'it supports' : 'they support'} only skills. ` +
|
|
914
|
+
`Run 'openspec config set delivery both' to generate skills.`));
|
|
915
|
+
}
|
|
916
|
+
if (successfulTools.length > 0 && !commandsGenerated && !skillsGenerated) {
|
|
917
|
+
// Nothing was generated for any tool: the correction above is the
|
|
918
|
+
// whole story, so don't advertise an invocation that doesn't exist.
|
|
919
|
+
}
|
|
920
|
+
else if (activeWorkflows.includes('propose')) {
|
|
921
|
+
printStartHints('/opsx:propose');
|
|
547
922
|
}
|
|
548
923
|
else if (activeWorkflows.includes('new')) {
|
|
549
|
-
|
|
550
|
-
console.log(' Start your first change: /opsx:new "your idea"');
|
|
924
|
+
printStartHints('/opsx:new');
|
|
551
925
|
}
|
|
552
926
|
else {
|
|
553
927
|
console.log("Done. Run 'openspec config profile' to configure your workflows.");
|
|
@@ -556,10 +930,16 @@ export class InitCommand {
|
|
|
556
930
|
console.log();
|
|
557
931
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
558
932
|
console.log(`Feedback: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec/issues')}`);
|
|
559
|
-
// Restart instruction if any tools were configured
|
|
560
|
-
|
|
933
|
+
// Restart instruction if any tools were configured and got a surface
|
|
934
|
+
// (when nothing was generated there is nothing a restart would pick up);
|
|
935
|
+
// only mention commands when commands were actually generated. Not "slash
|
|
936
|
+
// commands": Amazon Q's generated files are prompt-library entries invoked
|
|
937
|
+
// with @, so a restart line promising slash commands would be wrong for it.
|
|
938
|
+
if ((results.createdTools.length > 0 || results.refreshedTools.length > 0) && (commandsGenerated || skillsGenerated)) {
|
|
561
939
|
console.log();
|
|
562
|
-
console.log(chalk.white(
|
|
940
|
+
console.log(chalk.white(commandsGenerated
|
|
941
|
+
? 'Restart your IDE for the new commands to take effect.'
|
|
942
|
+
: 'Restart your IDE for the new skills to take effect.'));
|
|
563
943
|
}
|
|
564
944
|
console.log();
|
|
565
945
|
}
|
|
@@ -571,18 +951,19 @@ export class InitCommand {
|
|
|
571
951
|
spinner: PROGRESS_SPINNER,
|
|
572
952
|
}).start();
|
|
573
953
|
}
|
|
574
|
-
async removeSkillDirs(skillsDir) {
|
|
954
|
+
async removeSkillDirs(skillsRoot, skillsDir) {
|
|
575
955
|
let removed = 0;
|
|
576
956
|
for (const workflow of ALL_WORKFLOWS) {
|
|
577
957
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
578
958
|
if (!dirName)
|
|
579
959
|
continue;
|
|
580
960
|
const skillDir = path.join(skillsDir, dirName);
|
|
961
|
+
if (!fs.existsSync(skillDir))
|
|
962
|
+
continue;
|
|
963
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
|
|
581
964
|
try {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
removed++;
|
|
585
|
-
}
|
|
965
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
966
|
+
removed++;
|
|
586
967
|
}
|
|
587
968
|
catch {
|
|
588
969
|
// Ignore errors
|
|
@@ -597,7 +978,7 @@ export class InitCommand {
|
|
|
597
978
|
return 0;
|
|
598
979
|
for (const workflow of ALL_WORKFLOWS) {
|
|
599
980
|
const cmdPath = adapter.getFilePath(workflow);
|
|
600
|
-
const fullPath =
|
|
981
|
+
const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
|
|
601
982
|
try {
|
|
602
983
|
if (fs.existsSync(fullPath)) {
|
|
603
984
|
await fs.promises.unlink(fullPath);
|