@niujiajie/freespec 0.0.0-stage → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +58 -2
- package/bin/freespec.js +5 -0
- package/dist/cli/index.d.ts +25 -0
- package/dist/cli/index.js +359 -0
- package/dist/commands/shared-output.d.ts +18 -0
- package/dist/commands/shared-output.js +61 -0
- package/dist/commands/workflow/default-schema.d.ts +2 -0
- package/dist/commands/workflow/default-schema.js +4 -0
- package/dist/commands/workflow/index.d.ts +13 -0
- package/dist/commands/workflow/index.js +10 -0
- package/dist/commands/workflow/instructions.d.ts +39 -0
- package/dist/commands/workflow/instructions.js +565 -0
- package/dist/commands/workflow/new-change.d.ts +19 -0
- package/dist/commands/workflow/new-change.js +135 -0
- package/dist/commands/workflow/shared.d.ts +94 -0
- package/dist/commands/workflow/shared.js +169 -0
- package/dist/commands/workflow/status.d.ts +18 -0
- package/dist/commands/workflow/status.js +201 -0
- package/dist/core/artifact-graph/graph.d.ts +72 -0
- package/dist/core/artifact-graph/graph.js +165 -0
- package/dist/core/artifact-graph/index.d.ts +9 -0
- package/dist/core/artifact-graph/index.js +14 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +208 -0
- package/dist/core/artifact-graph/instruction-loader.js +268 -0
- package/dist/core/artifact-graph/outputs.d.ts +19 -0
- package/dist/core/artifact-graph/outputs.js +351 -0
- package/dist/core/artifact-graph/resolver.d.ts +89 -0
- package/dist/core/artifact-graph/resolver.js +342 -0
- package/dist/core/artifact-graph/schema.d.ts +27 -0
- package/dist/core/artifact-graph/schema.js +160 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +31 -0
- package/dist/core/artifact-graph/types.d.ts +40 -0
- package/dist/core/artifact-graph/types.js +59 -0
- package/dist/core/available-tools.d.ts +18 -0
- package/dist/core/available-tools.js +49 -0
- package/dist/core/change-layout.d.ts +17 -0
- package/dist/core/change-layout.js +30 -0
- package/dist/core/change-metadata/index.d.ts +2 -0
- package/dist/core/change-metadata/index.js +2 -0
- package/dist/core/change-metadata/schema.d.ts +23 -0
- package/dist/core/change-metadata/schema.js +46 -0
- package/dist/core/change-status-policy.d.ts +49 -0
- package/dist/core/change-status-policy.js +50 -0
- package/dist/core/command-generation/adapters/amazon-q.d.ts +17 -0
- package/dist/core/command-generation/adapters/amazon-q.js +32 -0
- package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
- package/dist/core/command-generation/adapters/antigravity.js +27 -0
- package/dist/core/command-generation/adapters/atomcode.d.ts +24 -0
- package/dist/core/command-generation/adapters/atomcode.js +46 -0
- package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
- package/dist/core/command-generation/adapters/auggie.js +28 -0
- package/dist/core/command-generation/adapters/bob.d.ts +18 -0
- package/dist/core/command-generation/adapters/bob.js +33 -0
- package/dist/core/command-generation/adapters/claude.d.ts +13 -0
- package/dist/core/command-generation/adapters/claude.js +32 -0
- package/dist/core/command-generation/adapters/cline.d.ts +14 -0
- package/dist/core/command-generation/adapters/cline.js +27 -0
- package/dist/core/command-generation/adapters/codeassistant.d.ts +15 -0
- package/dist/core/command-generation/adapters/codeassistant.js +29 -0
- package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
- package/dist/core/command-generation/adapters/codebuddy.js +29 -0
- package/dist/core/command-generation/adapters/codestudio.d.ts +13 -0
- package/dist/core/command-generation/adapters/codestudio.js +27 -0
- package/dist/core/command-generation/adapters/command-code.d.ts +19 -0
- package/dist/core/command-generation/adapters/command-code.js +34 -0
- package/dist/core/command-generation/adapters/continue.d.ts +13 -0
- package/dist/core/command-generation/adapters/continue.js +33 -0
- package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
- package/dist/core/command-generation/adapters/costrict.js +28 -0
- package/dist/core/command-generation/adapters/crush.d.ts +13 -0
- package/dist/core/command-generation/adapters/crush.js +30 -0
- package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
- package/dist/core/command-generation/adapters/cursor.js +31 -0
- 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/easycode.d.ts +17 -0
- package/dist/core/command-generation/adapters/easycode.js +28 -0
- package/dist/core/command-generation/adapters/factory.d.ts +13 -0
- package/dist/core/command-generation/adapters/factory.js +28 -0
- package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
- package/dist/core/command-generation/adapters/gemini.js +27 -0
- package/dist/core/command-generation/adapters/gigacode.d.ts +17 -0
- package/dist/core/command-generation/adapters/gigacode.js +31 -0
- package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
- package/dist/core/command-generation/adapters/github-copilot.js +27 -0
- package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
- package/dist/core/command-generation/adapters/iflow.js +30 -0
- package/dist/core/command-generation/adapters/index.d.ts +40 -0
- package/dist/core/command-generation/adapters/index.js +40 -0
- package/dist/core/command-generation/adapters/junie.d.ts +13 -0
- package/dist/core/command-generation/adapters/junie.js +27 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/kilocode.js +23 -0
- package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
- package/dist/core/command-generation/adapters/kiro.js +27 -0
- package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
- package/dist/core/command-generation/adapters/lingma.js +30 -0
- 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.d.ts +14 -0
- package/dist/core/command-generation/adapters/opencode.js +38 -0
- package/dist/core/command-generation/adapters/pi.d.ts +18 -0
- package/dist/core/command-generation/adapters/pi.js +39 -0
- package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
- package/dist/core/command-generation/adapters/qoder.js +30 -0
- package/dist/core/command-generation/adapters/qwen.d.ts +17 -0
- package/dist/core/command-generation/adapters/qwen.js +31 -0
- package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/roocode.js +27 -0
- 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 +29 -0
- package/dist/core/command-generation/generator.js +43 -0
- package/dist/core/command-generation/index.d.ts +22 -0
- package/dist/core/command-generation/index.js +24 -0
- 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.d.ts +36 -0
- package/dist/core/command-generation/registry.js +114 -0
- package/dist/core/command-generation/toml.d.ts +19 -0
- package/dist/core/command-generation/toml.js +39 -0
- package/dist/core/command-generation/types.d.ts +63 -0
- package/dist/core/command-generation/types.js +8 -0
- package/dist/core/command-generation/yaml.d.ts +36 -0
- package/dist/core/command-generation/yaml.js +48 -0
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/config-prompts.d.ts +9 -0
- package/dist/core/config-prompts.js +56 -0
- package/dist/core/config-schema.d.ts +98 -0
- package/dist/core/config-schema.js +308 -0
- package/dist/core/config.d.ts +50 -0
- package/dist/core/config.js +122 -0
- package/dist/core/file-state.d.ts +36 -0
- package/dist/core/file-state.js +144 -0
- package/dist/core/file-view.d.ts +13 -0
- package/dist/core/file-view.js +14 -0
- package/dist/core/global-config.d.ts +101 -0
- package/dist/core/global-config.js +182 -0
- package/dist/core/id.d.ts +17 -0
- package/dist/core/id.js +30 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +7 -0
- package/dist/core/init.d.ts +57 -0
- package/dist/core/init.js +836 -0
- package/dist/core/list.d.ts +11 -0
- package/dist/core/list.js +197 -0
- package/dist/core/migration.d.ts +121 -0
- package/dist/core/migration.js +540 -0
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/openspec-root.d.ts +47 -0
- package/dist/core/openspec-root.js +196 -0
- package/dist/core/planning-home.d.ts +16 -0
- package/dist/core/planning-home.js +67 -0
- package/dist/core/profile-sync-drift.d.ts +31 -0
- package/dist/core/profile-sync-drift.js +201 -0
- package/dist/core/profiles.d.ts +19 -0
- package/dist/core/profiles.js +33 -0
- package/dist/core/project-config.d.ts +132 -0
- package/dist/core/project-config.js +519 -0
- package/dist/core/references.d.ts +10 -0
- package/dist/core/references.js +46 -0
- package/dist/core/root-selection.d.ts +59 -0
- package/dist/core/root-selection.js +132 -0
- package/dist/core/shared/allowed-tools.d.ts +12 -0
- package/dist/core/shared/allowed-tools.js +12 -0
- package/dist/core/shared/ide-restart.d.ts +26 -0
- package/dist/core/shared/ide-restart.js +42 -0
- package/dist/core/shared/index.d.ts +11 -0
- package/dist/core/shared/index.js +11 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +45 -0
- package/dist/core/shared/skill-files.d.ts +7 -0
- package/dist/core/shared/skill-files.js +43 -0
- package/dist/core/shared/skill-generation.d.ts +49 -0
- package/dist/core/shared/skill-generation.js +125 -0
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.d.ts +99 -0
- package/dist/core/shared/tool-detection.js +318 -0
- package/dist/core/shared-skill-target.d.ts +51 -0
- package/dist/core/shared-skill-target.js +239 -0
- package/dist/core/store/errors.d.ts +20 -0
- package/dist/core/store/errors.js +22 -0
- package/dist/core/store/index.d.ts +2 -0
- package/dist/core/store/index.js +2 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/optional-workflow.d.ts +69 -0
- package/dist/core/templates/optional-workflow.js +140 -0
- package/dist/core/templates/types.d.ts +19 -0
- package/dist/core/templates/types.js +5 -0
- package/dist/core/templates/workflows/personal.d.ts +22 -0
- package/dist/core/templates/workflows/personal.js +225 -0
- package/dist/core/update-plan.d.ts +127 -0
- package/dist/core/update-plan.js +390 -0
- package/dist/core/update.d.ts +154 -0
- package/dist/core/update.js +723 -0
- package/dist/core/version-check.d.ts +188 -0
- package/dist/core/version-check.js +906 -0
- package/dist/core/zod-issues.d.ts +4 -0
- package/dist/core/zod-issues.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/prompts/searchable-multi-select.d.ts +35 -0
- package/dist/prompts/searchable-multi-select.js +188 -0
- package/dist/telemetry/config.d.ts +37 -0
- package/dist/telemetry/config.js +148 -0
- package/dist/telemetry/index.d.ts +39 -0
- package/dist/telemetry/index.js +217 -0
- package/dist/telemetry/opt-out.d.ts +17 -0
- package/dist/telemetry/opt-out.js +27 -0
- package/dist/ui/ascii-patterns.d.ts +16 -0
- package/dist/ui/ascii-patterns.js +133 -0
- package/dist/ui/welcome-screen.d.ts +18 -0
- package/dist/ui/welcome-screen.js +199 -0
- package/dist/utils/change-metadata.d.ts +116 -0
- package/dist/utils/change-metadata.js +324 -0
- package/dist/utils/change-utils.d.ts +79 -0
- package/dist/utils/change-utils.js +121 -0
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +104 -0
- package/dist/utils/command-references.js +190 -0
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/executable-path.d.ts +5 -0
- package/dist/utils/executable-path.js +44 -0
- package/dist/utils/file-system.d.ts +58 -0
- package/dist/utils/file-system.js +432 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/interactive.d.ts +85 -0
- package/dist/utils/interactive.js +174 -0
- package/dist/utils/item-discovery.d.ts +15 -0
- package/dist/utils/item-discovery.js +33 -0
- package/dist/utils/line-endings.d.ts +34 -0
- package/dist/utils/line-endings.js +46 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/nested-change.d.ts +35 -0
- package/dist/utils/nested-change.js +185 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +94 -0
- package/dist/utils/task-progress.d.ts +67 -0
- package/dist/utils/task-progress.js +184 -0
- package/package.json +74 -4
- package/schemas//351/273/230/350/256/244/schema.yaml +68 -0
- package/schemas//351/273/230/350/256/244/templates//344/273/273/345/212/241/346/270/205/345/215/225.md +25 -0
- package/schemas//351/273/230/350/256/244/templates//346/265/213/350/257/225/347/224/250/344/276/213.md +34 -0
- package/schemas//351/273/230/350/256/244/templates//350/256/276/350/256/241/346/226/207/346/241/243.md +172 -0
- package/schemas//351/273/230/350/256/244/templates//351/234/200/346/261/202/346/226/207/346/241/243.md +19 -0
- package/skill-files//344/273/273/345/212/241/references//346/212/244/346/240/217.md +43 -0
- package/skill-files//344/273/273/345/212/241/references//347/262/222/345/272/246.md +23 -0
- package/skill-files//344/273/273/345/212/241/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//344/273/273/345/212/241/scripts/check-tasks.mjs +83 -0
- package/skill-files//345/206/231Java/references/Java/350/247/204/350/214/203.md +25 -0
- package/skill-files//345/206/231Java/references//345/215/261/351/231/251/345/206/231/346/263/225.md +27 -0
- package/skill-files//345/206/231Java/references//345/256/214/346/210/220/345/257/271/347/205/247.md +15 -0
- package/skill-files//345/206/231Java/references//346/212/244/346/240/217.md +35 -0
- package/skill-files//345/206/231Java/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//345/206/231Java/references//350/270/251/345/235/221.md +17 -0
- package/skill-files//345/256/241Java/references//346/212/244/346/240/217.md +19 -0
- package/skill-files//345/256/241Java/references//347/244/272/344/276/213.md +23 -0
- package/skill-files//345/256/241Java/references//347/272/246/346/235/237.md +7 -0
- package/skill-files//345/256/241Java/references//350/247/204/345/210/231.md +23 -0
- package/skill-files//345/256/241Java/references//351/242/206/345/237/237/346/243/200/351/241/271.md +58 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//346/212/244/346/240/217.md +18 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//347/273/264/345/272/246.md +17 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//350/270/251/345/235/221.md +11 -0
- package/skill-files//346/226/260/345/273/272/references//344/272/244/346/216/245.md +11 -0
- package/skill-files//346/226/260/345/273/272/references//346/212/244/346/240/217.md +14 -0
- package/skill-files//346/226/260/345/273/272/references//347/272/246/346/235/237.md +7 -0
- package/skill-files//346/267/261/346/200/235/config/prompts.yaml +114 -0
- package/skill-files//346/267/261/346/200/235/references//344/270/211/346/226/271/346/240/270/345/257/271.md +72 -0
- package/skill-files//346/267/261/346/200/235/references//344/276/213/345/255/220.md +39 -0
- package/skill-files//346/267/261/346/200/235/references//346/200/216/344/271/210/346/203/263.md +61 -0
- package/skill-files//346/267/261/346/200/235/references//346/212/244/346/240/217.md +26 -0
- package/skill-files//346/267/261/346/200/235/references//347/202/271/351/200/211/346/217/220/351/227/256.md +66 -0
- package/skill-files//346/267/261/346/200/235/references//347/231/273/350/256/260.md +45 -0
- package/skill-files//346/267/261/346/200/235/references//347/272/246/346/235/237.md +9 -0
- package/skill-files//347/224/250/344/276/213/references//346/200/216/344/271/210/345/206/231.md +13 -0
- package/skill-files//347/224/250/344/276/213/references//346/212/244/346/240/217.md +42 -0
- package/skill-files//347/224/250/344/276/213/references//347/272/246/346/235/237.md +5 -0
- package/skill-files//347/224/250/344/276/213/scripts/check-cases.mjs +70 -0
- package/skill-files//350/256/276/350/256/241/references//345/207/206/345/244/207.md +25 -0
- package/skill-files//350/256/276/350/256/241/references//346/212/244/346/240/217.md +134 -0
- package/skill-files//350/256/276/350/256/241/references//346/226/207/351/243/216.md +17 -0
- package/skill-files//350/256/276/350/256/241/references//347/225/214/351/235/242.md +18 -0
- package/skill-files//350/256/276/350/256/241/references//347/261/273/345/236/213.md +19 -0
- package/skill-files//350/256/276/350/256/241/references//347/272/246/346/235/237.md +25 -0
- package/skill-files//350/256/276/350/256/241/scripts/_smoke.mjs +72 -0
- package/skill-files//350/256/276/350/256/241/scripts/check-design.mjs +331 -0
- package/skill-files//351/234/200/346/261/202/references//346/212/244/346/240/217.md +26 -0
- package/skill-files//351/234/200/346/261/202/references//346/226/207/351/243/216.md +23 -0
- package/skill-files//351/234/200/346/261/202/references//346/267/261/346/200/235/346/221/204/345/205/245.md +21 -0
- package/skill-files//351/234/200/346/261/202/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//351/234/200/346/261/202/references//351/234/200/346/261/202/345/276/205/345/256/232.md +13 -0
- package/skill-files//351/234/200/346/261/202/scripts/check-requirements.mjs +60 -0
- package/skill-files//351/252/214/346/224/266/references//346/212/244/346/240/217.md +27 -0
- package/skill-files//351/252/214/346/224/266/references//346/240/270/345/257/271.md +17 -0
- package/skill-files//351/252/214/346/224/266/references//347/272/246/346/235/237.md +7 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { CHANGE_METADATA_KNOWN_KEYS, type ChangeMetadata } from '../core/change-metadata/index.js';
|
|
2
|
+
import { type ProjectConfig } from '../core/project-config.js';
|
|
3
|
+
export declare const METADATA_FILENAME = ".freespec.yaml";
|
|
4
|
+
export { CHANGE_METADATA_KNOWN_KEYS };
|
|
5
|
+
/**
|
|
6
|
+
* Extension keys: a top-level key starting with a lowercase `x-` belongs to a
|
|
7
|
+
* tool or team that reads `.freespec.yaml` alongside FreeSpec (a tracker id, an
|
|
8
|
+
* owner). FreeSpec ignores it and does not report it as unknown. As with
|
|
9
|
+
* OpenAPI's `x-` extensions, the name after the prefix may use any case or
|
|
10
|
+
* spelling, but must be non-empty and free of whitespace, so `x-`, `x- foo`
|
|
11
|
+
* and `X-Foo` are still reported. Extensions are top-level only: a key inside
|
|
12
|
+
* `initiative:` is rejected by that object's strict schema.
|
|
13
|
+
*/
|
|
14
|
+
export declare const EXTENSION_KEY_PATTERN: RegExp;
|
|
15
|
+
export declare function isExtensionMetadataKey(key: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Unknown top-level keys on a parsed .freespec.yaml object. Extra keys are
|
|
18
|
+
* stripped by ChangeMetadataSchema rather than rejected, so callers that want
|
|
19
|
+
* to tell the author a key did nothing have to look at the raw object.
|
|
20
|
+
* Extension keys (`x-*`) are not unknown: they are deliberately someone else's.
|
|
21
|
+
*/
|
|
22
|
+
export declare function listUnknownChangeMetadataKeys(parsed: unknown): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Human-readable warning for keys ChangeMetadataSchema strips. Names the
|
|
25
|
+
* unknown keys and the keys that do exist.
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatUnknownChangeMetadataKeysMessage(keys: string[]): string;
|
|
28
|
+
/**
|
|
29
|
+
* Non-throwing read of unknown top-level keys. Missing, unreadable, or
|
|
30
|
+
* unparseable files yield no keys: those failures already have their own
|
|
31
|
+
* diagnostics on the read path.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readUnknownChangeMetadataKeys(changeDir: string): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Error thrown when change metadata validation fails.
|
|
36
|
+
*/
|
|
37
|
+
export declare class ChangeMetadataError extends Error {
|
|
38
|
+
readonly metadataPath: string;
|
|
39
|
+
readonly cause?: Error | undefined;
|
|
40
|
+
constructor(message: string, metadataPath: string, cause?: Error | undefined);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validates that a schema name is valid (exists in available schemas).
|
|
44
|
+
*
|
|
45
|
+
* @param schemaName - The schema name to validate
|
|
46
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
47
|
+
* @returns The validated schema name
|
|
48
|
+
* @throws Error if schema is not found
|
|
49
|
+
*/
|
|
50
|
+
export declare function validateSchemaName(schemaName: string, projectRoot?: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* Writes change metadata to .freespec.yaml in the change directory.
|
|
53
|
+
*
|
|
54
|
+
* Meant for a new file only (`createChange` refuses an existing change
|
|
55
|
+
* directory). `ChangeMetadataSchema` strips unknown keys, including `x-`
|
|
56
|
+
* extension keys, so a caller that rewrites an existing file must merge those
|
|
57
|
+
* keys back from the raw YAML first or it silently deletes them.
|
|
58
|
+
*
|
|
59
|
+
* @param changeDir - The path to the change directory
|
|
60
|
+
* @param metadata - The metadata to write
|
|
61
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
62
|
+
* @throws ChangeMetadataError if validation fails or write fails
|
|
63
|
+
*/
|
|
64
|
+
export declare function writeChangeMetadata(changeDir: string, metadata: ChangeMetadata, projectRoot?: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* Reads change metadata from .freespec.yaml in the change directory.
|
|
67
|
+
*
|
|
68
|
+
* @param changeDir - The path to the change directory
|
|
69
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
70
|
+
* @returns The validated metadata, or null if no metadata file exists
|
|
71
|
+
* @throws ChangeMetadataError if the file exists but is invalid
|
|
72
|
+
*/
|
|
73
|
+
export declare function readChangeMetadata(changeDir: string, projectRoot?: string): ChangeMetadata | null;
|
|
74
|
+
export interface ResolveSchemaForChangeOptions {
|
|
75
|
+
metadata?: ChangeMetadata | null;
|
|
76
|
+
/** Pre-read project config; suppresses the fallback config read when provided. */
|
|
77
|
+
projectConfig?: ProjectConfig | null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolves the schema for a change, with explicit override taking precedence.
|
|
81
|
+
*
|
|
82
|
+
* Resolution order:
|
|
83
|
+
* 1. Explicit schema (if provided)
|
|
84
|
+
* 2. Schema from .freespec.yaml metadata (if exists)
|
|
85
|
+
* 3. Schema from freespec/config.yaml (if exists)
|
|
86
|
+
* 4. Default schema (`默认`)
|
|
87
|
+
*
|
|
88
|
+
* @param changeDir - The path to the change directory
|
|
89
|
+
* @param explicitSchema - Optional explicit schema override
|
|
90
|
+
* @returns The resolved schema name
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveSchemaForChange(changeDir: string, explicitSchema?: string, projectRootOverride?: string, options?: ResolveSchemaForChangeOptions): string;
|
|
93
|
+
export interface MetadataMarker {
|
|
94
|
+
/**
|
|
95
|
+
* True when the metadata parses under ChangeMetadataSchema, sets
|
|
96
|
+
* the requested boolean marker to true, and names a schema that loads.
|
|
97
|
+
*/
|
|
98
|
+
declared: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Set when the marker cannot be honored: it appears in a file that
|
|
101
|
+
* fails the metadata contract, or the metadata file exists but cannot be
|
|
102
|
+
* read at all (so whether the marker is set cannot even be determined).
|
|
103
|
+
*/
|
|
104
|
+
invalidReason?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Non-throwing read of the retire_capabilities marker.
|
|
108
|
+
*
|
|
109
|
+
* Gates the one archive action that removes a file from `freespec/specs/`: when
|
|
110
|
+
* a change's REMOVED entries take a capability's last requirement, archive
|
|
111
|
+
* deletes the emptied main spec rather than aborting on a spec it cannot write
|
|
112
|
+
* (#1302). Declared rather than inferred because the delete is recoverable only
|
|
113
|
+
* from git, so it is the author's call.
|
|
114
|
+
*/
|
|
115
|
+
export declare function readRetireCapabilitiesMarker(changeDir: string): MetadataMarker;
|
|
116
|
+
//# sourceMappingURL=change-metadata.d.ts.map
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as yaml from 'yaml';
|
|
4
|
+
import { CHANGE_METADATA_KNOWN_KEYS, ChangeMetadataSchema, } from '../core/change-metadata/index.js';
|
|
5
|
+
import { listSchemas, resolveSchema } from '../core/artifact-graph/resolver.js';
|
|
6
|
+
import { readProjectConfig } from '../core/project-config.js';
|
|
7
|
+
import { sanitizeInline } from '../core/references.js';
|
|
8
|
+
import { DEFAULT_SCHEMA } from '../core/change-layout.js';
|
|
9
|
+
export const METADATA_FILENAME = '.freespec.yaml';
|
|
10
|
+
export { CHANGE_METADATA_KNOWN_KEYS };
|
|
11
|
+
/**
|
|
12
|
+
* Extension keys: a top-level key starting with a lowercase `x-` belongs to a
|
|
13
|
+
* tool or team that reads `.freespec.yaml` alongside FreeSpec (a tracker id, an
|
|
14
|
+
* owner). FreeSpec ignores it and does not report it as unknown. As with
|
|
15
|
+
* OpenAPI's `x-` extensions, the name after the prefix may use any case or
|
|
16
|
+
* spelling, but must be non-empty and free of whitespace, so `x-`, `x- foo`
|
|
17
|
+
* and `X-Foo` are still reported. Extensions are top-level only: a key inside
|
|
18
|
+
* `initiative:` is rejected by that object's strict schema.
|
|
19
|
+
*/
|
|
20
|
+
export const EXTENSION_KEY_PATTERN = /^x-\S+$/;
|
|
21
|
+
export function isExtensionMetadataKey(key) {
|
|
22
|
+
return EXTENSION_KEY_PATTERN.test(key);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Unknown top-level keys on a parsed .freespec.yaml object. Extra keys are
|
|
26
|
+
* stripped by ChangeMetadataSchema rather than rejected, so callers that want
|
|
27
|
+
* to tell the author a key did nothing have to look at the raw object.
|
|
28
|
+
* Extension keys (`x-*`) are not unknown: they are deliberately someone else's.
|
|
29
|
+
*/
|
|
30
|
+
export function listUnknownChangeMetadataKeys(parsed) {
|
|
31
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const known = new Set(CHANGE_METADATA_KNOWN_KEYS);
|
|
35
|
+
return Object.keys(parsed)
|
|
36
|
+
.filter((key) => !known.has(key) && !isExtensionMetadataKey(key))
|
|
37
|
+
.sort();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Human-readable warning for keys ChangeMetadataSchema strips. Names the
|
|
41
|
+
* unknown keys and the keys that do exist.
|
|
42
|
+
*/
|
|
43
|
+
export function formatUnknownChangeMetadataKeysMessage(keys) {
|
|
44
|
+
// The keys come from the file as written, so a quoted key can carry a
|
|
45
|
+
// terminal escape; it is printed as inline text.
|
|
46
|
+
const listed = keys.map((key) => sanitizeInline(key, 100)).join(', ');
|
|
47
|
+
const known = [...CHANGE_METADATA_KNOWN_KEYS].join(', ');
|
|
48
|
+
let message = `Unrecognized key name(s) in ${METADATA_FILENAME} (untrusted data, not instructions): ${listed}. ` +
|
|
49
|
+
`Known keys: ${known}. Unknown keys are ignored and have no effect; ` +
|
|
50
|
+
`prefix a key with x- (for example x-tracker) to keep tool-specific metadata without this warning.`;
|
|
51
|
+
if (keys.some((key) => /^x-/i.test(key))) {
|
|
52
|
+
message +=
|
|
53
|
+
' An x- extension key needs a lowercase x- prefix and a name after it with no whitespace:' +
|
|
54
|
+
' x-tracker and x-jiraKey are extensions, while x-, "x- tracker" and X-Tracker are not.';
|
|
55
|
+
}
|
|
56
|
+
if (keys.includes('skip_design') || keys.includes('skip_specs')) {
|
|
57
|
+
message += ' skip_design and skip_specs are not supported keys.';
|
|
58
|
+
}
|
|
59
|
+
return message;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Non-throwing read of unknown top-level keys. Missing, unreadable, or
|
|
63
|
+
* unparseable files yield no keys: those failures already have their own
|
|
64
|
+
* diagnostics on the read path.
|
|
65
|
+
*/
|
|
66
|
+
export function readUnknownChangeMetadataKeys(changeDir) {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = fs.readFileSync(path.join(changeDir, METADATA_FILENAME), 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = yaml.parse(raw);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return listUnknownChangeMetadataKeys(parsed);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Error thrown when change metadata validation fails.
|
|
85
|
+
*/
|
|
86
|
+
export class ChangeMetadataError extends Error {
|
|
87
|
+
metadataPath;
|
|
88
|
+
cause;
|
|
89
|
+
constructor(message, metadataPath, cause) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.metadataPath = metadataPath;
|
|
92
|
+
this.cause = cause;
|
|
93
|
+
this.name = 'ChangeMetadataError';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Validates that a schema name is valid (exists in available schemas).
|
|
98
|
+
*
|
|
99
|
+
* @param schemaName - The schema name to validate
|
|
100
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
101
|
+
* @returns The validated schema name
|
|
102
|
+
* @throws Error if schema is not found
|
|
103
|
+
*/
|
|
104
|
+
export function validateSchemaName(schemaName, projectRoot) {
|
|
105
|
+
const availableSchemas = listSchemas(projectRoot);
|
|
106
|
+
if (!availableSchemas.includes(schemaName)) {
|
|
107
|
+
throw new Error(`Unknown schema '${schemaName}'. Available: ${availableSchemas.join(', ')}`);
|
|
108
|
+
}
|
|
109
|
+
return schemaName;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Writes change metadata to .freespec.yaml in the change directory.
|
|
113
|
+
*
|
|
114
|
+
* Meant for a new file only (`createChange` refuses an existing change
|
|
115
|
+
* directory). `ChangeMetadataSchema` strips unknown keys, including `x-`
|
|
116
|
+
* extension keys, so a caller that rewrites an existing file must merge those
|
|
117
|
+
* keys back from the raw YAML first or it silently deletes them.
|
|
118
|
+
*
|
|
119
|
+
* @param changeDir - The path to the change directory
|
|
120
|
+
* @param metadata - The metadata to write
|
|
121
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
122
|
+
* @throws ChangeMetadataError if validation fails or write fails
|
|
123
|
+
*/
|
|
124
|
+
export function writeChangeMetadata(changeDir, metadata, projectRoot) {
|
|
125
|
+
const metaPath = path.join(changeDir, METADATA_FILENAME);
|
|
126
|
+
// Validate schema exists
|
|
127
|
+
validateSchemaName(metadata.schema, projectRoot);
|
|
128
|
+
// Validate with Zod
|
|
129
|
+
const parseResult = ChangeMetadataSchema.safeParse(metadata);
|
|
130
|
+
if (!parseResult.success) {
|
|
131
|
+
throw new ChangeMetadataError(`Invalid metadata: ${parseResult.error.message}`, metaPath);
|
|
132
|
+
}
|
|
133
|
+
// Write YAML file
|
|
134
|
+
const content = yaml.stringify(parseResult.data);
|
|
135
|
+
try {
|
|
136
|
+
fs.writeFileSync(metaPath, content, 'utf-8');
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
const ioError = err instanceof Error ? err : new Error(String(err));
|
|
140
|
+
throw new ChangeMetadataError(`Failed to write metadata: ${ioError.message}`, metaPath, ioError);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Reads change metadata from .freespec.yaml in the change directory.
|
|
145
|
+
*
|
|
146
|
+
* @param changeDir - The path to the change directory
|
|
147
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
148
|
+
* @returns The validated metadata, or null if no metadata file exists
|
|
149
|
+
* @throws ChangeMetadataError if the file exists but is invalid
|
|
150
|
+
*/
|
|
151
|
+
export function readChangeMetadata(changeDir, projectRoot) {
|
|
152
|
+
const metaPath = path.join(changeDir, METADATA_FILENAME);
|
|
153
|
+
if (!fs.existsSync(metaPath)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
let content;
|
|
157
|
+
try {
|
|
158
|
+
content = fs.readFileSync(metaPath, 'utf-8');
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
const ioError = err instanceof Error ? err : new Error(String(err));
|
|
162
|
+
throw new ChangeMetadataError(`Failed to read metadata: ${ioError.message}`, metaPath, ioError);
|
|
163
|
+
}
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = yaml.parse(content);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
const parseError = err instanceof Error ? err : new Error(String(err));
|
|
170
|
+
throw new ChangeMetadataError(`Invalid YAML in metadata file: ${parseError.message}`, metaPath, parseError);
|
|
171
|
+
}
|
|
172
|
+
// Validate with Zod
|
|
173
|
+
const parseResult = ChangeMetadataSchema.safeParse(parsed);
|
|
174
|
+
if (!parseResult.success) {
|
|
175
|
+
throw new ChangeMetadataError(`Invalid metadata: ${parseResult.error.message}`, metaPath);
|
|
176
|
+
}
|
|
177
|
+
// Validate that the schema exists
|
|
178
|
+
const availableSchemas = listSchemas(projectRoot);
|
|
179
|
+
if (!availableSchemas.includes(parseResult.data.schema)) {
|
|
180
|
+
throw new ChangeMetadataError(`Unknown schema '${parseResult.data.schema}'. Available: ${availableSchemas.join(', ')}`, metaPath);
|
|
181
|
+
}
|
|
182
|
+
return parseResult.data;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Resolves the schema for a change, with explicit override taking precedence.
|
|
186
|
+
*
|
|
187
|
+
* Resolution order:
|
|
188
|
+
* 1. Explicit schema (if provided)
|
|
189
|
+
* 2. Schema from .freespec.yaml metadata (if exists)
|
|
190
|
+
* 3. Schema from freespec/config.yaml (if exists)
|
|
191
|
+
* 4. Default schema (`默认`)
|
|
192
|
+
*
|
|
193
|
+
* @param changeDir - The path to the change directory
|
|
194
|
+
* @param explicitSchema - Optional explicit schema override
|
|
195
|
+
* @returns The resolved schema name
|
|
196
|
+
*/
|
|
197
|
+
export function resolveSchemaForChange(changeDir, explicitSchema, projectRootOverride, options = {}) {
|
|
198
|
+
// Change directories sit directly under freespec/: projectRoot/freespec/<名称>变更
|
|
199
|
+
const projectRoot = projectRootOverride ?? path.resolve(changeDir, '../..');
|
|
200
|
+
// 1. Explicit override wins
|
|
201
|
+
if (explicitSchema) {
|
|
202
|
+
return explicitSchema;
|
|
203
|
+
}
|
|
204
|
+
const metadata = options.metadata !== undefined ? options.metadata : readChangeMetadata(changeDir, projectRoot);
|
|
205
|
+
if (metadata?.schema) {
|
|
206
|
+
return metadata.schema;
|
|
207
|
+
}
|
|
208
|
+
// 3. Try reading from project config when metadata is absent.
|
|
209
|
+
if (options.projectConfig !== undefined) {
|
|
210
|
+
if (options.projectConfig?.schema) {
|
|
211
|
+
return options.projectConfig.schema;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
try {
|
|
216
|
+
const config = readProjectConfig(projectRoot);
|
|
217
|
+
if (config?.schema) {
|
|
218
|
+
return config.schema;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// If config read fails, fall back to default
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// 4. Default
|
|
226
|
+
return DEFAULT_SCHEMA;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Non-throwing read of the retire_capabilities marker.
|
|
230
|
+
*
|
|
231
|
+
* Gates the one archive action that removes a file from `freespec/specs/`: when
|
|
232
|
+
* a change's REMOVED entries take a capability's last requirement, archive
|
|
233
|
+
* deletes the emptied main spec rather than aborting on a spec it cannot write
|
|
234
|
+
* (#1302). Declared rather than inferred because the delete is recoverable only
|
|
235
|
+
* from git, so it is the author's call.
|
|
236
|
+
*/
|
|
237
|
+
export function readRetireCapabilitiesMarker(changeDir) {
|
|
238
|
+
return readBooleanMarker(changeDir, 'retire_capabilities');
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Shared implementation for the boolean change-metadata markers, keyed by field
|
|
242
|
+
* name. One body rather than two, so a marker can never drift into honoring
|
|
243
|
+
* metadata the other rejects - the whole point of the contract described above.
|
|
244
|
+
*/
|
|
245
|
+
/**
|
|
246
|
+
* A marker that cannot be honored, with its reason made safe to print.
|
|
247
|
+
*
|
|
248
|
+
* Every reason quotes something the author wrote - a schema name, a parser
|
|
249
|
+
* message carrying one, a filesystem error carrying a path - and callers print
|
|
250
|
+
* it straight to a terminal (`freespec archive`, `freespec validate`). A raw CR
|
|
251
|
+
* could forge a line of its own and an ESC could redraw the screen, so control
|
|
252
|
+
* characters never leave this function.
|
|
253
|
+
*/
|
|
254
|
+
function unhonorable(reason) {
|
|
255
|
+
return { declared: false, invalidReason: reason.replace(/[\u0000-\u001f\u007f]/g, '?') };
|
|
256
|
+
}
|
|
257
|
+
function readBooleanMarker(changeDir, key) {
|
|
258
|
+
let raw;
|
|
259
|
+
try {
|
|
260
|
+
raw = fs.readFileSync(path.join(changeDir, METADATA_FILENAME), 'utf-8');
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
if (err?.code === 'ENOENT') {
|
|
264
|
+
return { declared: false };
|
|
265
|
+
}
|
|
266
|
+
// The file exists but cannot be read (EACCES, EISDIR, ...). Status and
|
|
267
|
+
// instructions reject the change outright here, and whether a marker is
|
|
268
|
+
// set cannot be determined - fail closed rather than let archive treat
|
|
269
|
+
// the change as unmarked while every metadata-reading surface errors.
|
|
270
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
271
|
+
return unhonorable(`the metadata file cannot be read (${message})`);
|
|
272
|
+
}
|
|
273
|
+
let parsed;
|
|
274
|
+
try {
|
|
275
|
+
parsed = yaml.parse(raw);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Anchored so a comment like "# maybe add skip_specs later" does not
|
|
279
|
+
// claim the marker was set.
|
|
280
|
+
const mentioned = new RegExp(`^\\s*(['"]?)${key}\\1\\s*:`, 'm').test(raw);
|
|
281
|
+
return mentioned ? unhonorable('the file is not valid YAML') : { declared: false };
|
|
282
|
+
}
|
|
283
|
+
const result = ChangeMetadataSchema.safeParse(parsed);
|
|
284
|
+
if (result.success) {
|
|
285
|
+
if (result.data[key] !== true) {
|
|
286
|
+
return { declared: false };
|
|
287
|
+
}
|
|
288
|
+
// Schema loading is checked only when the marker is set: a broken schema
|
|
289
|
+
// on an ordinary change is status's problem to report, but honoring a
|
|
290
|
+
// marker that status rejects would let validate/archive pass what the
|
|
291
|
+
// rest of the CLI refuses to load. The membership check mirrors
|
|
292
|
+
// readChangeMetadata (which rejects names like 'spec-driven.yaml' that
|
|
293
|
+
// resolveSchema alone would normalize and accept); resolveSchema then
|
|
294
|
+
// proves the schema actually parses. Any failure fails closed.
|
|
295
|
+
try {
|
|
296
|
+
const projectRoot = path.resolve(changeDir, '../..');
|
|
297
|
+
if (!listSchemas(projectRoot).includes(result.data.schema)) {
|
|
298
|
+
return unhonorable(`schema: unknown schema '${result.data.schema}'`);
|
|
299
|
+
}
|
|
300
|
+
resolveSchema(result.data.schema, projectRoot);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
304
|
+
return unhonorable(message);
|
|
305
|
+
}
|
|
306
|
+
return { declared: true };
|
|
307
|
+
}
|
|
308
|
+
// Key presence, not value: skip_specs: "yes" must surface as unhonorable,
|
|
309
|
+
// not vanish while the zero-delta guidance tells the user to set the very
|
|
310
|
+
// marker they set. An explicit skip_specs: false is the opposite of setting
|
|
311
|
+
// the marker, so it must not drag unrelated metadata problems into
|
|
312
|
+
// validate - the change simply is not marked.
|
|
313
|
+
const markerMentioned = typeof parsed === 'object' &&
|
|
314
|
+
parsed !== null &&
|
|
315
|
+
key in parsed &&
|
|
316
|
+
parsed[key] !== false;
|
|
317
|
+
if (markerMentioned) {
|
|
318
|
+
const first = result.error.issues[0];
|
|
319
|
+
const where = first.path.length > 0 ? `${first.path.join('.')}: ` : '';
|
|
320
|
+
return unhonorable(`${where}${first.message}`);
|
|
321
|
+
}
|
|
322
|
+
return { declared: false };
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=change-metadata.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ChangeMetadata } from '../core/change-metadata/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for creating a change.
|
|
4
|
+
*/
|
|
5
|
+
export interface CreateChangeOptions {
|
|
6
|
+
/** The workflow schema to use (default: 默认) */
|
|
7
|
+
schema?: string;
|
|
8
|
+
/** Default schema to use when no explicit schema or project config is present */
|
|
9
|
+
defaultSchema?: string;
|
|
10
|
+
/** Directory that should contain the change directories */
|
|
11
|
+
changesDir?: string;
|
|
12
|
+
/** Additional metadata to persist in the change's .freespec.yaml */
|
|
13
|
+
metadata?: Partial<Pick<ChangeMetadata, 'goal' | 'affected_areas' | 'initiative'>>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Result of creating a change.
|
|
17
|
+
*/
|
|
18
|
+
export interface CreateChangeResult {
|
|
19
|
+
/** The schema that was actually used (resolved from options, config, or default) */
|
|
20
|
+
schema: string;
|
|
21
|
+
/** Folder name under freespec/, including the 变更 suffix */
|
|
22
|
+
changeName: string;
|
|
23
|
+
/** Absolute path to the created change directory */
|
|
24
|
+
changeDir: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Result of validating a change name.
|
|
28
|
+
*/
|
|
29
|
+
export interface ValidationResult {
|
|
30
|
+
valid: boolean;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validates that a change name follows kebab-case conventions.
|
|
35
|
+
*
|
|
36
|
+
* Uses FreeSpec's shared kebab-id grammar (the same one store ids and change
|
|
37
|
+
* metadata ids use), so a change name may:
|
|
38
|
+
* - Start with a lowercase letter or a digit
|
|
39
|
+
* - Contain only lowercase letters, numbers, and hyphens
|
|
40
|
+
* - Not start or end with a hyphen
|
|
41
|
+
* - Not contain consecutive hyphens
|
|
42
|
+
*
|
|
43
|
+
* A leading digit is allowed so ordering conventions like `100-add-feature` or
|
|
44
|
+
* `00001-add-auth` work; archive already treats such prefixes as a supported
|
|
45
|
+
* convention (see ARCHIVE_DATE_PREFIX_PATTERN).
|
|
46
|
+
*
|
|
47
|
+
* @param name - The change name to validate
|
|
48
|
+
* @returns Validation result with `valid: true` or `valid: false` with an error message
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* validateChangeName('add-auth') // { valid: true }
|
|
52
|
+
* validateChangeName('100-add-feature') // { valid: true }
|
|
53
|
+
* validateChangeName('Add-Auth') // { valid: false, error: '...' }
|
|
54
|
+
*/
|
|
55
|
+
export declare function validateChangeName(name: string): ValidationResult;
|
|
56
|
+
/**
|
|
57
|
+
* Creates a new change directory with metadata file.
|
|
58
|
+
*
|
|
59
|
+
* @param projectRoot - The root directory of the project (where `freespec/` lives)
|
|
60
|
+
* @param name - The change name (must be valid kebab-case)
|
|
61
|
+
* @param options - Optional settings for the change
|
|
62
|
+
* @throws Error if the change name is invalid
|
|
63
|
+
* @throws Error if the schema name is invalid
|
|
64
|
+
* @throws Error if the change directory already exists
|
|
65
|
+
*
|
|
66
|
+
* @returns Result containing the resolved schema name
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // Creates freespec/登录改造变更/ with default schema
|
|
70
|
+
* const result = await createChange('/path/to/project', 'add-auth')
|
|
71
|
+
* console.log(result.schema) // '默认' or value from config
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // Creates freespec/登录改造变更/ with custom schema
|
|
75
|
+
* const result = await createChange('/path/to/project', 'add-auth', { schema: 'my-workflow' })
|
|
76
|
+
* console.log(result.schema) // 'my-workflow'
|
|
77
|
+
*/
|
|
78
|
+
export declare function createChange(projectRoot: string, name: string, options?: CreateChangeOptions): Promise<CreateChangeResult>;
|
|
79
|
+
//# sourceMappingURL=change-utils.d.ts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { FileSystemUtils } from './file-system.js';
|
|
3
|
+
import { writeChangeMetadata, validateSchemaName } from './change-metadata.js';
|
|
4
|
+
import { formatLocalDate } from './date.js';
|
|
5
|
+
import { readProjectConfig } from '../core/project-config.js';
|
|
6
|
+
import { changeParentDir, DEFAULT_SCHEMA, isChangeDirectoryName, normalizeChangeName, } from '../core/change-layout.js';
|
|
7
|
+
/**
|
|
8
|
+
* Validates that a change name follows kebab-case conventions.
|
|
9
|
+
*
|
|
10
|
+
* Uses FreeSpec's shared kebab-id grammar (the same one store ids and change
|
|
11
|
+
* metadata ids use), so a change name may:
|
|
12
|
+
* - Start with a lowercase letter or a digit
|
|
13
|
+
* - Contain only lowercase letters, numbers, and hyphens
|
|
14
|
+
* - Not start or end with a hyphen
|
|
15
|
+
* - Not contain consecutive hyphens
|
|
16
|
+
*
|
|
17
|
+
* A leading digit is allowed so ordering conventions like `100-add-feature` or
|
|
18
|
+
* `00001-add-auth` work; archive already treats such prefixes as a supported
|
|
19
|
+
* convention (see ARCHIVE_DATE_PREFIX_PATTERN).
|
|
20
|
+
*
|
|
21
|
+
* @param name - The change name to validate
|
|
22
|
+
* @returns Validation result with `valid: true` or `valid: false` with an error message
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* validateChangeName('add-auth') // { valid: true }
|
|
26
|
+
* validateChangeName('100-add-feature') // { valid: true }
|
|
27
|
+
* validateChangeName('Add-Auth') // { valid: false, error: '...' }
|
|
28
|
+
*/
|
|
29
|
+
export function validateChangeName(name) {
|
|
30
|
+
const folderName = normalizeChangeName(name);
|
|
31
|
+
if (!folderName) {
|
|
32
|
+
return { valid: false, error: '变更名不能为空' };
|
|
33
|
+
}
|
|
34
|
+
if (folderName.length > 80) {
|
|
35
|
+
return { valid: false, error: '变更名太长(最多 80 个字符)' };
|
|
36
|
+
}
|
|
37
|
+
if (/\s/u.test(folderName)) {
|
|
38
|
+
return { valid: false, error: '变更名不能包含空格,用连字符连接' };
|
|
39
|
+
}
|
|
40
|
+
if (!isChangeDirectoryName(folderName)) {
|
|
41
|
+
return {
|
|
42
|
+
valid: false,
|
|
43
|
+
error: '变更名用中文,可以加数字和连字符,并以「变更」结尾。例如:登录改造变更',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { valid: true };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Creates a new change directory with metadata file.
|
|
50
|
+
*
|
|
51
|
+
* @param projectRoot - The root directory of the project (where `freespec/` lives)
|
|
52
|
+
* @param name - The change name (must be valid kebab-case)
|
|
53
|
+
* @param options - Optional settings for the change
|
|
54
|
+
* @throws Error if the change name is invalid
|
|
55
|
+
* @throws Error if the schema name is invalid
|
|
56
|
+
* @throws Error if the change directory already exists
|
|
57
|
+
*
|
|
58
|
+
* @returns Result containing the resolved schema name
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Creates freespec/登录改造变更/ with default schema
|
|
62
|
+
* const result = await createChange('/path/to/project', 'add-auth')
|
|
63
|
+
* console.log(result.schema) // '默认' or value from config
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* // Creates freespec/登录改造变更/ with custom schema
|
|
67
|
+
* const result = await createChange('/path/to/project', 'add-auth', { schema: 'my-workflow' })
|
|
68
|
+
* console.log(result.schema) // 'my-workflow'
|
|
69
|
+
*/
|
|
70
|
+
export async function createChange(projectRoot, name, options = {}) {
|
|
71
|
+
// Validate the name first
|
|
72
|
+
const folderName = normalizeChangeName(name);
|
|
73
|
+
const validation = validateChangeName(folderName);
|
|
74
|
+
if (!validation.valid) {
|
|
75
|
+
throw new Error(validation.error);
|
|
76
|
+
}
|
|
77
|
+
const defaultSchema = options.defaultSchema ?? DEFAULT_SCHEMA;
|
|
78
|
+
// Determine schema: explicit option → project config → supplied default
|
|
79
|
+
let schemaName;
|
|
80
|
+
if (options.schema) {
|
|
81
|
+
schemaName = options.schema;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Try to read from project config
|
|
85
|
+
try {
|
|
86
|
+
const config = readProjectConfig(projectRoot);
|
|
87
|
+
schemaName = config?.schema ?? defaultSchema;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// If config read fails, use default
|
|
91
|
+
schemaName = defaultSchema;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Validate the resolved schema
|
|
95
|
+
validateSchemaName(schemaName, projectRoot);
|
|
96
|
+
// Build the change directory path
|
|
97
|
+
const changeDir = path.join(options.changesDir ?? changeParentDir(projectRoot), folderName);
|
|
98
|
+
// Check if change already exists
|
|
99
|
+
if (await FileSystemUtils.directoryExists(changeDir)) {
|
|
100
|
+
throw new Error(`变更「${folderName}」已经存在:${changeDir}`);
|
|
101
|
+
}
|
|
102
|
+
// Write a project config only when none exists. The config records the
|
|
103
|
+
// project default schema, never a one-change --schema override.
|
|
104
|
+
const freespecDir = path.join(projectRoot, 'freespec');
|
|
105
|
+
// Create the directory (including parent directories if needed)
|
|
106
|
+
await FileSystemUtils.createDirectory(changeDir);
|
|
107
|
+
const configPath = path.join(freespecDir, 'config.yaml');
|
|
108
|
+
const configYmlPath = path.join(freespecDir, 'config.yml');
|
|
109
|
+
if (!(await FileSystemUtils.fileExists(configPath)) &&
|
|
110
|
+
!(await FileSystemUtils.fileExists(configYmlPath))) {
|
|
111
|
+
await FileSystemUtils.writeFile(configPath, `schema: ${defaultSchema}\n`);
|
|
112
|
+
}
|
|
113
|
+
// Write metadata file with schema and creation date
|
|
114
|
+
writeChangeMetadata(changeDir, {
|
|
115
|
+
schema: schemaName,
|
|
116
|
+
created: formatLocalDate(),
|
|
117
|
+
...options.metadata,
|
|
118
|
+
}, projectRoot);
|
|
119
|
+
return { schema: schemaName, changeName: folderName, changeDir };
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=change-utils.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI environment detection shared by telemetry and the version check.
|
|
3
|
+
*
|
|
4
|
+
* Providers set CI to "true", "1", "yes", etc. Only an explicit off-value
|
|
5
|
+
* counts as "not CI", so an unknown value still suppresses outbound requests
|
|
6
|
+
* rather than surprising a build.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* True when `CI` is set to anything other than an explicit off-value.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isCiEnvironment(env?: NodeJS.ProcessEnv): boolean;
|
|
12
|
+
//# sourceMappingURL=ci.d.ts.map
|
package/dist/utils/ci.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI environment detection shared by telemetry and the version check.
|
|
3
|
+
*
|
|
4
|
+
* Providers set CI to "true", "1", "yes", etc. Only an explicit off-value
|
|
5
|
+
* counts as "not CI", so an unknown value still suppresses outbound requests
|
|
6
|
+
* rather than surprising a build.
|
|
7
|
+
*/
|
|
8
|
+
const CI_DISABLED_VALUES = new Set(['', 'false', '0', 'no', 'off']);
|
|
9
|
+
/**
|
|
10
|
+
* True when `CI` is set to anything other than an explicit off-value.
|
|
11
|
+
*/
|
|
12
|
+
export function isCiEnvironment(env = process.env) {
|
|
13
|
+
const value = env.CI;
|
|
14
|
+
return value !== undefined && !CI_DISABLED_VALUES.has(value.trim().toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=ci.js.map
|