@codewalla_india/openspec 1.3.1 → 1.3.2
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/LICENSE +22 -22
- package/README.md +272 -191
- package/bin/openspec.js +5 -5
- package/dist/cli/index.js +99 -62
- package/dist/commands/change.d.ts +0 -2
- package/dist/commands/change.js +107 -86
- package/dist/commands/config.js +14 -16
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +79 -30
- package/dist/commands/modify.d.ts +26 -0
- package/dist/commands/modify.js +147 -0
- package/dist/commands/schema.js +170 -91
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +49 -29
- package/dist/commands/store.js +15 -21
- package/dist/commands/validate.d.ts +8 -0
- package/dist/commands/validate.js +43 -20
- package/dist/commands/workflow/index.d.ts +2 -2
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +6 -26
- package/dist/commands/workflow/instructions.js +183 -376
- package/dist/commands/workflow/new-change.d.ts +0 -4
- package/dist/commands/workflow/new-change.js +10 -28
- package/dist/commands/workflow/shared.d.ts +34 -19
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +10 -32
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/commands/workset.d.ts +2 -2
- package/dist/commands/workset.js +19 -22
- package/dist/comprehension-quiz/fingerprint.d.ts +39 -0
- package/dist/comprehension-quiz/fingerprint.js +84 -0
- package/dist/comprehension-quiz/index.d.ts +42 -0
- package/dist/comprehension-quiz/index.js +47 -0
- package/dist/comprehension-quiz/pass-record.d.ts +44 -0
- package/dist/comprehension-quiz/pass-record.js +98 -0
- package/dist/comprehension-quiz/providers/anthropic.d.ts +34 -0
- package/dist/comprehension-quiz/providers/anthropic.js +53 -0
- package/dist/comprehension-quiz/providers/factory.d.ts +44 -0
- package/dist/comprehension-quiz/providers/factory.js +174 -0
- package/dist/comprehension-quiz/providers/interface.d.ts +59 -0
- package/dist/comprehension-quiz/providers/interface.js +63 -0
- package/dist/comprehension-quiz/providers/local.d.ts +34 -0
- package/dist/comprehension-quiz/providers/local.js +53 -0
- package/dist/comprehension-quiz/providers/ollama.d.ts +34 -0
- package/dist/comprehension-quiz/providers/ollama.js +53 -0
- package/dist/comprehension-quiz/providers/openai.d.ts +34 -0
- package/dist/comprehension-quiz/providers/openai.js +53 -0
- package/dist/comprehension-quiz/question-generator.d.ts +69 -0
- package/dist/comprehension-quiz/question-generator.js +141 -0
- package/dist/comprehension-quiz/quiz-executor.d.ts +70 -0
- package/dist/comprehension-quiz/quiz-executor.js +165 -0
- package/dist/comprehension-quiz/types.d.ts +115 -0
- package/dist/comprehension-quiz/types.js +11 -0
- package/dist/core/archive.d.ts +14 -1
- package/dist/core/archive.js +1350 -190
- package/dist/core/artifact-graph/graph.d.ts +28 -9
- package/dist/core/artifact-graph/graph.js +94 -43
- 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 +10 -1
- package/dist/core/change-status-policy.js +30 -2
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +11 -5
- package/dist/core/command-generation/adapters/antigravity.js +6 -5
- package/dist/core/command-generation/adapters/auggie.js +7 -6
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +11 -10
- package/dist/core/command-generation/adapters/claude.d.ts +1 -1
- package/dist/core/command-generation/adapters/claude.js +12 -17
- package/dist/core/command-generation/adapters/cline.js +5 -5
- package/dist/core/command-generation/adapters/codebuddy.js +8 -7
- package/dist/core/command-generation/adapters/continue.js +8 -7
- package/dist/core/command-generation/adapters/costrict.js +7 -6
- package/dist/core/command-generation/adapters/crush.js +9 -9
- package/dist/core/command-generation/adapters/cursor.js +8 -8
- 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 +7 -6
- package/dist/core/command-generation/adapters/gemini.js +40 -5
- package/dist/core/command-generation/adapters/github-copilot.js +6 -5
- package/dist/core/command-generation/adapters/iflow.js +9 -8
- package/dist/core/command-generation/adapters/index.d.ts +4 -2
- package/dist/core/command-generation/adapters/index.js +4 -2
- package/dist/core/command-generation/adapters/junie.js +6 -5
- package/dist/core/command-generation/adapters/kilocode.js +1 -1
- package/dist/core/command-generation/adapters/kiro.js +6 -5
- package/dist/core/command-generation/adapters/lingma.js +9 -9
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
- package/dist/core/command-generation/adapters/opencode.js +6 -8
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +7 -10
- package/dist/core/command-generation/adapters/qoder.js +9 -9
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +14 -9
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +9 -9
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +28 -0
- 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 +8 -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 -61
- package/dist/core/completions/generators/bash-generator.js +41 -41
- package/dist/core/completions/generators/fish-generator.js +7 -7
- package/dist/core/completions/generators/powershell-generator.js +44 -37
- package/dist/core/completions/generators/zsh-generator.js +33 -33
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/completions/templates/bash-templates.js +24 -24
- package/dist/core/completions/templates/fish-templates.js +38 -38
- package/dist/core/completions/templates/powershell-templates.js +28 -28
- package/dist/core/completions/templates/zsh-templates.js +39 -39
- 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 +46 -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 +474 -73
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +253 -24
- package/dist/core/list.js +23 -16
- package/dist/core/migration.d.ts +73 -1
- package/dist/core/migration.js +360 -11
- package/dist/core/modify/conflict-detector.d.ts +26 -0
- package/dist/core/modify/conflict-detector.js +62 -0
- package/dist/core/modify/dependency-propagator.d.ts +38 -0
- package/dist/core/modify/dependency-propagator.js +103 -0
- package/dist/core/modify/history-tracker.d.ts +34 -0
- package/dist/core/modify/history-tracker.js +114 -0
- package/dist/core/modify/index.d.ts +7 -0
- package/dist/core/modify/index.js +7 -0
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/openspec-root.js +18 -15
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +35 -21
- 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.d.ts +0 -3
- package/dist/core/parsers/markdown-parser.js +7 -60
- package/dist/core/parsers/requirement-blocks.d.ts +29 -0
- package/dist/core/parsers/requirement-blocks.js +150 -31
- package/dist/core/parsers/requirement-text.d.ts +48 -0
- package/dist/core/parsers/requirement-text.js +98 -0
- 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 +49 -50
- package/dist/core/profiles.d.ts +2 -2
- package/dist/core/profiles.js +2 -2
- package/dist/core/project-config.d.ts +27 -18
- package/dist/core/project-config.js +101 -34
- 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/schemas/base.schema.js +7 -2
- package/dist/core/shared/allowed-tools.d.ts +12 -0
- package/dist/core/shared/allowed-tools.js +12 -0
- 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-generation.js +17 -13
- 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 +27 -7
- package/dist/core/shared/tool-detection.js +167 -37
- 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 +667 -145
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/store/operations.js +20 -0
- package/dist/core/templates/skill-templates.d.ts +3 -2
- package/dist/core/templates/skill-templates.js +3 -2
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +186 -224
- package/dist/core/templates/workflows/archive-change.js +387 -260
- package/dist/core/templates/workflows/bulk-archive-change.js +655 -475
- package/dist/core/templates/workflows/continue-change.js +210 -219
- package/dist/core/templates/workflows/explore.js +479 -443
- package/dist/core/templates/workflows/feedback.js +97 -97
- package/dist/core/templates/workflows/ff-change.js +200 -191
- package/dist/core/templates/workflows/modify-change.d.ts +4 -1
- package/dist/core/templates/workflows/modify-change.js +237 -115
- package/dist/core/templates/workflows/new-change.js +127 -134
- package/dist/core/templates/workflows/onboard.js +548 -542
- package/dist/core/templates/workflows/propose.js +273 -217
- 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 +500 -273
- package/dist/core/templates/workflows/update-change.d.ts +10 -0
- package/dist/core/templates/workflows/update-change.js +177 -0
- package/dist/core/templates/workflows/verify-change.js +324 -326
- package/dist/core/update.d.ts +40 -5
- package/dist/core/update.js +411 -96
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/plan-validator.d.ts +8 -0
- package/dist/core/validation/plan-validator.js +45 -0
- package/dist/core/validation/task-numbering.d.ts +16 -0
- package/dist/core/validation/task-numbering.js +57 -0
- package/dist/core/validation/validator.d.ts +27 -3
- package/dist/core/validation/validator.js +337 -48
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +731 -0
- package/dist/core/view.js +13 -18
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/caller-detection.d.ts +27 -0
- package/dist/telemetry/caller-detection.js +85 -0
- package/dist/telemetry/caller-detection.test.d.ts +5 -0
- package/dist/telemetry/config.d.ts +48 -2
- package/dist/telemetry/config.js +113 -11
- package/dist/telemetry/config.test.d.ts +5 -0
- package/dist/telemetry/deduplication.d.ts +35 -0
- package/dist/telemetry/deduplication.js +82 -0
- package/dist/telemetry/deduplication.test.d.ts +5 -0
- package/dist/telemetry/identity.d.ts +62 -22
- package/dist/telemetry/identity.js +147 -102
- package/dist/telemetry/index.d.ts +126 -15
- package/dist/telemetry/index.js +569 -37
- package/dist/telemetry/sanitization.d.ts +40 -0
- package/dist/telemetry/sanitization.js +114 -0
- package/dist/telemetry/sanitization.test.d.ts +5 -0
- package/dist/telemetry/throttling.d.ts +20 -0
- package/dist/telemetry/throttling.js +70 -0
- 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 +29 -1
- package/dist/utils/task-progress.js +109 -15
- package/package.json +96 -84
- package/schemas/spec-driven/schema.yaml +252 -184
- package/schemas/spec-driven/templates/design.md +19 -19
- package/schemas/spec-driven/templates/plan-template.md +91 -0
- package/schemas/spec-driven/templates/proposal.md +29 -24
- package/schemas/spec-driven/templates/spec.md +11 -8
- package/schemas/spec-driven/templates/tasks.md +9 -9
- package/scripts/postinstall.js +83 -83
- 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/comprehension/config.d.ts +0 -20
- package/dist/core/comprehension/config.js +0 -23
- package/dist/core/comprehension/fingerprint.d.ts +0 -14
- package/dist/core/comprehension/fingerprint.js +0 -33
- package/dist/core/comprehension/index.d.ts +0 -57
- package/dist/core/comprehension/index.js +0 -87
- package/dist/core/comprehension/pass-record.d.ts +0 -29
- package/dist/core/comprehension/pass-record.js +0 -64
- package/dist/core/comprehension/stats.d.ts +0 -35
- package/dist/core/comprehension/stats.js +0 -133
- package/dist/core/templates/workflows/comprehension-guidance.d.ts +0 -9
- package/dist/core/templates/workflows/comprehension-guidance.js +0 -73
- package/dist/core/templates/workflows/mcp-guidance.d.ts +0 -13
- package/dist/core/templates/workflows/mcp-guidance.js +0 -131
- package/dist/core/templates/workflows/user-prompt-guidance.d.ts +0 -16
- package/dist/core/templates/workflows/user-prompt-guidance.js +0 -48
- package/dist/telemetry/caller.d.ts +0 -5
- package/dist/telemetry/caller.js +0 -29
- package/dist/telemetry/client.d.ts +0 -27
- package/dist/telemetry/client.js +0 -127
- package/dist/telemetry/command-context.d.ts +0 -13
- package/dist/telemetry/command-context.js +0 -59
- package/dist/telemetry/comprehension.d.ts +0 -44
- package/dist/telemetry/comprehension.js +0 -105
- package/dist/telemetry/content.d.ts +0 -10
- package/dist/telemetry/content.js +0 -56
- package/dist/telemetry/git-stats.d.ts +0 -12
- package/dist/telemetry/git-stats.js +0 -69
- package/dist/telemetry/identify-cache.d.ts +0 -7
- package/dist/telemetry/identify-cache.js +0 -47
- package/dist/telemetry/input.d.ts +0 -17
- package/dist/telemetry/input.js +0 -68
- package/dist/telemetry/marker.d.ts +0 -37
- package/dist/telemetry/marker.js +0 -67
- package/dist/telemetry/workflow.d.ts +0 -85
- package/dist/telemetry/workflow.js +0 -318
- package/schemas/spec-driven/templates/plan.md +0 -19
package/dist/cli/index.js
CHANGED
|
@@ -3,10 +3,11 @@ import { Command, Option } from 'commander';
|
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import path from 'path';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { promises as fs } from 'fs';
|
|
8
|
-
import { AI_TOOLS } from '../core/config.js';
|
|
8
|
+
import { AI_TOOLS, TOOL_ID_ALIASES } from '../core/config.js';
|
|
9
9
|
import { UpdateCommand } from '../core/update.js';
|
|
10
|
+
import { getAvailableCliUpdate, displayCliUpdateNote, shouldOfferUpgrade, getInstallDir, offerCliUpgrade, rerunUpdateWithUpgradedCli, displayUpgradeCommand, isSourceCheckout, } from '../core/version-check.js';
|
|
10
11
|
import { ListCommand } from '../core/list.js';
|
|
11
12
|
import { ArchiveCommand } from '../core/archive.js';
|
|
12
13
|
import { ViewCommand } from '../core/view.js';
|
|
@@ -23,10 +24,12 @@ import { registerStoreCommand } from '../commands/store.js';
|
|
|
23
24
|
import { registerDoctorCommand } from '../commands/doctor.js';
|
|
24
25
|
import { registerContextCommand } from '../commands/context.js';
|
|
25
26
|
import { registerWorksetCommand } from '../commands/workset.js';
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
27
|
+
import { createModifyCommand } from '../commands/modify.js';
|
|
28
|
+
import { statusCommand, instructionsCommand, applyInstructionsCommand, archiveInstructionsCommand, templatesCommand, schemasCommand, newChangeCommand, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
|
|
29
|
+
import { maybeShowTelemetryNotice, trackCommand, shutdown } from '../telemetry/index.js';
|
|
30
|
+
import { readIdentity } from '../telemetry/identity.js';
|
|
29
31
|
import { COMMON_FLAGS } from '../core/completions/shared-flags.js';
|
|
32
|
+
import { isInteractive } from '../utils/interactive.js';
|
|
30
33
|
const STORE_OPTION_DESCRIPTION = COMMON_FLAGS.store.description;
|
|
31
34
|
// Deliberate rejection path: --store-path stays registered (hidden) so the
|
|
32
35
|
// resolver can explain that registering the path is the supported route,
|
|
@@ -71,33 +74,6 @@ export function getCommandPath(command) {
|
|
|
71
74
|
}
|
|
72
75
|
return names.join(':') || 'openspec';
|
|
73
76
|
}
|
|
74
|
-
function commandUsesJson(command) {
|
|
75
|
-
let current = command;
|
|
76
|
-
while (current) {
|
|
77
|
-
if (current.opts()?.json === true) {
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
current = current.parent;
|
|
81
|
-
}
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
function exitTelemetryIdentityRequired(error, json) {
|
|
85
|
-
if (json) {
|
|
86
|
-
console.log(JSON.stringify({
|
|
87
|
-
status: [
|
|
88
|
-
{
|
|
89
|
-
severity: 'error',
|
|
90
|
-
code: error.code,
|
|
91
|
-
message: error.message,
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
}, null, 2));
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
console.error(`Error: ${error.message}`);
|
|
98
|
-
}
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
77
|
program
|
|
102
78
|
.name('openspec')
|
|
103
79
|
.description('AI-native system for spec-driven development')
|
|
@@ -113,33 +89,43 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
|
113
89
|
if (opts.color === false) {
|
|
114
90
|
process.env.NO_COLOR = '1';
|
|
115
91
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
throw error;
|
|
92
|
+
// Identity gating: check for identity before executing command
|
|
93
|
+
// Exempt commands: init and update
|
|
94
|
+
const commandPath = getCommandPath(actionCommand);
|
|
95
|
+
const isExemptCommand = commandPath === 'init' || commandPath === 'update';
|
|
96
|
+
if (!isExemptCommand) {
|
|
97
|
+
const identity = await readIdentity();
|
|
98
|
+
if (!identity) {
|
|
99
|
+
console.error('Error: Identity required. Configure with: openspec init');
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
process.exit(1);
|
|
127
102
|
}
|
|
128
103
|
}
|
|
129
|
-
|
|
104
|
+
// Show first-run telemetry notice (if not seen)
|
|
105
|
+
await maybeShowTelemetryNotice();
|
|
106
|
+
// Track command execution (use actionCommand to get the actual subcommand)
|
|
107
|
+
await trackCommand(commandPath, version);
|
|
130
108
|
});
|
|
131
109
|
// Shutdown telemetry after command completes
|
|
132
110
|
program.hook('postAction', async () => {
|
|
133
111
|
await shutdown();
|
|
134
112
|
});
|
|
135
|
-
const availableToolIds = AI_TOOLS
|
|
136
|
-
|
|
113
|
+
const availableToolIds = AI_TOOLS
|
|
114
|
+
.filter((tool) => tool.skillsDir || tool.globalSkillsDir)
|
|
115
|
+
.map((tool) => tool.value);
|
|
116
|
+
const toolAliasNote = Object.entries(TOOL_ID_ALIASES)
|
|
117
|
+
.map(([retired, current]) => `${retired} (now ${current})`)
|
|
118
|
+
.join(', ');
|
|
119
|
+
const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}. Also accepted: ${toolAliasNote}`;
|
|
137
120
|
program
|
|
138
121
|
.command('init [path]')
|
|
139
122
|
.description('Initialize OpenSpec in your project')
|
|
140
123
|
.option('--tools <tools>', toolsOptionDescription)
|
|
141
124
|
.option('--force', 'Auto-cleanup legacy files without prompting')
|
|
142
125
|
.option('--profile <profile>', 'Override global config profile (core or custom)')
|
|
126
|
+
.option('--no-animation', 'Show a static welcome screen instead of the animated one')
|
|
127
|
+
.option('--copilot-cloud', 'Set up GitHub Copilot cloud coding-agent files without prompting')
|
|
128
|
+
.option('--no-copilot-cloud', 'Skip GitHub Copilot cloud coding-agent files without prompting')
|
|
143
129
|
.action(async (targetPath = '.', options) => {
|
|
144
130
|
try {
|
|
145
131
|
// Validate that the path is a valid directory
|
|
@@ -167,6 +153,8 @@ program
|
|
|
167
153
|
tools: options?.tools,
|
|
168
154
|
force: options?.force,
|
|
169
155
|
profile: options?.profile,
|
|
156
|
+
animation: options?.animation,
|
|
157
|
+
copilotCloud: options?.copilotCloud,
|
|
170
158
|
});
|
|
171
159
|
await initCommand.execute(targetPath);
|
|
172
160
|
}
|
|
@@ -202,8 +190,55 @@ program
|
|
|
202
190
|
.option('--force', 'Force update even when tools are up to date')
|
|
203
191
|
.action(async (targetPath = '.', options) => {
|
|
204
192
|
try {
|
|
193
|
+
const installDir = getInstallDir();
|
|
194
|
+
// Running from a clone: the version is whatever the branch says, so any
|
|
195
|
+
// upgrade advice would be noise. Decided before the request, so a
|
|
196
|
+
// contributor never waits on an answer that gets thrown away.
|
|
197
|
+
const latestVersion = isSourceCheckout(installDir) ? null : await getAvailableCliUpdate();
|
|
198
|
+
const announce = latestVersion !== null;
|
|
199
|
+
// Offer to upgrade first: this process generates files from its own
|
|
200
|
+
// templates, so upgrading afterwards would leave the old ones on disk.
|
|
201
|
+
// Both streams must be a terminal — with stdout redirected the question
|
|
202
|
+
// lands in the file and the user waits at a blank screen forever.
|
|
203
|
+
const canOffer = announce &&
|
|
204
|
+
shouldOfferUpgrade({
|
|
205
|
+
installDir,
|
|
206
|
+
projectPath: targetPath,
|
|
207
|
+
interactive: isInteractive(),
|
|
208
|
+
stdoutIsTty: Boolean(process.stdout.isTTY),
|
|
209
|
+
});
|
|
210
|
+
let declined = false;
|
|
211
|
+
if (latestVersion && canOffer) {
|
|
212
|
+
displayCliUpdateNote(latestVersion, targetPath, { withCommand: false });
|
|
213
|
+
const outcome = await offerCliUpgrade(latestVersion);
|
|
214
|
+
// Set the code and return rather than process.exit: exiting here would
|
|
215
|
+
// skip commander's postAction hook, killing the telemetry flush
|
|
216
|
+
// mid-request.
|
|
217
|
+
if (outcome === 'cancelled') {
|
|
218
|
+
// Ctrl-C means stop the command, not fall through to more prompts.
|
|
219
|
+
process.exitCode = 130;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (outcome === 'upgraded') {
|
|
223
|
+
process.exitCode = await rerunUpdateWithUpgradedCli(targetPath, {
|
|
224
|
+
force: options?.force,
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Declined, failed, or upgraded-but-unreachable: fall through to the
|
|
229
|
+
// update, then leave the command on screen underneath it.
|
|
230
|
+
declined = true;
|
|
231
|
+
}
|
|
205
232
|
const updateCommand = new UpdateCommand({ force: options?.force });
|
|
206
233
|
await updateCommand.execute(targetPath);
|
|
234
|
+
if (declined) {
|
|
235
|
+
// The headline was printed before the prompt; only the manual route is
|
|
236
|
+
// still owed, and it belongs where the user is looking now.
|
|
237
|
+
displayUpgradeCommand(targetPath);
|
|
238
|
+
}
|
|
239
|
+
else if (latestVersion) {
|
|
240
|
+
displayCliUpdateNote(latestVersion, targetPath);
|
|
241
|
+
}
|
|
207
242
|
}
|
|
208
243
|
catch (error) {
|
|
209
244
|
failWithError(error);
|
|
@@ -249,10 +284,19 @@ program
|
|
|
249
284
|
program
|
|
250
285
|
.command('view')
|
|
251
286
|
.description('Display an interactive dashboard of specs and changes')
|
|
252
|
-
.
|
|
287
|
+
.option('--store <id>', STORE_OPTION_DESCRIPTION)
|
|
288
|
+
.addOption(hiddenStorePathOption())
|
|
289
|
+
.action(async (options) => {
|
|
253
290
|
try {
|
|
291
|
+
// Implicit cwd fallback stays enabled so `view` keeps accepting the same
|
|
292
|
+
// directories as `list`/`status` — notably pre-config.yaml `openspec/`
|
|
293
|
+
// dirs. ViewCommand still reports a missing openspec/ directory itself.
|
|
294
|
+
const root = await resolveRootForCommand(options ?? {});
|
|
295
|
+
if (!root) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
254
298
|
const viewCommand = new ViewCommand();
|
|
255
|
-
await viewCommand.execute(
|
|
299
|
+
await viewCommand.execute(root.path);
|
|
256
300
|
}
|
|
257
301
|
catch (error) {
|
|
258
302
|
failWithError(error);
|
|
@@ -497,27 +541,24 @@ program
|
|
|
497
541
|
// Instructions command
|
|
498
542
|
program
|
|
499
543
|
.command('instructions [artifact]')
|
|
500
|
-
.description('Output enriched instructions for
|
|
544
|
+
.description('Output enriched instructions for artifacts, apply, archive, or modify')
|
|
501
545
|
.option('--change <id>', 'Change name')
|
|
502
546
|
.option('--schema <name>', 'Schema override (auto-detected from config.yaml)')
|
|
503
547
|
.option('--json', 'Output as JSON')
|
|
504
|
-
.option('--record-comprehension-pass', 'Record a successful comprehension quiz pass (use with instructions apply)')
|
|
505
|
-
.option('--score <percent>', 'Quiz score 0-100 (required with --record-comprehension-pass)', parseInt)
|
|
506
|
-
.option('--attempt <n>', 'Quiz attempt number', parseInt)
|
|
507
|
-
.option('--question-count <n>', 'Number of quiz questions taken', parseInt)
|
|
508
|
-
.option('--artifact <id>', 'Source artifact to modify (use with: instructions modify)')
|
|
509
|
-
.option('--workflow-input <text>', 'User modify request for telemetry (use with: instructions modify)')
|
|
510
|
-
.option('--workflow-input-file <path>', 'Read modify request from a file (use with: instructions modify)')
|
|
511
|
-
.option('--editor <tool>', 'AI editor used (cursor, windsurf, claude)')
|
|
512
548
|
.option('--store <id>', STORE_OPTION_DESCRIPTION)
|
|
513
549
|
.addOption(hiddenStorePathOption())
|
|
514
550
|
.action(async (artifactId, options) => {
|
|
515
551
|
try {
|
|
552
|
+
// Workflow instruction surfaces are reserved command branches, not artifacts.
|
|
516
553
|
if (artifactId === 'apply') {
|
|
517
554
|
await applyInstructionsCommand(options);
|
|
518
555
|
}
|
|
556
|
+
else if (artifactId === 'archive') {
|
|
557
|
+
await archiveInstructionsCommand(options);
|
|
558
|
+
}
|
|
519
559
|
else if (artifactId === 'modify') {
|
|
520
|
-
|
|
560
|
+
const modifyCommand = createModifyCommand();
|
|
561
|
+
modifyCommand.parse(process.argv.slice(2));
|
|
521
562
|
}
|
|
522
563
|
else {
|
|
523
564
|
await instructionsCommand(artifactId, options);
|
|
@@ -565,10 +606,6 @@ newCmd
|
|
|
565
606
|
.option('--description <text>', 'Description to add to README.md')
|
|
566
607
|
.option('--goal <text>', 'Optional goal metadata to store with the change')
|
|
567
608
|
.option('--schema <name>', `Workflow schema to use (default: ${DEFAULT_SCHEMA})`)
|
|
568
|
-
.option('--entry-point <point>', 'Workflow entry point (propose, new, ff, manual)', 'manual')
|
|
569
|
-
.option('--workflow-input <text>', 'User workflow intent for telemetry (verbatim chat/slash input)')
|
|
570
|
-
.option('--workflow-input-file <path>', 'Read workflow intent from a file for telemetry')
|
|
571
|
-
.option('--editor <tool>', 'AI editor used (cursor, windsurf, claude)')
|
|
572
609
|
.option('--json', 'Output as JSON')
|
|
573
610
|
.option('--store <id>', STORE_OPTION_DESCRIPTION)
|
|
574
611
|
.addOption(hiddenStorePathOption())
|
package/dist/commands/change.js
CHANGED
|
@@ -2,13 +2,31 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { JsonConverter } from '../core/converters/json-converter.js';
|
|
4
4
|
import { Validator } from '../core/validation/validator.js';
|
|
5
|
+
import { VALIDATION_MESSAGES } from '../core/validation/constants.js';
|
|
5
6
|
import { ChangeParser } from '../core/parsers/change-parser.js';
|
|
6
7
|
import { isInteractive } from '../utils/interactive.js';
|
|
7
8
|
import { getActiveChangeIds } from '../utils/item-discovery.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import { getTaskProgressForChange } from '../utils/task-progress.js';
|
|
10
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
11
|
+
/**
|
|
12
|
+
* True only when `target` is definitively absent. An EACCES or I/O failure
|
|
13
|
+
* means existence cannot be determined, so callers fall through to their
|
|
14
|
+
* read-error path rather than claim the file was never written.
|
|
15
|
+
*/
|
|
16
|
+
async function isDefinitelyMissing(target) {
|
|
17
|
+
return fs
|
|
18
|
+
.access(target)
|
|
19
|
+
.then(() => false)
|
|
20
|
+
.catch((error) => error?.code === 'ENOENT');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A change is a directory directly under changes/. Rejecting anything else up
|
|
24
|
+
* front keeps a traversing name (`../..`) from reading a proposal outside the
|
|
25
|
+
* changes directory, and keeps the missing-proposal message honest.
|
|
26
|
+
*/
|
|
27
|
+
function isChangeDirectoryName(changesPath, changeDir) {
|
|
28
|
+
return path.dirname(path.resolve(changeDir)) === path.resolve(changesPath);
|
|
29
|
+
}
|
|
12
30
|
export class ChangeCommand {
|
|
13
31
|
converter;
|
|
14
32
|
rootPath;
|
|
@@ -31,7 +49,8 @@ export class ChangeCommand {
|
|
|
31
49
|
const changesPath = this.getChangesPath();
|
|
32
50
|
if (!changeName) {
|
|
33
51
|
const canPrompt = isInteractive(options);
|
|
34
|
-
|
|
52
|
+
// Offer exactly the changes `show <name>` can resolve.
|
|
53
|
+
const changes = await getActiveChangeIds(this.rootPath ?? process.cwd());
|
|
35
54
|
if (canPrompt && changes.length > 0) {
|
|
36
55
|
const { select } = await import('@inquirer/prompts');
|
|
37
56
|
const selected = await select({
|
|
@@ -52,19 +71,40 @@ export class ChangeCommand {
|
|
|
52
71
|
return;
|
|
53
72
|
}
|
|
54
73
|
}
|
|
55
|
-
const
|
|
74
|
+
const changeDir = path.join(changesPath, changeName);
|
|
75
|
+
const proposalPath = path.join(changeDir, 'proposal.md');
|
|
76
|
+
if (!isChangeDirectoryName(changesPath, changeDir)) {
|
|
77
|
+
throw new Error(`Change "${changeName}" not found at ${proposalPath}`);
|
|
78
|
+
}
|
|
56
79
|
try {
|
|
57
80
|
await fs.access(proposalPath);
|
|
58
81
|
}
|
|
59
82
|
catch {
|
|
83
|
+
// A change can exist without a proposal: `openspec new change` scaffolds
|
|
84
|
+
// only .openspec.yaml, and a custom schema need not define a proposal
|
|
85
|
+
// artifact. Say which of the two cases this is instead of reporting a
|
|
86
|
+
// change that does exist as missing. A stray file under changes/ is not a
|
|
87
|
+
// change, and naming it one would point the user at a `status --change`
|
|
88
|
+
// call that cannot work.
|
|
89
|
+
const isChangeDirectory = await fs
|
|
90
|
+
.stat(changeDir)
|
|
91
|
+
.then((stats) => stats.isDirectory())
|
|
92
|
+
.catch(() => false);
|
|
93
|
+
if (isChangeDirectory) {
|
|
94
|
+
throw new Error(`Change "${changeName}" has no proposal.md yet. ` +
|
|
95
|
+
`Run "openspec status --change ${changeName}" to see which artifact comes next.`);
|
|
96
|
+
}
|
|
60
97
|
throw new Error(`Change "${changeName}" not found at ${proposalPath}`);
|
|
61
98
|
}
|
|
99
|
+
FileSystemUtils.assertPathWithin(path.dirname(proposalPath), proposalPath);
|
|
62
100
|
if (options?.json) {
|
|
101
|
+
FileSystemUtils.assertPathWithin(changeDir, proposalPath);
|
|
63
102
|
const jsonOutput = await this.converter.convertChangeToJson(proposalPath);
|
|
64
103
|
if (options.requirementsOnly) {
|
|
65
104
|
console.error('Flag --requirements-only is deprecated; use --deltas-only instead.');
|
|
66
105
|
}
|
|
67
106
|
const parsed = JSON.parse(jsonOutput);
|
|
107
|
+
FileSystemUtils.assertPathWithin(changeDir, proposalPath);
|
|
68
108
|
const contentForTitle = await fs.readFile(proposalPath, 'utf-8');
|
|
69
109
|
const title = this.extractTitle(contentForTitle, changeName);
|
|
70
110
|
const id = parsed.name;
|
|
@@ -79,6 +119,7 @@ export class ChangeCommand {
|
|
|
79
119
|
console.log(JSON.stringify(output, null, 2));
|
|
80
120
|
}
|
|
81
121
|
else {
|
|
122
|
+
FileSystemUtils.assertPathWithin(changeDir, proposalPath);
|
|
82
123
|
const content = await fs.readFile(proposalPath, 'utf-8');
|
|
83
124
|
console.log(content);
|
|
84
125
|
}
|
|
@@ -90,27 +131,31 @@ export class ChangeCommand {
|
|
|
90
131
|
*/
|
|
91
132
|
async list(options) {
|
|
92
133
|
const changesPath = path.join(process.cwd(), 'openspec', 'changes');
|
|
93
|
-
|
|
134
|
+
// Same directory-based resolution as `openspec list`, the command this
|
|
135
|
+
// deprecated alias points users at. Every output path below already
|
|
136
|
+
// tolerates a change whose proposal.md is missing or unreadable.
|
|
137
|
+
const changes = await getActiveChangeIds();
|
|
94
138
|
if (options?.json) {
|
|
95
139
|
const changeDetails = await Promise.all(changes.map(async (changeName) => {
|
|
96
|
-
const
|
|
97
|
-
const
|
|
140
|
+
const changeDir = path.join(changesPath, changeName);
|
|
141
|
+
const proposalPath = path.join(changeDir, 'proposal.md');
|
|
142
|
+
// Resolve task progress through the shared tracked-tasks helper so
|
|
143
|
+
// this deprecated noun-form list cannot re-fork the resolution
|
|
144
|
+
// (#1202). Tasks are independent of the proposal: a change can carry
|
|
145
|
+
// tasks before, or without, a proposal.md.
|
|
146
|
+
const taskStatus = await getTaskProgressForChange(changesPath, changeName, process.cwd());
|
|
147
|
+
// No proposal yet is an ordinary state (scaffolded change, or a
|
|
148
|
+
// schema with no proposal artifact), so name the change rather than
|
|
149
|
+
// labelling it Unknown. Unknown stays for a proposal that exists but
|
|
150
|
+
// cannot be read or parsed.
|
|
151
|
+
if (await isDefinitelyMissing(proposalPath)) {
|
|
152
|
+
return { id: changeName, title: changeName, deltaCount: 0, taskStatus };
|
|
153
|
+
}
|
|
98
154
|
try {
|
|
155
|
+
FileSystemUtils.assertPathWithin(changeDir, proposalPath);
|
|
99
156
|
const content = await fs.readFile(proposalPath, 'utf-8');
|
|
100
|
-
const changeDir = path.join(changesPath, changeName);
|
|
101
157
|
const parser = new ChangeParser(content, changeDir);
|
|
102
158
|
const change = await parser.parseChangeWithDeltas(changeName);
|
|
103
|
-
let taskStatus = { total: 0, completed: 0 };
|
|
104
|
-
try {
|
|
105
|
-
const tasksContent = await fs.readFile(tasksPath, 'utf-8');
|
|
106
|
-
taskStatus = this.countTasks(tasksContent);
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
// Tasks file may not exist, which is okay
|
|
110
|
-
if (process.env.DEBUG) {
|
|
111
|
-
console.error(`Failed to read tasks file at ${tasksPath}:`, error);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
159
|
return {
|
|
115
160
|
id: changeName,
|
|
116
161
|
title: this.extractTitle(content, changeName),
|
|
@@ -118,13 +163,8 @@ export class ChangeCommand {
|
|
|
118
163
|
taskStatus,
|
|
119
164
|
};
|
|
120
165
|
}
|
|
121
|
-
catch
|
|
122
|
-
return {
|
|
123
|
-
id: changeName,
|
|
124
|
-
title: 'Unknown',
|
|
125
|
-
deltaCount: 0,
|
|
126
|
-
taskStatus: { total: 0, completed: 0 },
|
|
127
|
-
};
|
|
166
|
+
catch {
|
|
167
|
+
return { id: changeName, title: 'Unknown', deltaCount: 0, taskStatus };
|
|
128
168
|
}
|
|
129
169
|
}));
|
|
130
170
|
const sorted = changeDetails.sort((a, b) => a.id.localeCompare(b.id));
|
|
@@ -143,30 +183,25 @@ export class ChangeCommand {
|
|
|
143
183
|
}
|
|
144
184
|
// Long format: id: title and minimal counts
|
|
145
185
|
for (const changeName of sorted) {
|
|
146
|
-
const
|
|
147
|
-
const
|
|
186
|
+
const changeDir = path.join(changesPath, changeName);
|
|
187
|
+
const proposalPath = path.join(changeDir, 'proposal.md');
|
|
188
|
+
const { total, completed } = await getTaskProgressForChange(changesPath, changeName, process.cwd());
|
|
189
|
+
const taskStatusText = total > 0 ? ` [tasks ${completed}/${total}]` : '';
|
|
190
|
+
if (await isDefinitelyMissing(proposalPath)) {
|
|
191
|
+
console.log(`${changeName}: (no proposal.md yet)${taskStatusText}`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
148
194
|
try {
|
|
195
|
+
FileSystemUtils.assertPathWithin(changeDir, proposalPath);
|
|
149
196
|
const content = await fs.readFile(proposalPath, 'utf-8');
|
|
150
197
|
const title = this.extractTitle(content, changeName);
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const tasksContent = await fs.readFile(tasksPath, 'utf-8');
|
|
154
|
-
const { total, completed } = this.countTasks(tasksContent);
|
|
155
|
-
taskStatusText = ` [tasks ${completed}/${total}]`;
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
if (process.env.DEBUG) {
|
|
159
|
-
console.error(`Failed to read tasks file at ${tasksPath}:`, error);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const changeDir = path.join(changesPath, changeName);
|
|
163
|
-
const parser = new ChangeParser(await fs.readFile(proposalPath, 'utf-8'), changeDir);
|
|
198
|
+
const parser = new ChangeParser(content, changeDir);
|
|
164
199
|
const change = await parser.parseChangeWithDeltas(changeName);
|
|
165
200
|
const deltaCountText = ` [deltas ${change.deltas.length}]`;
|
|
166
201
|
console.log(`${changeName}: ${title}${deltaCountText}${taskStatusText}`);
|
|
167
202
|
}
|
|
168
203
|
catch {
|
|
169
|
-
console.log(`${changeName}: (unable to read)`);
|
|
204
|
+
console.log(`${changeName}: (unable to read)${taskStatusText}`);
|
|
170
205
|
}
|
|
171
206
|
}
|
|
172
207
|
}
|
|
@@ -197,6 +232,9 @@ export class ChangeCommand {
|
|
|
197
232
|
}
|
|
198
233
|
}
|
|
199
234
|
const changeDir = path.join(changesPath, changeName);
|
|
235
|
+
if (!isChangeDirectoryName(changesPath, changeDir)) {
|
|
236
|
+
throw new Error(`Change "${changeName}" not found at ${changeDir}`);
|
|
237
|
+
}
|
|
200
238
|
try {
|
|
201
239
|
await fs.access(changeDir);
|
|
202
240
|
}
|
|
@@ -204,7 +242,12 @@ export class ChangeCommand {
|
|
|
204
242
|
throw new Error(`Change "${changeName}" not found at ${changeDir}`);
|
|
205
243
|
}
|
|
206
244
|
const validator = new Validator(options?.strict || false);
|
|
207
|
-
const report = await validator.validateChangeDeltaSpecs(changeDir
|
|
245
|
+
const report = await validator.validateChangeDeltaSpecs(changeDir, {
|
|
246
|
+
// Derived from changesPath so the main specs come from the same root the
|
|
247
|
+
// change itself was resolved against.
|
|
248
|
+
mainSpecsDir: path.join(path.dirname(changesPath), 'specs'),
|
|
249
|
+
projectRoot: path.dirname(path.dirname(changesPath)),
|
|
250
|
+
});
|
|
208
251
|
if (options?.json) {
|
|
209
252
|
console.log(JSON.stringify(report, null, 2));
|
|
210
253
|
}
|
|
@@ -220,58 +263,36 @@ export class ChangeCommand {
|
|
|
220
263
|
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
|
|
221
264
|
});
|
|
222
265
|
// Next steps footer to guide fixing issues
|
|
223
|
-
this.printNextSteps();
|
|
266
|
+
this.printNextSteps(report.issues);
|
|
224
267
|
if (!options?.json) {
|
|
225
268
|
process.exitCode = 1;
|
|
226
269
|
}
|
|
227
270
|
}
|
|
228
271
|
}
|
|
229
272
|
}
|
|
230
|
-
async getActiveChanges(changesPath) {
|
|
231
|
-
try {
|
|
232
|
-
const entries = await fs.readdir(changesPath, { withFileTypes: true });
|
|
233
|
-
const result = [];
|
|
234
|
-
for (const entry of entries) {
|
|
235
|
-
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === ARCHIVE_DIR)
|
|
236
|
-
continue;
|
|
237
|
-
const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
|
|
238
|
-
try {
|
|
239
|
-
await fs.access(proposalPath);
|
|
240
|
-
result.push(entry.name);
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
// skip directories without proposal.md
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return result.sort();
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
return [];
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
273
|
extractTitle(content, changeName) {
|
|
253
274
|
const match = content.match(/^#\s+(?:Change:\s+)?(.+)$/im);
|
|
254
275
|
return match ? match[1].trim() : changeName;
|
|
255
276
|
}
|
|
256
|
-
|
|
257
|
-
const lines = content.split('\n');
|
|
258
|
-
let total = 0;
|
|
259
|
-
let completed = 0;
|
|
260
|
-
for (const line of lines) {
|
|
261
|
-
if (line.match(TASK_PATTERN)) {
|
|
262
|
-
total++;
|
|
263
|
-
if (line.match(COMPLETED_TASK_PATTERN)) {
|
|
264
|
-
completed++;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return { total, completed };
|
|
269
|
-
}
|
|
270
|
-
printNextSteps() {
|
|
277
|
+
printNextSteps(issues = []) {
|
|
271
278
|
const bullets = [];
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
279
|
+
// Branch on the exact marker messages: the generic no-deltas guidance
|
|
280
|
+
// also mentions skip_specs and must not trigger the marker bullets.
|
|
281
|
+
const conflictIssue = issues.some(i => i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_CONFLICT));
|
|
282
|
+
const invalidMarkerIssue = issues.some(i => i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_INVALID_METADATA));
|
|
283
|
+
if (conflictIssue) {
|
|
284
|
+
bullets.push('- This change declares skip_specs (no spec deltas): delete the files under specs/, or remove skip_specs from .openspec.yaml if requirements do change');
|
|
285
|
+
bullets.push('- skip_specs is only honored when .openspec.yaml is valid change metadata (schema: <name> is required)');
|
|
286
|
+
}
|
|
287
|
+
else if (invalidMarkerIssue) {
|
|
288
|
+
bullets.push('- Fix .openspec.yaml so the skip_specs marker can be honored (schema: <name> is required)');
|
|
289
|
+
bullets.push('- Or remove skip_specs from .openspec.yaml and add delta specs instead');
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
|
|
293
|
+
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
|
|
294
|
+
bullets.push('- Debug parsed deltas: openspec change show <id> --json --deltas-only');
|
|
295
|
+
}
|
|
275
296
|
console.error('Next steps:');
|
|
276
297
|
bullets.forEach(b => console.error(` ${b}`));
|
|
277
298
|
}
|
package/dist/commands/config.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { spawn
|
|
2
|
-
import { trackEvent } from '../telemetry/index.js';
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
3
2
|
import * as fs from 'node:fs';
|
|
4
3
|
import * as path from 'node:path';
|
|
5
4
|
import { getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, } from '../core/global-config.js';
|
|
6
|
-
import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
|
|
5
|
+
import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, hasUnsafeKeySegment, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
|
|
7
6
|
import { CORE_WORKFLOWS, ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js';
|
|
8
7
|
import { OPENSPEC_DIR_NAME } from '../core/config.js';
|
|
9
8
|
import { hasProjectConfigDrift } from '../core/profile-sync-drift.js';
|
|
10
|
-
import {
|
|
9
|
+
import { UpdateCommand } from '../core/update.js';
|
|
10
|
+
import { asErrorMessage, isPromptCancellationError } from './shared-output.js';
|
|
11
11
|
const WORKFLOW_PROMPT_META = {
|
|
12
12
|
propose: {
|
|
13
13
|
name: 'Propose change',
|
|
@@ -234,11 +234,15 @@ export function registerConfigCommand(program) {
|
|
|
234
234
|
.action((key, value, options) => {
|
|
235
235
|
const allowUnknown = Boolean(options.allowUnknown);
|
|
236
236
|
const keyValidation = validateConfigKeyPath(key);
|
|
237
|
-
|
|
237
|
+
// --allow-unknown relaxes the known-key check, but never the prototype-safety check.
|
|
238
|
+
const unsafeKey = hasUnsafeKeySegment(key);
|
|
239
|
+
if (!keyValidation.valid && (!allowUnknown || unsafeKey)) {
|
|
238
240
|
const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
|
|
239
241
|
console.error(`Error: Invalid configuration key "${key}".${reason}`);
|
|
240
242
|
console.error('Use "openspec config list" to see available keys.');
|
|
241
|
-
|
|
243
|
+
if (!allowUnknown && !unsafeKey) {
|
|
244
|
+
console.error('Pass --allow-unknown to bypass this check.');
|
|
245
|
+
}
|
|
242
246
|
process.exitCode = 1;
|
|
243
247
|
return;
|
|
244
248
|
}
|
|
@@ -257,7 +261,6 @@ export function registerConfigCommand(program) {
|
|
|
257
261
|
// Apply changes and save
|
|
258
262
|
setNestedValue(config, key, coercedValue);
|
|
259
263
|
saveGlobalConfig(config);
|
|
260
|
-
trackEvent('config_value_set', { key });
|
|
261
264
|
const displayValue = typeof coercedValue === 'string' ? `"${coercedValue}"` : String(coercedValue);
|
|
262
265
|
console.log(`Set ${key} = ${displayValue}`);
|
|
263
266
|
});
|
|
@@ -519,12 +522,6 @@ export function registerConfigCommand(program) {
|
|
|
519
522
|
config.delivery = nextState.delivery;
|
|
520
523
|
config.workflows = nextState.workflows;
|
|
521
524
|
saveGlobalConfig(config);
|
|
522
|
-
trackEvent('config_profile_changed', {
|
|
523
|
-
profile: nextState.profile,
|
|
524
|
-
delivery: nextState.delivery,
|
|
525
|
-
workflow_count: nextState.workflows.length,
|
|
526
|
-
changes: diff.lines,
|
|
527
|
-
});
|
|
528
525
|
// Check if inside an OpenSpec project
|
|
529
526
|
const projectDir = process.cwd();
|
|
530
527
|
const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
|
|
@@ -535,11 +532,12 @@ export function registerConfigCommand(program) {
|
|
|
535
532
|
});
|
|
536
533
|
if (applyNow) {
|
|
537
534
|
try {
|
|
538
|
-
|
|
535
|
+
await new UpdateCommand().execute(projectDir);
|
|
539
536
|
console.log('Run `openspec update` in your other projects to apply.');
|
|
540
537
|
}
|
|
541
|
-
catch {
|
|
542
|
-
console.error(
|
|
538
|
+
catch (error) {
|
|
539
|
+
console.error(`\`openspec update\` failed: ${asErrorMessage(error)}`);
|
|
540
|
+
console.error('Please run it manually to apply the profile changes.');
|
|
543
541
|
process.exitCode = 1;
|
|
544
542
|
}
|
|
545
543
|
return;
|