@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,174 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves whether non-interactive mode is requested.
|
|
4
|
+
* Handles both explicit `noInteractive: true` and Commander.js style `interactive: false`.
|
|
5
|
+
* Use this helper instead of manually checking options.noInteractive to avoid bugs.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveNoInteractive(value) {
|
|
8
|
+
if (typeof value === 'boolean')
|
|
9
|
+
return value;
|
|
10
|
+
return value?.noInteractive === true || value?.interactive === false;
|
|
11
|
+
}
|
|
12
|
+
export function isInteractive(value) {
|
|
13
|
+
if (resolveNoInteractive(value))
|
|
14
|
+
return false;
|
|
15
|
+
if (process.env.OPEN_SPEC_INTERACTIVE === '0')
|
|
16
|
+
return false;
|
|
17
|
+
// Respect the standard CI environment variable (set by GitHub Actions, GitLab CI, Travis, etc.)
|
|
18
|
+
if ('CI' in process.env)
|
|
19
|
+
return false;
|
|
20
|
+
return !!process.stdin.isTTY;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* True when a prompt failed because no answer could be read — an agent or a
|
|
24
|
+
* script that ran the command with stdin closed, a CI job, or a shell whose
|
|
25
|
+
* stdin is not a terminal. @inquirer rejects those with `User force closed
|
|
26
|
+
* the prompt with 0 null`, which is accurate and useless: it names no flag
|
|
27
|
+
* and no next step (#1479).
|
|
28
|
+
*
|
|
29
|
+
* Two things it deliberately is not:
|
|
30
|
+
*
|
|
31
|
+
* - It is not a substitute for `isInteractive()`. This classifies a prompt
|
|
32
|
+
* that has *already failed*, so piped answers are unaffected: an answer
|
|
33
|
+
* that arrives resolves the prompt and never reaches this check. Refusing
|
|
34
|
+
* to prompt up front would break `printf 'y\n' | freespec archive ...`,
|
|
35
|
+
* which works today.
|
|
36
|
+
* - It is not a cancellation check. Ctrl-C raises the same error class, and
|
|
37
|
+
* it reaches a process whose stdin is a pipe just as easily as one at a
|
|
38
|
+
* terminal, so the SIGINT signal - not the terminal - is what proves
|
|
39
|
+
* somebody was there and chose to quit.
|
|
40
|
+
*
|
|
41
|
+
* Beyond that it defers to `isInteractive()`, so `CI`, `OPEN_SPEC_INTERACTIVE=0`
|
|
42
|
+
* and `--no-interactive` count even when a runner allocated a pty. It also
|
|
43
|
+
* counts a redirected stdout: `confirmPrompt` drops to the plain reader whenever
|
|
44
|
+
* *either* stream is not a TTY, so a stdin-TTY-but-stdout-redirected run
|
|
45
|
+
* (`freespec archive x > log.txt` from a terminal) that hits EOF must classify
|
|
46
|
+
* the same way the prompt was selected — otherwise it would leak the raw
|
|
47
|
+
* `ExitPromptError` instead of the `--yes` guidance.
|
|
48
|
+
*/
|
|
49
|
+
export function isNonInteractivePromptError(error, value) {
|
|
50
|
+
if (!(error instanceof Error))
|
|
51
|
+
return false;
|
|
52
|
+
const failedPrompt = error.name === 'ExitPromptError' || error.message.includes('force closed the prompt');
|
|
53
|
+
if (!failedPrompt)
|
|
54
|
+
return false;
|
|
55
|
+
if (error.message.includes('SIGINT'))
|
|
56
|
+
return false;
|
|
57
|
+
return !isInteractive(value) || !process.stdout.isTTY;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ask a yes/no question. A real terminal gets @inquirer's rich prompt;
|
|
61
|
+
* everything else — a pipe, a file redirect, an agent that captures stdout —
|
|
62
|
+
* reads one plain line instead.
|
|
63
|
+
*
|
|
64
|
+
* @inquirer renders `confirm` by writing ANSI cursor-movement escape sequences,
|
|
65
|
+
* and it emits them even when stdout is not a TTY. Redirected to a file those
|
|
66
|
+
* sequences are noise, and in some non-TTY hosts the render loop never settles
|
|
67
|
+
* and repeats `ESC[NNG` cursor moves until the disk fills (#1526). Reading
|
|
68
|
+
* the answer ourselves keeps the single piped answer @inquirer ever supported
|
|
69
|
+
* working (`printf 'y\n' | freespec archive ...`) without emitting any escapes.
|
|
70
|
+
*
|
|
71
|
+
* `io` overrides the streams; it exists for tests and mirrors @inquirer's own
|
|
72
|
+
* `{ input, output }` context. Production callers pass only the prompt.
|
|
73
|
+
*/
|
|
74
|
+
export async function confirmPrompt(prompt, io = {}) {
|
|
75
|
+
const input = io.input ?? process.stdin;
|
|
76
|
+
const output = io.output ?? process.stdout;
|
|
77
|
+
const isTerminal = Boolean(input.isTTY) &&
|
|
78
|
+
Boolean(output.isTTY);
|
|
79
|
+
if (isTerminal) {
|
|
80
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
81
|
+
return confirm(prompt);
|
|
82
|
+
}
|
|
83
|
+
return readYesNo(prompt, input, output);
|
|
84
|
+
}
|
|
85
|
+
function readYesNo(prompt, input, output) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
let settled = false;
|
|
88
|
+
// Detach the input-stream error listener on settle: `input` is the
|
|
89
|
+
// long-lived process.stdin, so a leftover listener would accumulate across
|
|
90
|
+
// archive's successive prompts and swallow a later, unrelated stdin error.
|
|
91
|
+
const cleanup = () => {
|
|
92
|
+
input.removeListener('error', onError);
|
|
93
|
+
};
|
|
94
|
+
const blockOnNoAnswer = () => {
|
|
95
|
+
if (settled)
|
|
96
|
+
return;
|
|
97
|
+
settled = true;
|
|
98
|
+
cleanup();
|
|
99
|
+
// No line could be read (stdin closed / EOF). Mirror @inquirer's failure
|
|
100
|
+
// so callers that classify it — isNonInteractivePromptError, the #1479
|
|
101
|
+
// "rerun with --yes" guidance — keep working unchanged.
|
|
102
|
+
const error = new Error('User force closed the prompt');
|
|
103
|
+
error.name = 'ExitPromptError';
|
|
104
|
+
reject(error);
|
|
105
|
+
};
|
|
106
|
+
// A stdin error surfaces on the interface (readline forwards input-stream
|
|
107
|
+
// errors since Node 16). Without a handler the promise would hang and the
|
|
108
|
+
// 'error' would go unhandled; settle it with the real fault instead.
|
|
109
|
+
const onError = (err) => {
|
|
110
|
+
if (settled)
|
|
111
|
+
return;
|
|
112
|
+
settled = true;
|
|
113
|
+
cleanup();
|
|
114
|
+
rl.close();
|
|
115
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
116
|
+
};
|
|
117
|
+
// An earlier prompt may have already drained stdin (only one piped answer
|
|
118
|
+
// was ever supported). A fresh readline over an ended stream never emits
|
|
119
|
+
// 'close', so guard here rather than hang and exit as a no-op.
|
|
120
|
+
if (input.readableEnded) {
|
|
121
|
+
blockOnNoAnswer();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
output.write(`${prompt.message} ${prompt.default ? '(Y/n)' : '(y/N)'} `);
|
|
125
|
+
// terminal:false guarantees readline never emits its own line-editing
|
|
126
|
+
// escapes — an escape-free read is the whole point here.
|
|
127
|
+
const rl = createInterface({ input, terminal: false });
|
|
128
|
+
input.once('error', onError);
|
|
129
|
+
rl.once('error', onError);
|
|
130
|
+
rl.once('line', (line) => {
|
|
131
|
+
if (settled)
|
|
132
|
+
return;
|
|
133
|
+
settled = true;
|
|
134
|
+
cleanup();
|
|
135
|
+
rl.close();
|
|
136
|
+
output.write('\n');
|
|
137
|
+
// Mirror @inquirer/confirm's parser (prefix match on y/yes and n/no,
|
|
138
|
+
// otherwise the default) so a piped answer resolves identically to the
|
|
139
|
+
// interactive prompt it replaces.
|
|
140
|
+
const answer = line.trim();
|
|
141
|
+
if (/^(y|yes)/i.test(answer)) {
|
|
142
|
+
resolve(true);
|
|
143
|
+
}
|
|
144
|
+
else if (/^(n|no)/i.test(answer)) {
|
|
145
|
+
resolve(false);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
resolve(prompt.default);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
rl.once('close', () => {
|
|
152
|
+
blockOnNoAnswer();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Whether a spinner can safely render to `stream`.
|
|
158
|
+
*
|
|
159
|
+
* ora decides how many lines to erase with `ceil(width / stream.columns)`, so a
|
|
160
|
+
* TTY that reports zero columns makes that count `Infinity` and `clear()` never
|
|
161
|
+
* ends: the spinner emits cursor-up/clear-line escapes as fast as it can and the
|
|
162
|
+
* command never finishes (#2106). Zero-width TTYs are real — a pty inherited
|
|
163
|
+
* from a pre-commit runner such as lefthook, or `script -qec` — and nothing can
|
|
164
|
+
* be drawn in zero columns anyway, so refuse the spinner and let the caller fall
|
|
165
|
+
* back to its plain output.
|
|
166
|
+
*/
|
|
167
|
+
export function canRenderSpinner(stream = process.stderr) {
|
|
168
|
+
// A non-TTY stream never enters the clear loop: ora disables itself and
|
|
169
|
+
// prints one plain line instead.
|
|
170
|
+
if (!stream.isTTY)
|
|
171
|
+
return true;
|
|
172
|
+
return (stream.columns ?? 0) > 0;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=interactive.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the ids of active changes: every `<名称>变更` directory directly under freespec/.
|
|
3
|
+
*
|
|
4
|
+
* A change is resolved by its directory alone - the same rule `list`,
|
|
5
|
+
* `status`, `instructions` and `validate` use (`getAvailableChanges`).
|
|
6
|
+
* Requiring proposal.md here made `freespec show` and shell completion miss
|
|
7
|
+
* changes those commands resolve: `freespec new change <name>` scaffolds only
|
|
8
|
+
* `.freespec.yaml`, and a custom schema need not define a proposal artifact at
|
|
9
|
+
* all (#1161).
|
|
10
|
+
*/
|
|
11
|
+
export declare function getActiveChangeIds(root?: string): Promise<string[]>;
|
|
12
|
+
/** Main specs and archived changes are not part of this workflow. */
|
|
13
|
+
export declare function getSpecIds(_root?: string): Promise<string[]>;
|
|
14
|
+
export declare function getArchivedChangeIds(_root?: string): Promise<string[]>;
|
|
15
|
+
//# sourceMappingURL=item-discovery.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { changeParentDir, isChangeDirectoryName } from '../core/change-layout.js';
|
|
3
|
+
/**
|
|
4
|
+
* Returns the ids of active changes: every `<名称>变更` directory directly under freespec/.
|
|
5
|
+
*
|
|
6
|
+
* A change is resolved by its directory alone - the same rule `list`,
|
|
7
|
+
* `status`, `instructions` and `validate` use (`getAvailableChanges`).
|
|
8
|
+
* Requiring proposal.md here made `freespec show` and shell completion miss
|
|
9
|
+
* changes those commands resolve: `freespec new change <name>` scaffolds only
|
|
10
|
+
* `.freespec.yaml`, and a custom schema need not define a proposal artifact at
|
|
11
|
+
* all (#1161).
|
|
12
|
+
*/
|
|
13
|
+
export async function getActiveChangeIds(root = process.cwd()) {
|
|
14
|
+
const changesPath = changeParentDir(root);
|
|
15
|
+
try {
|
|
16
|
+
const entries = await fs.readdir(changesPath, { withFileTypes: true });
|
|
17
|
+
return entries
|
|
18
|
+
.filter((entry) => entry.isDirectory() && isChangeDirectoryName(entry.name))
|
|
19
|
+
.map((entry) => entry.name)
|
|
20
|
+
.sort();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Main specs and archived changes are not part of this workflow. */
|
|
27
|
+
export async function getSpecIds(_root = process.cwd()) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
export async function getArchivedChangeIds(_root = process.cwd()) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=item-discovery.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-ending helpers.
|
|
3
|
+
*
|
|
4
|
+
* Every parser in this codebase normalizes CRLF to LF on the way in, so all
|
|
5
|
+
* serialization logic can assume '\n'. That leaves the write side responsible
|
|
6
|
+
* for restoring whatever convention the file already used: without it, editing
|
|
7
|
+
* one requirement in a CRLF spec rewrites every line of the file and buries the
|
|
8
|
+
* real change in the diff.
|
|
9
|
+
*/
|
|
10
|
+
export type LineEnding = '\n' | '\r\n';
|
|
11
|
+
/**
|
|
12
|
+
* The dominant line ending in `content`, or undefined when it holds no line
|
|
13
|
+
* break to judge from.
|
|
14
|
+
*
|
|
15
|
+
* Mixed files resolve to whichever ending is more common, with CRLF winning a
|
|
16
|
+
* tie: a file that is mostly CRLF is a CRLF file that picked up a stray LF, and
|
|
17
|
+
* settling the whole file on one ending is what keeps later diffs small.
|
|
18
|
+
*/
|
|
19
|
+
export declare function detectLineEnding(content: string): LineEnding | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Re-apply `ending` to LF-normalized `content`.
|
|
22
|
+
*
|
|
23
|
+
* Normalizes to LF first, so the result is uniform even if the caller passed
|
|
24
|
+
* content that already contained CRLF.
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyLineEnding(content: string, ending: LineEnding): string;
|
|
27
|
+
/**
|
|
28
|
+
* Rewrite `content` to match the convention of `original`.
|
|
29
|
+
*
|
|
30
|
+
* When `original` has no line break to learn from, `content` is left as LF —
|
|
31
|
+
* the portable default this project writes new files with.
|
|
32
|
+
*/
|
|
33
|
+
export declare function matchLineEnding(content: string, original: string): string;
|
|
34
|
+
//# sourceMappingURL=line-endings.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-ending helpers.
|
|
3
|
+
*
|
|
4
|
+
* Every parser in this codebase normalizes CRLF to LF on the way in, so all
|
|
5
|
+
* serialization logic can assume '\n'. That leaves the write side responsible
|
|
6
|
+
* for restoring whatever convention the file already used: without it, editing
|
|
7
|
+
* one requirement in a CRLF spec rewrites every line of the file and buries the
|
|
8
|
+
* real change in the diff.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The dominant line ending in `content`, or undefined when it holds no line
|
|
12
|
+
* break to judge from.
|
|
13
|
+
*
|
|
14
|
+
* Mixed files resolve to whichever ending is more common, with CRLF winning a
|
|
15
|
+
* tie: a file that is mostly CRLF is a CRLF file that picked up a stray LF, and
|
|
16
|
+
* settling the whole file on one ending is what keeps later diffs small.
|
|
17
|
+
*/
|
|
18
|
+
export function detectLineEnding(content) {
|
|
19
|
+
const crlf = content.match(/\r\n/g)?.length ?? 0;
|
|
20
|
+
// Count LFs not preceded by CR, so CRLF is never also counted as LF.
|
|
21
|
+
const lf = content.match(/(?<!\r)\n/g)?.length ?? 0;
|
|
22
|
+
if (crlf === 0 && lf === 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
return crlf >= lf ? '\r\n' : '\n';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Re-apply `ending` to LF-normalized `content`.
|
|
28
|
+
*
|
|
29
|
+
* Normalizes to LF first, so the result is uniform even if the caller passed
|
|
30
|
+
* content that already contained CRLF.
|
|
31
|
+
*/
|
|
32
|
+
export function applyLineEnding(content, ending) {
|
|
33
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
34
|
+
return ending === '\n' ? normalized : normalized.replace(/\n/g, '\r\n');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Rewrite `content` to match the convention of `original`.
|
|
38
|
+
*
|
|
39
|
+
* When `original` has no line break to learn from, `content` is left as LF —
|
|
40
|
+
* the portable default this project writes new files with.
|
|
41
|
+
*/
|
|
42
|
+
export function matchLineEnding(content, original) {
|
|
43
|
+
const ending = detectLineEnding(original);
|
|
44
|
+
return ending === undefined ? content : applyLineEnding(content, ending);
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=line-endings.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function nearestMatches(input, candidates, max = 5) {
|
|
2
|
+
const scored = candidates.map(candidate => ({ candidate, distance: levenshtein(input, candidate) }));
|
|
3
|
+
scored.sort((a, b) => a.distance - b.distance);
|
|
4
|
+
return scored.slice(0, max).map(s => s.candidate);
|
|
5
|
+
}
|
|
6
|
+
export function levenshtein(a, b) {
|
|
7
|
+
const m = a.length;
|
|
8
|
+
const n = b.length;
|
|
9
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
10
|
+
for (let i = 0; i <= m; i++)
|
|
11
|
+
dp[i][0] = i;
|
|
12
|
+
for (let j = 0; j <= n; j++)
|
|
13
|
+
dp[0][j] = j;
|
|
14
|
+
for (let i = 1; i <= m; i++) {
|
|
15
|
+
for (let j = 1; j <= n; j++) {
|
|
16
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
17
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return dp[m][n];
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=match.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** A namespace folder under `freespec/` and the change directories buried in it. */
|
|
2
|
+
export interface NestedChangeFinding {
|
|
3
|
+
/** The directory name directly under `freespec/` that is not a change. */
|
|
4
|
+
name: string;
|
|
5
|
+
/**
|
|
6
|
+
* Slash-separated paths of the nested change directories, relative to
|
|
7
|
+
* `freespec/`, sorted. Always at least one entry.
|
|
8
|
+
*/
|
|
9
|
+
nested: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Reports whether the directory `freespec/<name>/` is a namespace folder holding
|
|
13
|
+
* one or more nested change directories rather than a change of its own.
|
|
14
|
+
*
|
|
15
|
+
* Returns undefined for every ordinary change, including a scaffolded one with
|
|
16
|
+
* no artifacts yet: only a *nested* change directory distinguishes the two.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findNestedChangesIn(changesDir: string, name: string): Promise<NestedChangeFinding | undefined>;
|
|
19
|
+
/**
|
|
20
|
+
* The same check across every candidate directory under `freespec/`, for the
|
|
21
|
+
* commands that enumerate rather than resolve a single name.
|
|
22
|
+
*/
|
|
23
|
+
export declare function findNestedChanges(changesDir: string, names: string[]): Promise<NestedChangeFinding[]>;
|
|
24
|
+
/**
|
|
25
|
+
* The stable phrase every nested-change diagnostic contains. Callers that have
|
|
26
|
+
* only the rendered message - `validate`'s next-steps, which branches on the
|
|
27
|
+
* exact issue it is advising about - recognise the case by this.
|
|
28
|
+
*/
|
|
29
|
+
export declare const NESTED_CHANGE_ISSUE_MARKER = "is not a change: it is a folder wrapping";
|
|
30
|
+
/**
|
|
31
|
+
* The one explanation every surface prints, so the constraint and the way out
|
|
32
|
+
* read identically wherever a user meets them.
|
|
33
|
+
*/
|
|
34
|
+
export declare function describeNestedChange(finding: NestedChangeFinding): string;
|
|
35
|
+
//# sourceMappingURL=nested-change.d.ts.map
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { artifactOutputExists } from '../core/artifact-graph/outputs.js';
|
|
4
|
+
import { resolveSchema } from '../core/artifact-graph/resolver.js';
|
|
5
|
+
import { METADATA_FILENAME, resolveSchemaForChange } from './change-metadata.js';
|
|
6
|
+
/**
|
|
7
|
+
* Files that only ever sit at the root of a change directory. Their presence
|
|
8
|
+
* inside a directory that is *not* directly under `freespec/` is one of the two
|
|
9
|
+
* signals that someone laid a change out in a namespace folder
|
|
10
|
+
* (`freespec/<area>/<名称>变更/`), which this workflow does not support: a change
|
|
11
|
+
* is a directory directly under `freespec/`, named `<名称>变更`.
|
|
12
|
+
*
|
|
13
|
+
* The list is deliberately short. Every entry is written by FreeSpec itself for
|
|
14
|
+
* the default schema - `.freespec.yaml` by `freespec new change` for *every*
|
|
15
|
+
* schema. A custom schema whose root artifacts are named differently can still
|
|
16
|
+
* be missed when the directory was created by hand rather than by the CLI; that
|
|
17
|
+
* misses the diagnostic and behaves exactly as it did before, which is the safe
|
|
18
|
+
* way round.
|
|
19
|
+
*/
|
|
20
|
+
const CHANGE_ROOT_MARKERS = [
|
|
21
|
+
METADATA_FILENAME,
|
|
22
|
+
'需求文档.md',
|
|
23
|
+
'设计文档.md',
|
|
24
|
+
'任务清单.md',
|
|
25
|
+
'测试用例.md',
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* How far below a candidate directory to look for a nested change. One level
|
|
29
|
+
* covers a nested layout (`freespec/移动端/刷新变更/`); the
|
|
30
|
+
* extra levels cover a store namespaced by more than one axis
|
|
31
|
+
* (`freespec/移动端/苹果/刷新变更/`) without turning this into an unbounded
|
|
32
|
+
* walk of every change's spec tree.
|
|
33
|
+
*/
|
|
34
|
+
const MAX_NESTING_DEPTH = 3;
|
|
35
|
+
async function hasChangeRootMarker(dir) {
|
|
36
|
+
for (const marker of CHANGE_ROOT_MARKERS) {
|
|
37
|
+
const stats = await fs.stat(path.join(dir, marker)).catch(() => undefined);
|
|
38
|
+
if (stats?.isFile())
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether `dir` is a change rather than a folder wrapping one.
|
|
45
|
+
*
|
|
46
|
+
* Two independent signals, because either alone leaves a hole. A root artifact
|
|
47
|
+
* covers the CLI-created change and the hand-made one that starts from a
|
|
48
|
+
* proposal. A populated `specs/` covers the change that starts from its delta
|
|
49
|
+
* specs - the case that arrives here most often, since a nested change is
|
|
50
|
+
* always hand-made (`freespec new change` rejects a name with a separator) and
|
|
51
|
+
* "mkdir the tree, write the deltas" is how someone gets there.
|
|
52
|
+
*
|
|
53
|
+
* The `specs/` signal is what keeps a custom schema safe. A schema may generate
|
|
54
|
+
* every root artifact into a subdirectory (`generates: rfc/proposal.md`), and
|
|
55
|
+
* such a change, created by hand so it has no `.freespec.yaml`, would otherwise
|
|
56
|
+
* look exactly like a namespace folder wrapping a change called `rfc`. Its
|
|
57
|
+
* delta specs still live under `specs/`, so it is recognised as the change it
|
|
58
|
+
* is. `specs/**\/*.md` is fixed by the delta format itself, not by the schema.
|
|
59
|
+
*/
|
|
60
|
+
async function looksLikeChange(dir, projectRoot) {
|
|
61
|
+
if (await hasChangeRootMarker(dir))
|
|
62
|
+
return true;
|
|
63
|
+
const thinking = await fs.stat(path.join(dir, '深思')).catch(() => undefined);
|
|
64
|
+
if (thinking?.isDirectory())
|
|
65
|
+
return true;
|
|
66
|
+
return hasSchemaOutput(dir, projectRoot);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether `dir` already holds a file exactly where the schema it resolves to
|
|
70
|
+
* would generate one - the signal `freespec status` itself uses to call an
|
|
71
|
+
* artifact done. It covers the subdirectory-rooted custom schema before any
|
|
72
|
+
* delta spec exists, when neither other signal can. A schema that cannot be
|
|
73
|
+
* resolved gives no signal, which leaves the other two in charge.
|
|
74
|
+
*/
|
|
75
|
+
function hasSchemaOutput(dir, projectRoot) {
|
|
76
|
+
try {
|
|
77
|
+
const schema = resolveSchema(resolveSchemaForChange(dir, undefined, projectRoot), projectRoot);
|
|
78
|
+
return schema.artifacts.some((artifact) => artifactOutputExists(dir, artifact.generates));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether `dir` holds any file of its own. A change directory with content at
|
|
86
|
+
* its root - a custom schema's artifact under a name this module does not know,
|
|
87
|
+
* a README, a note - is never reported as a namespace folder. A namespace
|
|
88
|
+
* folder someone created with `mkdir` holds nothing but directories.
|
|
89
|
+
*/
|
|
90
|
+
async function hasOwnFile(dir) {
|
|
91
|
+
const entries = await fs
|
|
92
|
+
.readdir(dir, { withFileTypes: true })
|
|
93
|
+
.catch(() => []);
|
|
94
|
+
return entries.some((entry) => !entry.name.startsWith('.') && !entry.isDirectory());
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Subdirectories of `dir`, or none when it cannot be read. This probe only ever
|
|
98
|
+
* adds a diagnostic, so an unreadable directory falls back to the behaviour
|
|
99
|
+
* that existed before it rather than failing the command around it.
|
|
100
|
+
*/
|
|
101
|
+
async function readSubdirectories(dir) {
|
|
102
|
+
const entries = await fs
|
|
103
|
+
.readdir(dir, { withFileTypes: true })
|
|
104
|
+
.catch(() => []);
|
|
105
|
+
return entries
|
|
106
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
107
|
+
.map((entry) => entry.name);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Collects the nested change directories under `dir`, deepest search bounded by
|
|
111
|
+
* `MAX_NESTING_DEPTH`. A directory that carries a marker is reported and not
|
|
112
|
+
* descended into: its own `specs/` tree is part of that change, not a further
|
|
113
|
+
* nesting level.
|
|
114
|
+
*/
|
|
115
|
+
async function collectNested(dir, prefix, depth, found, projectRoot) {
|
|
116
|
+
if (depth > MAX_NESTING_DEPTH)
|
|
117
|
+
return;
|
|
118
|
+
for (const child of await readSubdirectories(dir)) {
|
|
119
|
+
const childPath = path.join(dir, child);
|
|
120
|
+
const id = `${prefix}/${child}`;
|
|
121
|
+
if (await looksLikeChange(childPath, projectRoot)) {
|
|
122
|
+
found.push(id);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
await collectNested(childPath, id, depth + 1, found, projectRoot);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Reports whether the directory `freespec/<name>/` is a namespace folder holding
|
|
130
|
+
* one or more nested change directories rather than a change of its own.
|
|
131
|
+
*
|
|
132
|
+
* Returns undefined for every ordinary change, including a scaffolded one with
|
|
133
|
+
* no artifacts yet: only a *nested* change directory distinguishes the two.
|
|
134
|
+
*/
|
|
135
|
+
export async function findNestedChangesIn(changesDir, name) {
|
|
136
|
+
// `archive` is not a change and never was, so its dated entries must not be
|
|
137
|
+
// read as nested changes and offered up for renaming. `list`, `status` and
|
|
138
|
+
// `validate` already exclude it; `change show` does not.
|
|
139
|
+
if (name === 'archive' || name.startsWith('.'))
|
|
140
|
+
return undefined;
|
|
141
|
+
const dir = path.join(changesDir, name);
|
|
142
|
+
// Change folders sit directly under <root>/freespec.
|
|
143
|
+
const projectRoot = path.resolve(changesDir, '..');
|
|
144
|
+
if (await looksLikeChange(dir, projectRoot))
|
|
145
|
+
return undefined;
|
|
146
|
+
if (await hasOwnFile(dir))
|
|
147
|
+
return undefined;
|
|
148
|
+
const nested = [];
|
|
149
|
+
await collectNested(dir, name, 1, nested, projectRoot);
|
|
150
|
+
if (nested.length === 0)
|
|
151
|
+
return undefined;
|
|
152
|
+
return { name, nested: nested.sort() };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The same check across every candidate directory under `freespec/`, for the
|
|
156
|
+
* commands that enumerate rather than resolve a single name.
|
|
157
|
+
*/
|
|
158
|
+
export async function findNestedChanges(changesDir, names) {
|
|
159
|
+
const findings = await Promise.all(names.map((name) => findNestedChangesIn(changesDir, name)));
|
|
160
|
+
return findings.filter((finding) => finding !== undefined);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The stable phrase every nested-change diagnostic contains. Callers that have
|
|
164
|
+
* only the rendered message - `validate`'s next-steps, which branches on the
|
|
165
|
+
* exact issue it is advising about - recognise the case by this.
|
|
166
|
+
*/
|
|
167
|
+
export const NESTED_CHANGE_ISSUE_MARKER = 'is not a change: it is a folder wrapping';
|
|
168
|
+
/** Flattens a namespace path into the change name FreeSpec would accept. */
|
|
169
|
+
function flatten(nestedId) {
|
|
170
|
+
return nestedId.split('/').join('-');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The one explanation every surface prints, so the constraint and the way out
|
|
174
|
+
* read identically wherever a user meets them.
|
|
175
|
+
*/
|
|
176
|
+
export function describeNestedChange(finding) {
|
|
177
|
+
const list = finding.nested.map((id) => `freespec/${id}/`).join(', ');
|
|
178
|
+
const example = flatten(finding.nested[0]);
|
|
179
|
+
return (`"${finding.name}" ${NESTED_CHANGE_ISSUE_MARKER} ${list}. ` +
|
|
180
|
+
'A change must be a directory directly under freespec/, named with a 变更 suffix, so those ' +
|
|
181
|
+
'nested directories are invisible while the folder around them ' +
|
|
182
|
+
'is reported as a change. ' +
|
|
183
|
+
`Rename each nested change to a flat name (for example "${example}").`);
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=nested-change.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported shell types for completion generation
|
|
3
|
+
*/
|
|
4
|
+
export type SupportedShell = 'zsh' | 'bash' | 'fish' | 'powershell';
|
|
5
|
+
/**
|
|
6
|
+
* Result of shell detection
|
|
7
|
+
*/
|
|
8
|
+
export interface ShellDetectionResult {
|
|
9
|
+
/** The detected shell if supported, otherwise undefined */
|
|
10
|
+
shell: SupportedShell | undefined;
|
|
11
|
+
/** The raw shell name detected (even if unsupported), or undefined if nothing detected */
|
|
12
|
+
detected: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Detects the current user's shell based on the parent process and environment
|
|
16
|
+
*
|
|
17
|
+
* @returns Detection result with supported shell and raw detected name
|
|
18
|
+
*/
|
|
19
|
+
export declare function detectShell(): ShellDetectionResult;
|
|
20
|
+
//# sourceMappingURL=shell-detection.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Map a raw shell name/path to a supported shell, if any.
|
|
4
|
+
*/
|
|
5
|
+
function matchSupportedShell(name) {
|
|
6
|
+
// Match the executable basename exactly so lookalikes such as `fish-lsp` or
|
|
7
|
+
// `bash-language-server` don't get mistaken for the shell itself. Login
|
|
8
|
+
// shells report a leading dash (e.g. `-zsh`), so strip it first.
|
|
9
|
+
const executable = name.trim().toLowerCase().split('/').pop()?.replace(/^-/, '');
|
|
10
|
+
if (executable === 'zsh')
|
|
11
|
+
return 'zsh';
|
|
12
|
+
if (executable === 'bash')
|
|
13
|
+
return 'bash';
|
|
14
|
+
if (executable === 'fish')
|
|
15
|
+
return 'fish';
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Detect the interactive shell from the parent process.
|
|
20
|
+
*
|
|
21
|
+
* `process.env.SHELL` is only the login shell, so users whose interactive shell
|
|
22
|
+
* differs from it (e.g. running fish while their login shell is bash) are
|
|
23
|
+
* misdetected. Inspecting the parent process reflects the shell that actually
|
|
24
|
+
* launched freespec. POSIX-only and best-effort — any failure returns undefined
|
|
25
|
+
* so the caller falls back to `$SHELL`.
|
|
26
|
+
*
|
|
27
|
+
* @returns The supported shell running as the parent process, or undefined
|
|
28
|
+
*/
|
|
29
|
+
function detectShellFromParentProcess() {
|
|
30
|
+
// `ps` is POSIX-only; Windows shells are handled via PSModulePath/COMSPEC.
|
|
31
|
+
if (process.platform === 'win32') {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const ppid = process.ppid;
|
|
35
|
+
if (!ppid || ppid <= 1) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const comm = execFileSync('ps', ['-p', String(ppid), '-o', 'comm='], {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
timeout: 1000,
|
|
42
|
+
}).trim();
|
|
43
|
+
if (!comm) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
// Only trust the parent process when it maps to a supported shell; an
|
|
47
|
+
// unrelated parent (node, npm, sudo, a pager) falls through to `$SHELL`.
|
|
48
|
+
return matchSupportedShell(comm);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Detects the current user's shell based on the parent process and environment
|
|
56
|
+
*
|
|
57
|
+
* @returns Detection result with supported shell and raw detected name
|
|
58
|
+
*/
|
|
59
|
+
export function detectShell() {
|
|
60
|
+
// Prefer the actual running shell (parent process) over `$SHELL`, which only
|
|
61
|
+
// reflects the login shell and misses users whose interactive shell differs.
|
|
62
|
+
const parentShell = detectShellFromParentProcess();
|
|
63
|
+
if (parentShell) {
|
|
64
|
+
return { shell: parentShell, detected: parentShell };
|
|
65
|
+
}
|
|
66
|
+
// Try SHELL environment variable next (Unix-like systems)
|
|
67
|
+
const shellPath = process.env.SHELL;
|
|
68
|
+
if (shellPath) {
|
|
69
|
+
const supported = matchSupportedShell(shellPath);
|
|
70
|
+
if (supported) {
|
|
71
|
+
return { shell: supported, detected: supported };
|
|
72
|
+
}
|
|
73
|
+
// Shell detected but not supported
|
|
74
|
+
// Extract shell name from path (e.g., /bin/tcsh -> tcsh)
|
|
75
|
+
const match = shellPath.match(/\/([^/]+)$/);
|
|
76
|
+
const detectedName = match ? match[1] : shellPath;
|
|
77
|
+
return { shell: undefined, detected: detectedName };
|
|
78
|
+
}
|
|
79
|
+
// Check for PowerShell on Windows
|
|
80
|
+
// PSModulePath is a reliable PowerShell-specific environment variable
|
|
81
|
+
if (process.env.PSModulePath || process.platform === 'win32') {
|
|
82
|
+
const comspec = process.env.COMSPEC?.toLowerCase();
|
|
83
|
+
// If PSModulePath exists, we're definitely in PowerShell
|
|
84
|
+
if (process.env.PSModulePath) {
|
|
85
|
+
return { shell: 'powershell', detected: 'powershell' };
|
|
86
|
+
}
|
|
87
|
+
// On Windows without PSModulePath, we might be in cmd.exe
|
|
88
|
+
if (comspec?.includes('cmd.exe')) {
|
|
89
|
+
return { shell: undefined, detected: 'cmd.exe' };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { shell: undefined, detected: undefined };
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=shell-detection.js.map
|