@codewalla_india/openspec 1.3.0 → 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 -230
- 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 -323
- 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 -71
- 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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data sanitization module for telemetry.
|
|
3
|
+
*
|
|
4
|
+
* Sanitizes sensitive information from telemetry events:
|
|
5
|
+
* - Removes file paths from stack traces while preserving function names
|
|
6
|
+
* - Excludes file paths from event properties
|
|
7
|
+
* - Sanitizes artifact bodies for comprehension events
|
|
8
|
+
* - Ensures IP address exclusion consistency
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Sanitize a stack trace by removing file paths while preserving function names and error types.
|
|
12
|
+
*
|
|
13
|
+
* @param stackTrace - The raw stack trace string
|
|
14
|
+
* @returns The sanitized stack trace
|
|
15
|
+
*/
|
|
16
|
+
export function sanitizeStackTrace(stackTrace) {
|
|
17
|
+
if (!stackTrace) {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
// Remove file paths (both Unix and Windows styles)
|
|
21
|
+
// Pattern matches: /path/to/file.js:line:column or C:\path\to\file.js:line:column
|
|
22
|
+
const sanitized = stackTrace
|
|
23
|
+
.replace(/at\s+.*?\.js:\d+:\d+/g, 'at <redacted>')
|
|
24
|
+
.replace(/at\s+.*?\.ts:\d+:\d+/g, 'at <redacted>')
|
|
25
|
+
.replace(/at\s+.*?\.mjs:\d+:\d+/g, 'at <redacted>')
|
|
26
|
+
.replace(/at\s+.*?\.cjs:\d+:\d+/g, 'at <redacted>')
|
|
27
|
+
// Remove absolute paths in error messages
|
|
28
|
+
.replace(/[A-Za-z]:\\[^\\]+\\/g, '<redacted>')
|
|
29
|
+
.replace(/\/[^\/\s]+\/[^\/\s]+/g, '<redacted>');
|
|
30
|
+
return sanitized;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Sanitize an object by removing file paths from string properties.
|
|
34
|
+
*
|
|
35
|
+
* @param obj - The object to sanitize
|
|
36
|
+
* @returns The sanitized object
|
|
37
|
+
*/
|
|
38
|
+
export function sanitizeObject(obj) {
|
|
39
|
+
const sanitized = {};
|
|
40
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
41
|
+
if (typeof value === 'string') {
|
|
42
|
+
// Remove file paths from string values
|
|
43
|
+
sanitized[key] = sanitizeString(value);
|
|
44
|
+
}
|
|
45
|
+
else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
46
|
+
// Recursively sanitize nested objects
|
|
47
|
+
sanitized[key] = sanitizeObject(value);
|
|
48
|
+
}
|
|
49
|
+
else if (Array.isArray(value)) {
|
|
50
|
+
// Recursively sanitize array elements
|
|
51
|
+
sanitized[key] = value.map((item) => {
|
|
52
|
+
if (typeof item === 'string') {
|
|
53
|
+
return sanitizeString(item);
|
|
54
|
+
}
|
|
55
|
+
else if (typeof item === 'object' && item !== null) {
|
|
56
|
+
return sanitizeObject(item);
|
|
57
|
+
}
|
|
58
|
+
return item;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
sanitized[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return sanitized;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Sanitize a string by removing file paths.
|
|
69
|
+
*
|
|
70
|
+
* @param str - The string to sanitize
|
|
71
|
+
* @returns The sanitized string
|
|
72
|
+
*/
|
|
73
|
+
function sanitizeString(str) {
|
|
74
|
+
// Remove absolute file paths (both Unix and Windows)
|
|
75
|
+
return str
|
|
76
|
+
.replace(/[A-Za-z]:\\[^\\]+\\/g, '<redacted>')
|
|
77
|
+
.replace(/\/[^\/\s]+\/[^\/\s]+/g, '<redacted>');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Sanitize artifact body content for comprehension events.
|
|
81
|
+
*
|
|
82
|
+
* Removes file paths and sensitive information while preserving code structure.
|
|
83
|
+
*
|
|
84
|
+
* @param content - The artifact body content
|
|
85
|
+
* @returns The sanitized artifact body
|
|
86
|
+
*/
|
|
87
|
+
export function sanitizeArtifactBody(content) {
|
|
88
|
+
if (!content) {
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
// Remove file paths in comments and strings
|
|
92
|
+
const lines = content.split('\n');
|
|
93
|
+
const sanitizedLines = lines.map((line) => {
|
|
94
|
+
// Remove file paths from import statements and require calls
|
|
95
|
+
if (line.includes('from ') || line.includes('require(') || line.includes('import ')) {
|
|
96
|
+
return sanitizeString(line);
|
|
97
|
+
}
|
|
98
|
+
return line;
|
|
99
|
+
});
|
|
100
|
+
return sanitizedLines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Ensure IP address exclusion by setting $ip to null in event properties.
|
|
104
|
+
*
|
|
105
|
+
* @param properties - The event properties object
|
|
106
|
+
* @returns The properties with $ip set to null
|
|
107
|
+
*/
|
|
108
|
+
export function ensureIpExclusion(properties) {
|
|
109
|
+
return {
|
|
110
|
+
...properties,
|
|
111
|
+
$ip: null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=sanitization.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the path to the throttling state file.
|
|
3
|
+
*
|
|
4
|
+
* @returns Path to the throttling state file
|
|
5
|
+
*/
|
|
6
|
+
export declare function getThrottleFilePath(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Check if $identify call should be throttled based on 24-hour window.
|
|
9
|
+
*
|
|
10
|
+
* @param identity - The identity to check throttling for
|
|
11
|
+
* @returns True if $identify should be throttled, false otherwise
|
|
12
|
+
*/
|
|
13
|
+
export declare function shouldThrottleIdentify(identity: string): Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Record timestamp of $identify call for throttling.
|
|
16
|
+
*
|
|
17
|
+
* @param identity - The identity to record the call for
|
|
18
|
+
*/
|
|
19
|
+
export declare function recordIdentifyCall(identity: string): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=throttling.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throttling module for $identify calls.
|
|
3
|
+
*
|
|
4
|
+
* Implements local state persistence to throttle $identify calls to once per 24 hours.
|
|
5
|
+
*/
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { getGlobalConfigDir } from '../core/global-config.js';
|
|
9
|
+
// Constants
|
|
10
|
+
const THROTTLE_FILE_NAME = 'telemetry-identify-throttle.json';
|
|
11
|
+
const THROTTLE_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
|
12
|
+
/**
|
|
13
|
+
* Get the path to the throttling state file.
|
|
14
|
+
*
|
|
15
|
+
* @returns Path to the throttling state file
|
|
16
|
+
*/
|
|
17
|
+
export function getThrottleFilePath() {
|
|
18
|
+
const configDir = getGlobalConfigDir();
|
|
19
|
+
return path.join(configDir, THROTTLE_FILE_NAME);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if $identify call should be throttled based on 24-hour window.
|
|
23
|
+
*
|
|
24
|
+
* @param identity - The identity to check throttling for
|
|
25
|
+
* @returns True if $identify should be throttled, false otherwise
|
|
26
|
+
*/
|
|
27
|
+
export async function shouldThrottleIdentify(identity) {
|
|
28
|
+
const throttlePath = getThrottleFilePath();
|
|
29
|
+
try {
|
|
30
|
+
const content = await fs.readFile(throttlePath, 'utf-8');
|
|
31
|
+
const state = JSON.parse(content);
|
|
32
|
+
const lastIdentifyTime = state[identity];
|
|
33
|
+
if (!lastIdentifyTime) {
|
|
34
|
+
return false; // No previous call, don't throttle
|
|
35
|
+
}
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const timeSinceLastCall = now - lastIdentifyTime;
|
|
38
|
+
return timeSinceLastCall < THROTTLE_WINDOW_MS;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// If file doesn't exist or is invalid, don't throttle
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Record timestamp of $identify call for throttling.
|
|
47
|
+
*
|
|
48
|
+
* @param identity - The identity to record the call for
|
|
49
|
+
*/
|
|
50
|
+
export async function recordIdentifyCall(identity) {
|
|
51
|
+
const throttlePath = getThrottleFilePath();
|
|
52
|
+
const throttleDir = path.dirname(throttlePath);
|
|
53
|
+
// Create directory if it doesn't exist
|
|
54
|
+
await fs.mkdir(throttleDir, { recursive: true });
|
|
55
|
+
// Read existing state or create new
|
|
56
|
+
let state = {};
|
|
57
|
+
try {
|
|
58
|
+
const content = await fs.readFile(throttlePath, 'utf-8');
|
|
59
|
+
state = JSON.parse(content);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// File doesn't exist or is invalid, start fresh
|
|
63
|
+
state = {};
|
|
64
|
+
}
|
|
65
|
+
// Update timestamp for this identity
|
|
66
|
+
state[identity] = Date.now();
|
|
67
|
+
// Write back to file
|
|
68
|
+
await fs.writeFile(throttlePath, JSON.stringify(state, null, 2) + '\n');
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=throttling.js.map
|
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
* Animated welcome screen for the experimental artifact workflow setup.
|
|
3
3
|
* Shows side-by-side layout with animated ASCII art on left and welcome text on right.
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Best-effort check of the OS-level reduced-motion preference (#722).
|
|
7
|
+
* Any lookup failure (missing binary, unset key, timeout) means
|
|
8
|
+
* "no preference detected" and animation stays enabled.
|
|
9
|
+
*/
|
|
10
|
+
export declare function prefersReducedMotion(platform?: NodeJS.Platform): boolean;
|
|
5
11
|
/**
|
|
6
12
|
* Shows the animated welcome screen.
|
|
7
13
|
* Returns when user presses Enter.
|
|
8
14
|
*/
|
|
9
|
-
export declare function showWelcomeScreen(
|
|
15
|
+
export declare function showWelcomeScreen(workflows: readonly string[], options?: {
|
|
16
|
+
animate?: boolean;
|
|
17
|
+
}): Promise<void>;
|
|
10
18
|
//# sourceMappingURL=welcome-screen.d.ts.map
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* Shows side-by-side layout with animated ASCII art on left and welcome text on right.
|
|
4
4
|
*/
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
+
import { execFileSync, } from 'node:child_process';
|
|
6
7
|
import { WELCOME_ANIMATION } from './ascii-patterns.js';
|
|
8
|
+
import { getOnboardingCommands } from '../core/onboarding-commands.js';
|
|
7
9
|
// Minimum terminal width for side-by-side layout
|
|
8
10
|
const MIN_WIDTH = 60;
|
|
9
11
|
// Width of the ASCII art column (with padding)
|
|
@@ -11,20 +13,35 @@ const ART_COLUMN_WIDTH = 24;
|
|
|
11
13
|
/**
|
|
12
14
|
* Welcome text content (right column)
|
|
13
15
|
*/
|
|
14
|
-
function getWelcomeText() {
|
|
16
|
+
function getWelcomeText(workflows) {
|
|
17
|
+
const onboardingCommands = getOnboardingCommands(workflows);
|
|
18
|
+
const quickStart = [];
|
|
19
|
+
if (onboardingCommands.length > 0) {
|
|
20
|
+
const commandWidth = Math.max(...onboardingCommands.map((c) => c.command.length));
|
|
21
|
+
quickStart.push(chalk.white('Quick start after setup:'));
|
|
22
|
+
for (const { command, description } of onboardingCommands) {
|
|
23
|
+
quickStart.push(` ${chalk.yellow(command.padEnd(commandWidth + 1))} ${chalk.dim(description)}`);
|
|
24
|
+
}
|
|
25
|
+
// These are the canonical names. How each tool spells them differs
|
|
26
|
+
// (/opsx-propose, @opsx-propose, $openspec-propose ...) and cannot be known
|
|
27
|
+
// until tools are picked, one prompt later — so flag it rather than let the
|
|
28
|
+
// canonical form read as the literal thing to type. "Getting started"
|
|
29
|
+
// prints the real spelling once the selection is known.
|
|
30
|
+
quickStart.push(chalk.dim(' (spelling varies by tool)'));
|
|
31
|
+
quickStart.push('');
|
|
32
|
+
}
|
|
15
33
|
return [
|
|
16
34
|
chalk.white.bold('Welcome to OpenSpec'),
|
|
17
35
|
chalk.dim('A lightweight spec-driven framework'),
|
|
18
36
|
'',
|
|
19
37
|
chalk.white('This setup will configure:'),
|
|
20
38
|
chalk.dim(' • Agent Skills for AI tools'),
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
` ${chalk.yellow('/opsx:continue')} ${chalk.dim('Next artifact')}`,
|
|
26
|
-
` ${chalk.yellow('/opsx:apply')} ${chalk.dim('Implement tasks')}`,
|
|
39
|
+
// Not "opsx slash commands": this screen runs before tool selection, and
|
|
40
|
+
// skills-only tools (Codex, Kimi Code, ...) correctly get no command files
|
|
41
|
+
// at all. The exact spelling per tool is printed in "Getting started".
|
|
42
|
+
chalk.dim(' • Workflow commands, if supported'),
|
|
27
43
|
'',
|
|
44
|
+
...quickStart,
|
|
28
45
|
chalk.cyan('Press Enter to select tools...'),
|
|
29
46
|
];
|
|
30
47
|
}
|
|
@@ -46,6 +63,36 @@ function renderFrame(artLines, textLines) {
|
|
|
46
63
|
}
|
|
47
64
|
return lines.join('\n');
|
|
48
65
|
}
|
|
66
|
+
const REDUCED_MOTION_EXEC_OPTIONS = {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
timeout: 500,
|
|
69
|
+
// SIGKILL so a wedged lookup can never outlive the timeout and stall init.
|
|
70
|
+
killSignal: 'SIGKILL',
|
|
71
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Best-effort check of the OS-level reduced-motion preference (#722).
|
|
75
|
+
* Any lookup failure (missing binary, unset key, timeout) means
|
|
76
|
+
* "no preference detected" and animation stays enabled.
|
|
77
|
+
*/
|
|
78
|
+
export function prefersReducedMotion(platform = process.platform) {
|
|
79
|
+
try {
|
|
80
|
+
if (platform === 'darwin') {
|
|
81
|
+
// The key only exists once the user has toggled Reduce Motion; when it
|
|
82
|
+
// is unset `defaults` exits non-zero and lands in the catch below.
|
|
83
|
+
const out = execFileSync('defaults', ['read', 'com.apple.universalaccess', 'reduceMotion'], REDUCED_MOTION_EXEC_OPTIONS);
|
|
84
|
+
return out.trim() === '1';
|
|
85
|
+
}
|
|
86
|
+
if (platform === 'linux') {
|
|
87
|
+
const out = execFileSync('gsettings', ['get', 'org.gnome.desktop.interface', 'enable-animations'], REDUCED_MOTION_EXEC_OPTIONS);
|
|
88
|
+
return out.trim() === 'false';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Detection is best-effort only.
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
49
96
|
/**
|
|
50
97
|
* Checks if the terminal supports animation
|
|
51
98
|
*/
|
|
@@ -56,54 +103,60 @@ function canAnimate() {
|
|
|
56
103
|
// Respect NO_COLOR
|
|
57
104
|
if (process.env.NO_COLOR)
|
|
58
105
|
return false;
|
|
106
|
+
// Manual override for users who need reduced motion (#722). Presence is
|
|
107
|
+
// what counts: even an empty value disables the animation.
|
|
108
|
+
if (process.env.OPENSPEC_NO_ANIMATION !== undefined)
|
|
109
|
+
return false;
|
|
59
110
|
// Check terminal width
|
|
60
111
|
const columns = process.stdout.columns || 80;
|
|
61
112
|
if (columns < MIN_WIDTH)
|
|
62
113
|
return false;
|
|
114
|
+
// Last so only interactive terminals pay for the OS lookup
|
|
115
|
+
if (prefersReducedMotion())
|
|
116
|
+
return false;
|
|
63
117
|
return true;
|
|
64
118
|
}
|
|
65
119
|
/**
|
|
66
120
|
* Wait for Enter key press
|
|
67
121
|
*/
|
|
68
|
-
function waitForEnter() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
stdin.removeListener('data', onData);
|
|
84
|
-
stdin.setRawMode(wasRaw);
|
|
85
|
-
stdin.pause();
|
|
86
|
-
// Handle Ctrl+C
|
|
87
|
-
if (char === '\u0003') {
|
|
88
|
-
process.stdout.write('\n');
|
|
89
|
-
process.exit(0);
|
|
90
|
-
}
|
|
91
|
-
resolve();
|
|
122
|
+
async function waitForEnter() {
|
|
123
|
+
if (!process.stdin.isTTY) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Keep all interactive input on Inquirer's keypress lifecycle. Mixing a raw
|
|
127
|
+
// `data` listener between Inquirer prompts breaks arrow/space keys on Windows.
|
|
128
|
+
const { createPrompt, isEnterKey, useKeypress } = await import('@inquirer/core');
|
|
129
|
+
const prompt = createPrompt((_config, done) => {
|
|
130
|
+
useKeypress((key) => {
|
|
131
|
+
if (key.ctrl && key.name === 'c') {
|
|
132
|
+
process.stdout.write('\n');
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
if (isEnterKey(key)) {
|
|
136
|
+
done(undefined);
|
|
92
137
|
}
|
|
93
|
-
};
|
|
94
|
-
|
|
138
|
+
});
|
|
139
|
+
return '';
|
|
95
140
|
});
|
|
141
|
+
await prompt({});
|
|
96
142
|
}
|
|
97
143
|
/**
|
|
98
144
|
* Shows the animated welcome screen.
|
|
99
145
|
* Returns when user presses Enter.
|
|
100
146
|
*/
|
|
101
|
-
export async function showWelcomeScreen() {
|
|
102
|
-
const textLines = getWelcomeText();
|
|
103
|
-
if (!canAnimate()) {
|
|
104
|
-
// Fallback: show static welcome
|
|
147
|
+
export async function showWelcomeScreen(workflows, options = {}) {
|
|
148
|
+
const textLines = getWelcomeText(workflows);
|
|
149
|
+
if (options.animate === false || !canAnimate()) {
|
|
150
|
+
// Fallback: show static welcome. The "Press Enter" line is only honest
|
|
151
|
+
// when we actually wait; in a TTY, returning immediately would let the
|
|
152
|
+
// Enter it asks for fall through into the tool picker and submit the
|
|
153
|
+
// pre-selected tools sight-unseen. Without a TTY, drop the line instead.
|
|
154
|
+
const staticLines = process.stdin.isTTY
|
|
155
|
+
? textLines
|
|
156
|
+
: textLines.filter((line) => !line.includes('Press Enter'));
|
|
105
157
|
const frame = WELCOME_ANIMATION.frames[3]; // Peak frame
|
|
106
|
-
process.stdout.write('\n' + renderFrame(frame,
|
|
158
|
+
process.stdout.write('\n' + renderFrame(frame, staticLines) + '\n\n');
|
|
159
|
+
await waitForEnter();
|
|
107
160
|
return;
|
|
108
161
|
}
|
|
109
162
|
let frameIndex = 0;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ChangeMetadata } from '../core/change-metadata/index.js';
|
|
2
|
+
import { type ProjectConfig } from '../core/project-config.js';
|
|
2
3
|
export declare const METADATA_FILENAME = ".openspec.yaml";
|
|
3
4
|
/**
|
|
4
5
|
* Error thrown when change metadata validation fails.
|
|
@@ -37,6 +38,8 @@ export declare function writeChangeMetadata(changeDir: string, metadata: ChangeM
|
|
|
37
38
|
export declare function readChangeMetadata(changeDir: string, projectRoot?: string): ChangeMetadata | null;
|
|
38
39
|
export interface ResolveSchemaForChangeOptions {
|
|
39
40
|
metadata?: ChangeMetadata | null;
|
|
41
|
+
/** Pre-read project config; suppresses the fallback config read when provided. */
|
|
42
|
+
projectConfig?: ProjectConfig | null;
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
42
45
|
* Resolves the schema for a change, with explicit override taking precedence.
|
|
@@ -52,4 +55,44 @@ export interface ResolveSchemaForChangeOptions {
|
|
|
52
55
|
* @returns The resolved schema name
|
|
53
56
|
*/
|
|
54
57
|
export declare function resolveSchemaForChange(changeDir: string, explicitSchema?: string, projectRootOverride?: string, options?: ResolveSchemaForChangeOptions): string;
|
|
58
|
+
export interface MetadataMarker {
|
|
59
|
+
/**
|
|
60
|
+
* True when the metadata parses under ChangeMetadataSchema, sets
|
|
61
|
+
* the requested boolean marker to true, and names a schema that loads.
|
|
62
|
+
*/
|
|
63
|
+
declared: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Set when the marker cannot be honored: it appears in a file that
|
|
66
|
+
* fails the metadata contract, or the metadata file exists but cannot be
|
|
67
|
+
* read at all (so whether the marker is set cannot even be determined).
|
|
68
|
+
*/
|
|
69
|
+
invalidReason?: string;
|
|
70
|
+
}
|
|
71
|
+
/** @deprecated Use MetadataMarker. */
|
|
72
|
+
export type SkipSpecsMarker = MetadataMarker;
|
|
73
|
+
/**
|
|
74
|
+
* Non-throwing read of the skip_specs marker. The marker only counts when the
|
|
75
|
+
* metadata would load for status/instructions: the file parses under
|
|
76
|
+
* ChangeMetadataSchema, its schema name passes readChangeMetadata's
|
|
77
|
+
* listSchemas membership check, AND the schema itself loads via resolveSchema
|
|
78
|
+
* (a schema.yaml that exists but does not parse fails status just the same).
|
|
79
|
+
* Validate and archive must never honor metadata the rest of the CLI rejects,
|
|
80
|
+
* in either direction. The project root for schema resolution is derived from
|
|
81
|
+
* changeDir exactly like resolveSchemaForChange (changeDir is
|
|
82
|
+
* <root>/openspec/changes/<name> for every root type, including store roots).
|
|
83
|
+
* Missing metadata means "not declared"; a marker that cannot be honored
|
|
84
|
+
* yields invalidReason so callers can say why.
|
|
85
|
+
*/
|
|
86
|
+
export declare function readSkipSpecsMarker(changeDir: string): MetadataMarker;
|
|
87
|
+
/**
|
|
88
|
+
* Non-throwing read of the retire_capabilities marker, with exactly the
|
|
89
|
+
* semantics `readSkipSpecsMarker` documents above.
|
|
90
|
+
*
|
|
91
|
+
* Gates the one archive action that removes a file from `openspec/specs/`: when
|
|
92
|
+
* a change's REMOVED entries take a capability's last requirement, archive
|
|
93
|
+
* deletes the emptied main spec rather than aborting on a spec it cannot write
|
|
94
|
+
* (#1302). Declared rather than inferred because the delete is recoverable only
|
|
95
|
+
* from git, so it is the author's call.
|
|
96
|
+
*/
|
|
97
|
+
export declare function readRetireCapabilitiesMarker(changeDir: string): MetadataMarker;
|
|
55
98
|
//# sourceMappingURL=change-metadata.d.ts.map
|
|
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as yaml from 'yaml';
|
|
4
4
|
import { ChangeMetadataSchema } from '../core/change-metadata/index.js';
|
|
5
|
-
import { listSchemas } from '../core/artifact-graph/resolver.js';
|
|
5
|
+
import { listSchemas, resolveSchema } from '../core/artifact-graph/resolver.js';
|
|
6
6
|
import { readProjectConfig } from '../core/project-config.js';
|
|
7
7
|
export const METADATA_FILENAME = '.openspec.yaml';
|
|
8
8
|
/**
|
|
@@ -126,16 +126,132 @@ export function resolveSchemaForChange(changeDir, explicitSchema, projectRootOve
|
|
|
126
126
|
return metadata.schema;
|
|
127
127
|
}
|
|
128
128
|
// 3. Try reading from project config when metadata is absent.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return config.schema;
|
|
129
|
+
if (options.projectConfig !== undefined) {
|
|
130
|
+
if (options.projectConfig?.schema) {
|
|
131
|
+
return options.projectConfig.schema;
|
|
133
132
|
}
|
|
134
133
|
}
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
else {
|
|
135
|
+
try {
|
|
136
|
+
const config = readProjectConfig(projectRoot);
|
|
137
|
+
if (config?.schema) {
|
|
138
|
+
return config.schema;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// If config read fails, fall back to default
|
|
143
|
+
}
|
|
137
144
|
}
|
|
138
145
|
// 4. Default
|
|
139
146
|
return 'spec-driven';
|
|
140
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Non-throwing read of the skip_specs marker. The marker only counts when the
|
|
150
|
+
* metadata would load for status/instructions: the file parses under
|
|
151
|
+
* ChangeMetadataSchema, its schema name passes readChangeMetadata's
|
|
152
|
+
* listSchemas membership check, AND the schema itself loads via resolveSchema
|
|
153
|
+
* (a schema.yaml that exists but does not parse fails status just the same).
|
|
154
|
+
* Validate and archive must never honor metadata the rest of the CLI rejects,
|
|
155
|
+
* in either direction. The project root for schema resolution is derived from
|
|
156
|
+
* changeDir exactly like resolveSchemaForChange (changeDir is
|
|
157
|
+
* <root>/openspec/changes/<name> for every root type, including store roots).
|
|
158
|
+
* Missing metadata means "not declared"; a marker that cannot be honored
|
|
159
|
+
* yields invalidReason so callers can say why.
|
|
160
|
+
*/
|
|
161
|
+
export function readSkipSpecsMarker(changeDir) {
|
|
162
|
+
return readBooleanMarker(changeDir, 'skip_specs');
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Non-throwing read of the retire_capabilities marker, with exactly the
|
|
166
|
+
* semantics `readSkipSpecsMarker` documents above.
|
|
167
|
+
*
|
|
168
|
+
* Gates the one archive action that removes a file from `openspec/specs/`: when
|
|
169
|
+
* a change's REMOVED entries take a capability's last requirement, archive
|
|
170
|
+
* deletes the emptied main spec rather than aborting on a spec it cannot write
|
|
171
|
+
* (#1302). Declared rather than inferred because the delete is recoverable only
|
|
172
|
+
* from git, so it is the author's call.
|
|
173
|
+
*/
|
|
174
|
+
export function readRetireCapabilitiesMarker(changeDir) {
|
|
175
|
+
return readBooleanMarker(changeDir, 'retire_capabilities');
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Shared implementation for the boolean change-metadata markers, keyed by field
|
|
179
|
+
* name. One body rather than two, so a marker can never drift into honoring
|
|
180
|
+
* metadata the other rejects - the whole point of the contract described above.
|
|
181
|
+
*/
|
|
182
|
+
function readBooleanMarker(changeDir, key) {
|
|
183
|
+
let raw;
|
|
184
|
+
try {
|
|
185
|
+
raw = fs.readFileSync(path.join(changeDir, METADATA_FILENAME), 'utf-8');
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
if (err?.code === 'ENOENT') {
|
|
189
|
+
return { declared: false };
|
|
190
|
+
}
|
|
191
|
+
// The file exists but cannot be read (EACCES, EISDIR, ...). Status and
|
|
192
|
+
// instructions reject the change outright here, and whether a marker is
|
|
193
|
+
// set cannot be determined - fail closed rather than let archive treat
|
|
194
|
+
// the change as unmarked while every metadata-reading surface errors.
|
|
195
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
196
|
+
return {
|
|
197
|
+
declared: false,
|
|
198
|
+
invalidReason: `the metadata file cannot be read (${message})`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
let parsed;
|
|
202
|
+
try {
|
|
203
|
+
parsed = yaml.parse(raw);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Anchored so a comment like "# maybe add skip_specs later" does not
|
|
207
|
+
// claim the marker was set.
|
|
208
|
+
const mentioned = new RegExp(`^\\s*(['"]?)${key}\\1\\s*:`, 'm').test(raw);
|
|
209
|
+
return mentioned
|
|
210
|
+
? { declared: false, invalidReason: 'the file is not valid YAML' }
|
|
211
|
+
: { declared: false };
|
|
212
|
+
}
|
|
213
|
+
const result = ChangeMetadataSchema.safeParse(parsed);
|
|
214
|
+
if (result.success) {
|
|
215
|
+
if (result.data[key] !== true) {
|
|
216
|
+
return { declared: false };
|
|
217
|
+
}
|
|
218
|
+
// Schema loading is checked only when the marker is set: a broken schema
|
|
219
|
+
// on an ordinary change is status's problem to report, but honoring a
|
|
220
|
+
// marker that status rejects would let validate/archive pass what the
|
|
221
|
+
// rest of the CLI refuses to load. The membership check mirrors
|
|
222
|
+
// readChangeMetadata (which rejects names like 'spec-driven.yaml' that
|
|
223
|
+
// resolveSchema alone would normalize and accept); resolveSchema then
|
|
224
|
+
// proves the schema actually parses. Any failure fails closed.
|
|
225
|
+
try {
|
|
226
|
+
const projectRoot = path.resolve(changeDir, '../../..');
|
|
227
|
+
if (!listSchemas(projectRoot).includes(result.data.schema)) {
|
|
228
|
+
return {
|
|
229
|
+
declared: false,
|
|
230
|
+
invalidReason: `schema: unknown schema '${result.data.schema}'`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
resolveSchema(result.data.schema, projectRoot);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
237
|
+
return { declared: false, invalidReason: message };
|
|
238
|
+
}
|
|
239
|
+
return { declared: true };
|
|
240
|
+
}
|
|
241
|
+
// Key presence, not value: skip_specs: "yes" must surface as unhonorable,
|
|
242
|
+
// not vanish while the zero-delta guidance tells the user to set the very
|
|
243
|
+
// marker they set. An explicit skip_specs: false is the opposite of setting
|
|
244
|
+
// the marker, so it must not drag unrelated metadata problems into
|
|
245
|
+
// validate - the change simply is not marked.
|
|
246
|
+
const markerMentioned = typeof parsed === 'object' &&
|
|
247
|
+
parsed !== null &&
|
|
248
|
+
key in parsed &&
|
|
249
|
+
parsed[key] !== false;
|
|
250
|
+
if (markerMentioned) {
|
|
251
|
+
const first = result.error.issues[0];
|
|
252
|
+
const where = first.path.length > 0 ? `${first.path.join('.')}: ` : '';
|
|
253
|
+
return { declared: false, invalidReason: `${where}${first.message}` };
|
|
254
|
+
}
|
|
255
|
+
return { declared: false };
|
|
256
|
+
}
|
|
141
257
|
//# sourceMappingURL=change-metadata.js.map
|
|
@@ -31,17 +31,23 @@ export interface ValidationResult {
|
|
|
31
31
|
/**
|
|
32
32
|
* Validates that a change name follows kebab-case conventions.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* Uses OpenSpec's shared kebab-id grammar (the same one store ids and change
|
|
35
|
+
* metadata ids use), so a change name may:
|
|
36
|
+
* - Start with a lowercase letter or a digit
|
|
36
37
|
* - Contain only lowercase letters, numbers, and hyphens
|
|
37
|
-
* -
|
|
38
|
-
* -
|
|
38
|
+
* - Not start or end with a hyphen
|
|
39
|
+
* - Not contain consecutive hyphens
|
|
40
|
+
*
|
|
41
|
+
* A leading digit is allowed so ordering conventions like `100-add-feature` or
|
|
42
|
+
* `00001-add-auth` work; archive already treats such prefixes as a supported
|
|
43
|
+
* convention (see ARCHIVE_DATE_PREFIX_PATTERN).
|
|
39
44
|
*
|
|
40
45
|
* @param name - The change name to validate
|
|
41
46
|
* @returns Validation result with `valid: true` or `valid: false` with an error message
|
|
42
47
|
*
|
|
43
48
|
* @example
|
|
44
49
|
* validateChangeName('add-auth') // { valid: true }
|
|
50
|
+
* validateChangeName('100-add-feature') // { valid: true }
|
|
45
51
|
* validateChangeName('Add-Auth') // { valid: false, error: '...' }
|
|
46
52
|
*/
|
|
47
53
|
export declare function validateChangeName(name: string): ValidationResult;
|