@niujiajie/freespec 0.0.0-stage → 1.15.1
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,268 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { getSchemaDir, resolveSchema, listSchemasWithInfo } from './resolver.js';
|
|
4
|
+
import { ArtifactGraph } from './graph.js';
|
|
5
|
+
import { detectCompleted } from './state.js';
|
|
6
|
+
import { resolveArtifactOutputPath, resolveArtifactOutputs, } from './outputs.js';
|
|
7
|
+
import { formatUnknownChangeMetadataKeysMessage, readChangeMetadata, readUnknownChangeMetadataKeys, resolveSchemaForChange, } from '../../utils/change-metadata.js';
|
|
8
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
9
|
+
import { changeParentDir } from '../change-layout.js';
|
|
10
|
+
import { buildActionContext, buildNextSteps, summarizePlanningHome, } from '../change-status-policy.js';
|
|
11
|
+
import { readProjectConfig, validateConfigRules } from '../project-config.js';
|
|
12
|
+
// Session-level cache for validation warnings (avoid repeating same warnings)
|
|
13
|
+
const shownWarnings = new Set();
|
|
14
|
+
/**
|
|
15
|
+
* Error thrown when loading a template fails.
|
|
16
|
+
*/
|
|
17
|
+
export class TemplateLoadError extends Error {
|
|
18
|
+
templatePath;
|
|
19
|
+
constructor(message, templatePath) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.templatePath = templatePath;
|
|
22
|
+
this.name = 'TemplateLoadError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Loads a template from a schema's templates directory.
|
|
27
|
+
*
|
|
28
|
+
* @param schemaName - Schema name (e.g., "spec-driven")
|
|
29
|
+
* @param templatePath - Relative path within the templates directory (e.g., "proposal.md")
|
|
30
|
+
* @param projectRoot - Optional project root for project-local schema resolution
|
|
31
|
+
* @returns The template content
|
|
32
|
+
* @throws TemplateLoadError if the template cannot be loaded
|
|
33
|
+
*/
|
|
34
|
+
export function loadTemplate(schemaName, templatePath, projectRoot) {
|
|
35
|
+
const schemaDir = getSchemaDir(schemaName, projectRoot);
|
|
36
|
+
if (!schemaDir) {
|
|
37
|
+
throw new TemplateLoadError(`Schema '${schemaName}' not found`, templatePath);
|
|
38
|
+
}
|
|
39
|
+
const templatesDir = path.join(schemaDir, 'templates');
|
|
40
|
+
const templatePathOnDisk = path.join(templatesDir, templatePath);
|
|
41
|
+
try {
|
|
42
|
+
FileSystemUtils.assertPathWithin(templatesDir, templatePathOnDisk);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new TemplateLoadError(error instanceof Error ? error.message : String(error), templatePathOnDisk);
|
|
46
|
+
}
|
|
47
|
+
if (!fs.existsSync(templatePathOnDisk)) {
|
|
48
|
+
throw new TemplateLoadError(`Template not found: ${templatePathOnDisk}`, templatePathOnDisk);
|
|
49
|
+
}
|
|
50
|
+
const fullPath = FileSystemUtils.canonicalizeExistingPath(templatePathOnDisk);
|
|
51
|
+
try {
|
|
52
|
+
return fs.readFileSync(fullPath, 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const ioError = err instanceof Error ? err : new Error(String(err));
|
|
56
|
+
throw new TemplateLoadError(`Failed to read template: ${ioError.message}`, fullPath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Loads change context combining graph and completion state.
|
|
61
|
+
*
|
|
62
|
+
* Schema resolution order:
|
|
63
|
+
* 1. Explicit schemaName parameter (if provided)
|
|
64
|
+
* 2. Schema from .freespec.yaml metadata (if exists in change directory)
|
|
65
|
+
* 3. Default schema (`默认`)
|
|
66
|
+
*
|
|
67
|
+
* @param projectRoot - Project root directory
|
|
68
|
+
* @param changeName - Change name
|
|
69
|
+
* @param schemaName - Optional schema name override. If not provided, auto-detected from metadata.
|
|
70
|
+
* @returns Change context with graph, completed set, and metadata
|
|
71
|
+
*/
|
|
72
|
+
export function loadChangeContext(projectRoot, changeName, schemaName, options = {}) {
|
|
73
|
+
const changeDir = FileSystemUtils.canonicalizeExistingPath(options.changeDir ?? path.join(changeParentDir(projectRoot), changeName));
|
|
74
|
+
const metadata = readChangeMetadata(changeDir, projectRoot) ?? undefined;
|
|
75
|
+
const unknownMetadataKeys = readUnknownChangeMetadataKeys(changeDir);
|
|
76
|
+
const warnings = unknownMetadataKeys.length > 0
|
|
77
|
+
? [formatUnknownChangeMetadataKeysMessage(unknownMetadataKeys)]
|
|
78
|
+
: [];
|
|
79
|
+
const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName, projectRoot, {
|
|
80
|
+
metadata: metadata ?? null,
|
|
81
|
+
projectConfig: options.projectConfig,
|
|
82
|
+
});
|
|
83
|
+
const schema = resolveSchema(resolvedSchemaName, projectRoot);
|
|
84
|
+
const graph = ArtifactGraph.fromSchema(schema);
|
|
85
|
+
const completed = detectCompleted(graph, changeDir);
|
|
86
|
+
return {
|
|
87
|
+
graph,
|
|
88
|
+
completed,
|
|
89
|
+
schemaName: resolvedSchemaName,
|
|
90
|
+
changeName,
|
|
91
|
+
changeDir,
|
|
92
|
+
projectRoot,
|
|
93
|
+
...(options.planningHome ? { planningHome: options.planningHome } : {}),
|
|
94
|
+
...(metadata ? { metadata } : {}),
|
|
95
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function generateInstructions(context, artifactId, projectRoot, options = {}) {
|
|
99
|
+
const artifact = context.graph.getArtifact(artifactId);
|
|
100
|
+
if (!artifact) {
|
|
101
|
+
throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'`);
|
|
102
|
+
}
|
|
103
|
+
const templateContent = loadTemplate(context.schemaName, artifact.template, context.projectRoot);
|
|
104
|
+
const dependencies = getDependencyInfo(artifact, context.graph, context.completed, context.skippedArtifacts);
|
|
105
|
+
const unlocks = getUnlockedArtifacts(context.graph, artifactId);
|
|
106
|
+
// Use projectRoot from context if not explicitly provided
|
|
107
|
+
const effectiveProjectRoot = projectRoot ?? context.projectRoot;
|
|
108
|
+
// Use the pre-read config when provided; otherwise read it here.
|
|
109
|
+
let projectConfig = options.projectConfig ?? null;
|
|
110
|
+
if (options.projectConfig === undefined && effectiveProjectRoot) {
|
|
111
|
+
try {
|
|
112
|
+
projectConfig = readProjectConfig(effectiveProjectRoot);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// If config read fails, continue without config
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Validate rules artifact IDs if config has rules (only once per session).
|
|
119
|
+
// The rules map is global while each change can use a different schema, so a
|
|
120
|
+
// key is only "unknown" when it matches no artifact in ANY available schema.
|
|
121
|
+
if (projectConfig?.rules) {
|
|
122
|
+
const validArtifactIds = new Set(listSchemasWithInfo(effectiveProjectRoot ?? undefined).flatMap((s) => s.artifacts));
|
|
123
|
+
const warnings = validateConfigRules(projectConfig.rules, validArtifactIds);
|
|
124
|
+
// Show each unique warning only once per session
|
|
125
|
+
for (const warning of warnings) {
|
|
126
|
+
if (!shownWarnings.has(warning)) {
|
|
127
|
+
console.warn(warning);
|
|
128
|
+
shownWarnings.add(warning);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Extract context and rules as separate fields (not prepended to template)
|
|
133
|
+
const configContext = projectConfig?.context?.trim() || undefined;
|
|
134
|
+
const rulesForArtifact = projectConfig?.rules && Object.hasOwn(projectConfig.rules, artifactId)
|
|
135
|
+
? projectConfig.rules[artifactId]
|
|
136
|
+
: undefined;
|
|
137
|
+
const configRules = rulesForArtifact && rulesForArtifact.length > 0 ? rulesForArtifact : undefined;
|
|
138
|
+
return {
|
|
139
|
+
changeName: context.changeName,
|
|
140
|
+
artifactId: artifact.id,
|
|
141
|
+
schemaName: context.schemaName,
|
|
142
|
+
changeDir: context.changeDir,
|
|
143
|
+
planningHome: summarizePlanningHome(context.planningHome),
|
|
144
|
+
outputPath: artifact.generates,
|
|
145
|
+
resolvedOutputPath: resolveArtifactOutputPath(context.changeDir, artifact.generates),
|
|
146
|
+
existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
|
|
147
|
+
description: artifact.description,
|
|
148
|
+
instruction: artifact.instruction,
|
|
149
|
+
context: configContext,
|
|
150
|
+
rules: configRules,
|
|
151
|
+
...(context.warnings ? { warnings: context.warnings } : {}),
|
|
152
|
+
template: templateContent,
|
|
153
|
+
dependencies,
|
|
154
|
+
unlocks,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Gets dependency info including paths and descriptions.
|
|
159
|
+
*/
|
|
160
|
+
function getDependencyInfo(artifact, graph, completed, skippedArtifacts) {
|
|
161
|
+
return artifact.requires.map(id => {
|
|
162
|
+
const depArtifact = graph.getArtifact(id);
|
|
163
|
+
return {
|
|
164
|
+
id,
|
|
165
|
+
done: completed.has(id),
|
|
166
|
+
path: depArtifact?.generates ?? id,
|
|
167
|
+
description: depArtifact?.description ?? '',
|
|
168
|
+
...(skippedArtifacts?.has(id) ? { skipped: true } : {}),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Gets artifacts that become available after completing the given artifact.
|
|
174
|
+
*
|
|
175
|
+
* `getAllArtifacts()` already yields the schema's declaration order, so the list
|
|
176
|
+
* is returned as collected: sorting it alphabetically would have `unlocks` name
|
|
177
|
+
* the artifacts in a different order than `status` recommends them.
|
|
178
|
+
*/
|
|
179
|
+
function getUnlockedArtifacts(graph, artifactId) {
|
|
180
|
+
const unlocks = [];
|
|
181
|
+
for (const artifact of graph.getAllArtifacts()) {
|
|
182
|
+
if (artifact.requires.includes(artifactId)) {
|
|
183
|
+
unlocks.push(artifact.id);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return unlocks;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Formats the status of all artifacts in a change.
|
|
190
|
+
*
|
|
191
|
+
* @param context - Change context
|
|
192
|
+
* @returns Formatted change status
|
|
193
|
+
*/
|
|
194
|
+
export function formatChangeStatus(context) {
|
|
195
|
+
// Load schema to get apply phase configuration
|
|
196
|
+
const schema = resolveSchema(context.schemaName, context.projectRoot);
|
|
197
|
+
const applyRequires = schema.apply?.requires ?? schema.artifacts.map(a => a.id);
|
|
198
|
+
const artifacts = context.graph.getAllArtifacts();
|
|
199
|
+
const ready = new Set(context.graph.getNextArtifacts(context.completed));
|
|
200
|
+
const blocked = context.graph.getBlocked(context.completed);
|
|
201
|
+
const artifactPaths = {};
|
|
202
|
+
const artifactStatuses = artifacts.map(artifact => {
|
|
203
|
+
artifactPaths[artifact.id] = {
|
|
204
|
+
outputPath: artifact.generates,
|
|
205
|
+
resolvedOutputPath: resolveArtifactOutputPath(context.changeDir, artifact.generates),
|
|
206
|
+
existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
|
|
207
|
+
};
|
|
208
|
+
if (context.skippedArtifacts?.has(artifact.id)) {
|
|
209
|
+
return {
|
|
210
|
+
id: artifact.id,
|
|
211
|
+
outputPath: artifact.generates,
|
|
212
|
+
status: 'skipped',
|
|
213
|
+
requires: artifact.requires,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (context.completed.has(artifact.id)) {
|
|
217
|
+
return {
|
|
218
|
+
id: artifact.id,
|
|
219
|
+
outputPath: artifact.generates,
|
|
220
|
+
status: 'done',
|
|
221
|
+
requires: artifact.requires,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (ready.has(artifact.id)) {
|
|
225
|
+
return {
|
|
226
|
+
id: artifact.id,
|
|
227
|
+
outputPath: artifact.generates,
|
|
228
|
+
status: 'ready',
|
|
229
|
+
requires: artifact.requires,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
id: artifact.id,
|
|
234
|
+
outputPath: artifact.generates,
|
|
235
|
+
status: 'blocked',
|
|
236
|
+
requires: artifact.requires,
|
|
237
|
+
missingDeps: blocked[artifact.id] ?? [],
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
// Sort by build order for consistent output
|
|
241
|
+
const buildOrder = context.graph.getBuildOrder();
|
|
242
|
+
const orderMap = new Map(buildOrder.map((id, idx) => [id, idx]));
|
|
243
|
+
artifactStatuses.sort((a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0));
|
|
244
|
+
const isComplete = context.graph.isComplete(context.completed);
|
|
245
|
+
const artifactIds = artifactStatuses.map((artifact) => artifact.id);
|
|
246
|
+
return {
|
|
247
|
+
changeName: context.changeName,
|
|
248
|
+
schemaName: context.schemaName,
|
|
249
|
+
planningHome: summarizePlanningHome(context.planningHome),
|
|
250
|
+
changeRoot: context.changeDir,
|
|
251
|
+
artifactPaths,
|
|
252
|
+
isPlanningComplete: isComplete,
|
|
253
|
+
isComplete,
|
|
254
|
+
applyRequires,
|
|
255
|
+
nextSteps: buildNextSteps({
|
|
256
|
+
changeName: context.changeName,
|
|
257
|
+
artifactStatuses,
|
|
258
|
+
allArtifactsComplete: isComplete,
|
|
259
|
+
}),
|
|
260
|
+
actionContext: buildActionContext({
|
|
261
|
+
projectRoot: context.projectRoot,
|
|
262
|
+
artifactIds,
|
|
263
|
+
}),
|
|
264
|
+
artifacts: artifactStatuses,
|
|
265
|
+
...(context.warnings ? { warnings: context.warnings } : {}),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=instruction-loader.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recognizes artifact globs while preserving literal output filenames.
|
|
3
|
+
*/
|
|
4
|
+
export declare function isGlobPattern(pattern: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Returns whether an artifact generates files under the change's specs/ tree.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isSpecsArtifactPath(generates: string): boolean;
|
|
9
|
+
export declare function resolveArtifactOutputPath(changeDir: string, generates: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Resolves an artifact's output path(s) to concrete files that currently exist.
|
|
12
|
+
* Returns absolute file paths. Glob matches are sorted for deterministic output.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveArtifactOutputs(changeDir: string, generates: string): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Checks if an artifact has at least one resolved output file.
|
|
17
|
+
*/
|
|
18
|
+
export declare function artifactOutputExists(changeDir: string, generates: string): boolean;
|
|
19
|
+
//# sourceMappingURL=outputs.d.ts.map
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { expand } from 'brace-expansion';
|
|
4
|
+
import picomatch from 'picomatch';
|
|
5
|
+
import * as globby from 'tinyglobby';
|
|
6
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
7
|
+
const EXTGLOB_RE = /[!*+?@]\([^(]*\)/u;
|
|
8
|
+
const BRACE_EXPANSION_SEPARATORS_RE = /,|\.\./u;
|
|
9
|
+
const DUPLICATE_SLASH_RE = /(?!^)\/{2,}/gu;
|
|
10
|
+
// Deeply nested braces can overflow a recursive expander. Reject nesting no real
|
|
11
|
+
// artifact pattern needs before brace-expansion or tinyglobby see the pattern.
|
|
12
|
+
const MAX_BRACE_NESTING = 16;
|
|
13
|
+
// Nesting depth alone does not bound the work: `{a,b}` repeated 20 times is
|
|
14
|
+
// only one level deep and expands to 1,048,576 patterns, which exhausts the
|
|
15
|
+
// heap. fast-glob never expanded that far because micromatch compiled the
|
|
16
|
+
// braces to a regex. No real artifact pattern needs more than a few hundred.
|
|
17
|
+
const MAX_BRACE_EXPANSIONS = 1024;
|
|
18
|
+
const ARTIFACT_GLOB_OPTIONS = {
|
|
19
|
+
absolute: true,
|
|
20
|
+
// Braces are expanded first so `{draft}` left behind stays a literal name.
|
|
21
|
+
braceExpansion: false,
|
|
22
|
+
// fast-glob migration: directory patterns must not grow a trailing `/**`.
|
|
23
|
+
expandDirectories: false,
|
|
24
|
+
followSymbolicLinks: true,
|
|
25
|
+
onlyFiles: true,
|
|
26
|
+
};
|
|
27
|
+
function assertBraceNesting(pattern) {
|
|
28
|
+
// Mirror the braces parser: `[...]` and quoted runs are literal, `(` nests like
|
|
29
|
+
// `{`, and a closer only closes the innermost group of its own kind.
|
|
30
|
+
const open = [];
|
|
31
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
32
|
+
const char = pattern[index];
|
|
33
|
+
if (char === '[') {
|
|
34
|
+
for (let brackets = 1; brackets > 0 && index + 1 < pattern.length;) {
|
|
35
|
+
index += 1;
|
|
36
|
+
if (pattern[index] === '[')
|
|
37
|
+
brackets += 1;
|
|
38
|
+
else if (pattern[index] === ']')
|
|
39
|
+
brackets -= 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (char === '"' || char === "'" || char === '`') {
|
|
43
|
+
const end = pattern.indexOf(char, index + 1);
|
|
44
|
+
index = end === -1 ? pattern.length : end;
|
|
45
|
+
}
|
|
46
|
+
else if (char === '{' || char === '(') {
|
|
47
|
+
open.push(char);
|
|
48
|
+
if (open.length > MAX_BRACE_NESTING) {
|
|
49
|
+
// A pattern this deep can run to thousands of characters; quote only its start.
|
|
50
|
+
const shown = pattern.length > 80 ? `${pattern.slice(0, 80)}...` : pattern;
|
|
51
|
+
throw new Error(`Artifact output pattern nests braces more than ${MAX_BRACE_NESTING} levels deep: ${shown}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if ((char === '}' && open.at(-1) === '{') || (char === ')' && open.at(-1) === '(')) {
|
|
55
|
+
open.pop();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function isDynamicSegment(segment) {
|
|
60
|
+
return segment.includes('*') || segment.includes('?') || segment.includes('[')
|
|
61
|
+
|| EXTGLOB_RE.test(segment);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Static directory prefix of an expanded pattern, equivalent to the `base`
|
|
65
|
+
* fast-glob's generateTasks used to reject `../` before walking.
|
|
66
|
+
*/
|
|
67
|
+
function globBase(pattern) {
|
|
68
|
+
const segments = pattern.split('/');
|
|
69
|
+
if (segments.length <= 1)
|
|
70
|
+
return '.';
|
|
71
|
+
const base = [];
|
|
72
|
+
for (const segment of segments.slice(0, -1)) {
|
|
73
|
+
if (segment === '')
|
|
74
|
+
continue;
|
|
75
|
+
if (isDynamicSegment(segment))
|
|
76
|
+
break;
|
|
77
|
+
base.push(segment);
|
|
78
|
+
}
|
|
79
|
+
// An absolute pattern must stay absolute, or `path.resolve(changeDir, base)`
|
|
80
|
+
// rebases `/etc/*` inside the change directory and the containment check
|
|
81
|
+
// passes for a pattern that globs the filesystem root.
|
|
82
|
+
if (pattern.startsWith('/'))
|
|
83
|
+
return `/${base.join('/')}`;
|
|
84
|
+
return base.length > 0 ? base.join('/') : '.';
|
|
85
|
+
}
|
|
86
|
+
// brace-expansion 5 leaves the rest of a pattern literal when a non-expanding
|
|
87
|
+
// group such as `{draft}` comes before a range. Expand one real group at a time.
|
|
88
|
+
// NUL-prefixed so no `generates` value can forge them: `resolveArtifactOutputPath`
|
|
89
|
+
// runs `path.join` first, which rejects a NUL byte. A single-character sentinel
|
|
90
|
+
// such as U+0001 is a legal filename character, and a pattern containing one
|
|
91
|
+
// would come back out of the expander as a brace.
|
|
92
|
+
const LITERAL_BRACE_OPEN = '\u0000<';
|
|
93
|
+
const LITERAL_BRACE_CLOSE = '\u0000>';
|
|
94
|
+
function hasTopLevelExpansion(body) {
|
|
95
|
+
let depth = 0;
|
|
96
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
97
|
+
const char = body[index];
|
|
98
|
+
if (char === '{')
|
|
99
|
+
depth += 1;
|
|
100
|
+
else if (char === '}')
|
|
101
|
+
depth = Math.max(0, depth - 1);
|
|
102
|
+
else if (depth === 0) {
|
|
103
|
+
if (char === ',')
|
|
104
|
+
return true;
|
|
105
|
+
if (char === '.' && body[index + 1] === '.')
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
function findTopLevelExpansion(pattern) {
|
|
112
|
+
const stack = [];
|
|
113
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
114
|
+
const char = pattern[index];
|
|
115
|
+
if (char === '{') {
|
|
116
|
+
stack.push(index);
|
|
117
|
+
}
|
|
118
|
+
else if (char === '}' && stack.length > 0) {
|
|
119
|
+
const open = stack.pop();
|
|
120
|
+
if (open === undefined)
|
|
121
|
+
continue;
|
|
122
|
+
// Only the outermost group that actually expands. brace-expansion expands
|
|
123
|
+
// its nested groups in one linear pass. Peeling the innermost `{a,b}` first
|
|
124
|
+
// turns `{a,{a,{a,b}}}` into a binary tree, so a 16-deep pattern does not
|
|
125
|
+
// finish before the test timeout.
|
|
126
|
+
if (stack.length > 0)
|
|
127
|
+
continue;
|
|
128
|
+
const body = pattern.slice(open + 1, index);
|
|
129
|
+
if (!hasTopLevelExpansion(body)) {
|
|
130
|
+
// `{prefix{a,b}}` has no separator of its own, so the outer braces stay
|
|
131
|
+
// literal. Keep scanning inside them; a nested group may still expand.
|
|
132
|
+
const nested = findTopLevelExpansion(body);
|
|
133
|
+
if (!nested)
|
|
134
|
+
continue;
|
|
135
|
+
return {
|
|
136
|
+
pre: `${pattern.slice(0, open + 1)}${nested.pre}`,
|
|
137
|
+
body: nested.body,
|
|
138
|
+
post: `${nested.post}${pattern.slice(index)}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return { pre: pattern.slice(0, open), body, post: pattern.slice(index + 1) };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function assertExpansionBudget(pattern, count) {
|
|
147
|
+
if (count <= MAX_BRACE_EXPANSIONS)
|
|
148
|
+
return;
|
|
149
|
+
// A pattern this wide can run to thousands of characters; quote only its start.
|
|
150
|
+
const shown = pattern.length > 80 ? `${pattern.slice(0, 80)}...` : pattern;
|
|
151
|
+
throw new Error(`Artifact output pattern expands to more than ${MAX_BRACE_EXPANSIONS} patterns: ${shown}`);
|
|
152
|
+
}
|
|
153
|
+
function expandArtifactBraces(pattern) {
|
|
154
|
+
if (pattern.includes('\u0000')) {
|
|
155
|
+
throw new Error('Artifact output pattern contains a NUL byte');
|
|
156
|
+
}
|
|
157
|
+
const pending = [pattern];
|
|
158
|
+
const finished = [];
|
|
159
|
+
while (pending.length > 0) {
|
|
160
|
+
const current = pending.pop();
|
|
161
|
+
if (current === undefined)
|
|
162
|
+
break;
|
|
163
|
+
const group = findTopLevelExpansion(current);
|
|
164
|
+
if (!group) {
|
|
165
|
+
finished.push(current.replaceAll(LITERAL_BRACE_OPEN, '{').replaceAll(LITERAL_BRACE_CLOSE, '}'));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const wrapped = `{${group.body}}`;
|
|
169
|
+
// `max` makes this budget the binding one. Without it `{1..1000000000}`
|
|
170
|
+
// allocates brace-expansion's own 100,000-result default before the check
|
|
171
|
+
// below rejects it. `max` truncates silently, so ask for one result more
|
|
172
|
+
// than the budget allows: a result that long means the pattern is over
|
|
173
|
+
// budget rather than fully expanded. A truncated group never looks literal,
|
|
174
|
+
// because its first alternative is not the group itself.
|
|
175
|
+
const remaining = Math.max(MAX_BRACE_EXPANSIONS - (pending.length + finished.length), 0);
|
|
176
|
+
const alternatives = expand(wrapped, { max: remaining + 1 });
|
|
177
|
+
if (alternatives.length === 1 && alternatives[0] === wrapped) {
|
|
178
|
+
pending.push(`${group.pre}${LITERAL_BRACE_OPEN}${group.body}${LITERAL_BRACE_CLOSE}${group.post}`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
// Checked before the push so the budget bounds memory, not just the result.
|
|
182
|
+
assertExpansionBudget(pattern, pending.length + finished.length + alternatives.length);
|
|
183
|
+
for (const alternative of alternatives) {
|
|
184
|
+
pending.push(`${group.pre}${alternative}${group.post}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const normalized = finished
|
|
188
|
+
.map((value) => value.replace(DUPLICATE_SLASH_RE, '/'))
|
|
189
|
+
.filter((value) => value.length > 0);
|
|
190
|
+
return normalized.length > 0 ? normalized : [pattern];
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* One directory level. picomatch matches the segment; readdir keeps symlink
|
|
194
|
+
* entries tinyglobby would drop when followSymbolicLinks is false.
|
|
195
|
+
*/
|
|
196
|
+
function listDirectoryMatches(directory, segment) {
|
|
197
|
+
let entries;
|
|
198
|
+
try {
|
|
199
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error.code === 'ENOENT')
|
|
203
|
+
return [];
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
const isMatch = picomatch(segment, {
|
|
207
|
+
dot: false,
|
|
208
|
+
nobrace: true,
|
|
209
|
+
noglobstar: true,
|
|
210
|
+
posix: true,
|
|
211
|
+
});
|
|
212
|
+
const names = [];
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
if (isMatch(entry.name))
|
|
215
|
+
names.push(entry.name);
|
|
216
|
+
}
|
|
217
|
+
return names;
|
|
218
|
+
}
|
|
219
|
+
function hasBraceExpansion(pattern) {
|
|
220
|
+
const openings = [];
|
|
221
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
222
|
+
if (pattern[index] === '{')
|
|
223
|
+
openings.push(index);
|
|
224
|
+
if (pattern[index] !== '}')
|
|
225
|
+
continue;
|
|
226
|
+
const opening = openings.pop();
|
|
227
|
+
if (opening !== undefined && BRACE_EXPANSION_SEPARATORS_RE.test(pattern.slice(opening, index))) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Recognizes artifact globs while preserving literal output filenames.
|
|
235
|
+
*/
|
|
236
|
+
export function isGlobPattern(pattern) {
|
|
237
|
+
// Keep the original wildcard rules and recognize brace expansions and extglobs.
|
|
238
|
+
// Its full dynamic predicate also reinterprets literal !, parentheses, and backslashes.
|
|
239
|
+
const normalized = FileSystemUtils.toPosixPath(pattern);
|
|
240
|
+
return normalized.includes('*') || normalized.includes('?') || normalized.includes('[')
|
|
241
|
+
|| EXTGLOB_RE.test(normalized) || hasBraceExpansion(normalized);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Returns whether an artifact generates files under the change's specs/ tree.
|
|
245
|
+
*/
|
|
246
|
+
export function isSpecsArtifactPath(generates) {
|
|
247
|
+
const normalized = path.posix.normalize(FileSystemUtils.toPosixPath(generates));
|
|
248
|
+
return normalized.startsWith('specs/');
|
|
249
|
+
}
|
|
250
|
+
export function resolveArtifactOutputPath(changeDir, generates) {
|
|
251
|
+
const outputPath = path.join(changeDir, generates);
|
|
252
|
+
FileSystemUtils.assertPathWithin(changeDir, outputPath);
|
|
253
|
+
return outputPath;
|
|
254
|
+
}
|
|
255
|
+
function assertGlobDirectoryTraversal(changeDir, currentDir, directorySegments, segmentIndex = 0, visited = new Set(), canonicalChangeDir = FileSystemUtils.canonicalizeExistingPath(changeDir), ancestors = new Set()) {
|
|
256
|
+
if (segmentIndex >= directorySegments.length)
|
|
257
|
+
return;
|
|
258
|
+
const canonicalDir = FileSystemUtils.canonicalizeExistingPath(currentDir);
|
|
259
|
+
FileSystemUtils.assertPathWithin(canonicalChangeDir, canonicalDir);
|
|
260
|
+
const visitKey = `${canonicalDir}\0${segmentIndex}`;
|
|
261
|
+
if (ancestors.has(visitKey)) {
|
|
262
|
+
throw new Error(`Cannot resolve artifact outputs through a linked directory cycle: ${currentDir}`);
|
|
263
|
+
}
|
|
264
|
+
if (visited.has(visitKey))
|
|
265
|
+
return;
|
|
266
|
+
visited.add(visitKey);
|
|
267
|
+
ancestors.add(visitKey);
|
|
268
|
+
try {
|
|
269
|
+
const segment = directorySegments[segmentIndex];
|
|
270
|
+
if (segment === '**') {
|
|
271
|
+
// `**` may consume no directory at all.
|
|
272
|
+
assertGlobDirectoryTraversal(changeDir, canonicalDir, directorySegments, segmentIndex + 1, visited, canonicalChangeDir, ancestors);
|
|
273
|
+
}
|
|
274
|
+
// `.` and `..` are not directory entries. Resolve them directly so a
|
|
275
|
+
// `**/../` pattern is confined before any glob runs.
|
|
276
|
+
if (segment === '' || segment === '.' || segment === '..') {
|
|
277
|
+
const candidate = path.resolve(canonicalDir, segment === '' ? '.' : segment);
|
|
278
|
+
const canonicalCandidate = FileSystemUtils.canonicalizeExistingPath(candidate);
|
|
279
|
+
FileSystemUtils.assertPathWithin(canonicalChangeDir, canonicalCandidate);
|
|
280
|
+
assertGlobDirectoryTraversal(changeDir, canonicalCandidate, directorySegments, segmentIndex + 1, visited, canonicalChangeDir, ancestors);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const matches = listDirectoryMatches(canonicalDir, segment === '**' ? '*' : segment);
|
|
284
|
+
for (const match of matches) {
|
|
285
|
+
const candidate = path.join(canonicalDir, match);
|
|
286
|
+
try {
|
|
287
|
+
if (!fs.statSync(candidate).isDirectory())
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (error.code === 'ENOENT')
|
|
292
|
+
continue;
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
const canonicalCandidate = FileSystemUtils.canonicalizeExistingPath(candidate);
|
|
296
|
+
FileSystemUtils.assertPathWithin(canonicalChangeDir, canonicalCandidate);
|
|
297
|
+
assertGlobDirectoryTraversal(changeDir, canonicalCandidate, directorySegments, segment === '**' ? segmentIndex : segmentIndex + 1, visited, canonicalChangeDir, ancestors);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
ancestors.delete(visitKey);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Resolves an artifact's output path(s) to concrete files that currently exist.
|
|
306
|
+
* Returns absolute file paths. Glob matches are sorted for deterministic output.
|
|
307
|
+
*/
|
|
308
|
+
export function resolveArtifactOutputs(changeDir, generates) {
|
|
309
|
+
const isGlob = isGlobPattern(generates);
|
|
310
|
+
if (isGlob)
|
|
311
|
+
assertBraceNesting(FileSystemUtils.toPosixPath(generates));
|
|
312
|
+
const outputPath = resolveArtifactOutputPath(changeDir, generates);
|
|
313
|
+
if (!isGlob) {
|
|
314
|
+
try {
|
|
315
|
+
return fs.statSync(outputPath).isFile()
|
|
316
|
+
? [FileSystemUtils.canonicalizeExistingPath(outputPath)]
|
|
317
|
+
: [];
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const normalizedPattern = FileSystemUtils.toPosixPath(generates).replace(DUPLICATE_SLASH_RE, '/');
|
|
324
|
+
// Expand braces before any filesystem access. tinyglobby has no generateTasks,
|
|
325
|
+
// and a `../` alternative is invisible until the braces are expanded.
|
|
326
|
+
const patterns = expandArtifactBraces(normalizedPattern);
|
|
327
|
+
for (const pattern of patterns) {
|
|
328
|
+
FileSystemUtils.assertPathWithin(changeDir, path.resolve(changeDir, globBase(pattern)));
|
|
329
|
+
}
|
|
330
|
+
for (const pattern of patterns) {
|
|
331
|
+
assertGlobDirectoryTraversal(changeDir, changeDir, pattern.split('/').slice(0, -1));
|
|
332
|
+
}
|
|
333
|
+
const matches = globby
|
|
334
|
+
.globSync(patterns, {
|
|
335
|
+
cwd: changeDir,
|
|
336
|
+
...ARTIFACT_GLOB_OPTIONS,
|
|
337
|
+
})
|
|
338
|
+
.map((match) => {
|
|
339
|
+
const normalizedMatch = path.normalize(match);
|
|
340
|
+
FileSystemUtils.assertPathWithin(changeDir, normalizedMatch);
|
|
341
|
+
return FileSystemUtils.canonicalizeExistingPath(normalizedMatch);
|
|
342
|
+
});
|
|
343
|
+
return Array.from(new Set(matches)).sort();
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Checks if an artifact has at least one resolved output file.
|
|
347
|
+
*/
|
|
348
|
+
export function artifactOutputExists(changeDir, generates) {
|
|
349
|
+
return resolveArtifactOutputs(changeDir, generates).length > 0;
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=outputs.js.map
|