@gordon.gan/specflow 1.0.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 +31 -0
- package/README.md +442 -0
- package/bin/specflow.js +2 -0
- package/dist/cli/commands/change-archive.d.ts +26 -0
- package/dist/cli/commands/change-archive.js +50 -0
- package/dist/cli/commands/change-new.d.ts +18 -0
- package/dist/cli/commands/change-new.js +53 -0
- package/dist/cli/commands/change-phase.d.ts +33 -0
- package/dist/cli/commands/change-phase.js +66 -0
- package/dist/cli/commands/change-status.d.ts +35 -0
- package/dist/cli/commands/change-status.js +95 -0
- package/dist/cli/commands/doctor.d.ts +9 -0
- package/dist/cli/commands/doctor.js +48 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +122 -0
- package/dist/cli/commands/instructions.d.ts +21 -0
- package/dist/cli/commands/instructions.js +75 -0
- package/dist/cli/commands/parity-report.d.ts +10 -0
- package/dist/cli/commands/parity-report.js +67 -0
- package/dist/cli/commands/sync.d.ts +4 -0
- package/dist/cli/commands/sync.js +40 -0
- package/dist/cli/commands/validate.d.ts +21 -0
- package/dist/cli/commands/validate.js +46 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +52 -0
- package/dist/core/archive.d.ts +39 -0
- package/dist/core/archive.js +154 -0
- package/dist/core/artifact-graph/graph.d.ts +60 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +10 -0
- package/dist/core/artifact-graph/index.js +7 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +45 -0
- package/dist/core/artifact-graph/instruction-loader.js +37 -0
- package/dist/core/artifact-graph/outputs.d.ts +35 -0
- package/dist/core/artifact-graph/outputs.js +48 -0
- package/dist/core/artifact-graph/resolver.d.ts +54 -0
- package/dist/core/artifact-graph/resolver.js +139 -0
- package/dist/core/artifact-graph/schema.d.ts +23 -0
- package/dist/core/artifact-graph/schema.js +115 -0
- package/dist/core/artifact-graph/state.d.ts +9 -0
- package/dist/core/artifact-graph/state.js +93 -0
- package/dist/core/artifact-graph/types.d.ts +123 -0
- package/dist/core/artifact-graph/types.js +29 -0
- package/dist/core/parsers/change-parser.d.ts +20 -0
- package/dist/core/parsers/change-parser.js +22 -0
- package/dist/core/parsers/index.d.ts +7 -0
- package/dist/core/parsers/index.js +4 -0
- package/dist/core/parsers/markdown-parser.d.ts +35 -0
- package/dist/core/parsers/markdown-parser.js +193 -0
- package/dist/core/parsers/requirement-blocks.d.ts +21 -0
- package/dist/core/parsers/requirement-blocks.js +57 -0
- package/dist/core/parsers/spec-structure.d.ts +22 -0
- package/dist/core/parsers/spec-structure.js +105 -0
- package/dist/core/specs-apply.d.ts +22 -0
- package/dist/core/specs-apply.js +165 -0
- package/dist/core/templates/index.d.ts +31 -0
- package/dist/core/templates/index.js +80 -0
- package/dist/core/templates/types.d.ts +4 -0
- package/dist/core/templates/types.js +1 -0
- package/dist/core/validation/constants.d.ts +14 -0
- package/dist/core/validation/constants.js +14 -0
- package/dist/core/validation/types.d.ts +13 -0
- package/dist/core/validation/types.js +6 -0
- package/dist/core/validation/validator.d.ts +24 -0
- package/dist/core/validation/validator.js +250 -0
- package/dist/integrations/claude/adapter.d.ts +2 -0
- package/dist/integrations/claude/adapter.js +106 -0
- package/dist/integrations/codex/adapter.d.ts +2 -0
- package/dist/integrations/codex/adapter.js +141 -0
- package/dist/integrations/cursor/adapter.d.ts +2 -0
- package/dist/integrations/cursor/adapter.js +126 -0
- package/dist/integrations/shared/adapter-registry.d.ts +4 -0
- package/dist/integrations/shared/adapter-registry.js +27 -0
- package/dist/integrations/shared/asset-copy.d.ts +2 -0
- package/dist/integrations/shared/asset-copy.js +44 -0
- package/dist/integrations/shared/asset-hash.d.ts +4 -0
- package/dist/integrations/shared/asset-hash.js +54 -0
- package/dist/integrations/shared/capability-evidence.d.ts +1 -0
- package/dist/integrations/shared/capability-evidence.js +42 -0
- package/dist/integrations/shared/command-catalog.d.ts +6 -0
- package/dist/integrations/shared/command-catalog.js +13 -0
- package/dist/integrations/shared/managed-assets.d.ts +2 -0
- package/dist/integrations/shared/managed-assets.js +21 -0
- package/dist/integrations/shared/marker-write.d.ts +3 -0
- package/dist/integrations/shared/marker-write.js +48 -0
- package/dist/integrations/shared/migration-state.d.ts +2 -0
- package/dist/integrations/shared/migration-state.js +67 -0
- package/dist/integrations/shared/parity-comparator.d.ts +6 -0
- package/dist/integrations/shared/parity-comparator.js +97 -0
- package/dist/integrations/shared/parity-manifest.d.ts +13 -0
- package/dist/integrations/shared/parity-manifest.js +58 -0
- package/dist/integrations/shared/runtime-assets.d.ts +2 -0
- package/dist/integrations/shared/runtime-assets.js +21 -0
- package/dist/integrations/shared/skill-renderer.d.ts +5 -0
- package/dist/integrations/shared/skill-renderer.js +77 -0
- package/dist/integrations/shared/types.d.ts +35 -0
- package/dist/integrations/shared/types.js +1 -0
- package/dist/utils/change-metadata.d.ts +38 -0
- package/dist/utils/change-metadata.js +82 -0
- package/dist/utils/change-utils.d.ts +53 -0
- package/dist/utils/change-utils.js +97 -0
- package/dist/utils/file-system.d.ts +35 -0
- package/dist/utils/file-system.js +67 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/project-root.d.ts +22 -0
- package/dist/utils/project-root.js +44 -0
- package/dist/utils/task-progress.d.ts +39 -0
- package/dist/utils/task-progress.js +62 -0
- package/package.json +72 -0
- package/prompts/build/ecc-go-reviewer.md +80 -0
- package/prompts/build/ecc-java-reviewer.md +185 -0
- package/prompts/build/ecc-kotlin-reviewer.md +163 -0
- package/prompts/build/ecc-python-reviewer.md +102 -0
- package/prompts/build/ecc-rust-reviewer.md +98 -0
- package/prompts/build/ecc-typescript-reviewer.md +116 -0
- package/prompts/build/language-router.md +22 -0
- package/prompts/build/phase-a-plan.md +288 -0
- package/prompts/build/phase-b-execute.md +276 -0
- package/prompts/build/phase-b-review.md +119 -0
- package/prompts/build/phase-b-worktree.md +215 -0
- package/prompts/build/plan-document-reviewer.md +51 -0
- package/prompts/build/tdd.md +184 -0
- package/prompts/done/branch-finish.md +204 -0
- package/prompts/fix/debug.md +249 -0
- package/prompts/plan/design-draft.md +123 -0
- package/prompts/plan/proposal.md +85 -0
- package/prompts/plan/specs.md +114 -0
- package/prompts/plan/tasks-draft.md +123 -0
- package/prompts/reference/openspec/apply-instruction.md +2 -0
- package/prompts/reference/openspec/design-instruction.md +20 -0
- package/prompts/reference/openspec/proposal-instruction.md +18 -0
- package/prompts/reference/openspec/specs-instruction.md +47 -0
- package/prompts/reference/openspec/tasks-instruction.md +26 -0
- package/prompts/reference/specflow/example-design.md +284 -0
- package/prompts/reference/superpowers/anthropic-best-practices.md +1150 -0
- package/prompts/reference/superpowers/codex-tools.md +29 -0
- package/prompts/reference/superpowers/condition-based-waiting-example.ts +158 -0
- package/prompts/reference/superpowers/condition-based-waiting.md +115 -0
- package/prompts/reference/superpowers/defense-in-depth.md +122 -0
- package/prompts/reference/superpowers/find-polluter.sh +63 -0
- package/prompts/reference/superpowers/graphviz-conventions.dot +172 -0
- package/prompts/reference/superpowers/persuasion-principles.md +187 -0
- package/prompts/reference/superpowers/root-cause-tracing.md +169 -0
- package/prompts/reference/superpowers/testing-anti-patterns.md +299 -0
- package/prompts/reference/superpowers/testing-skills-with-subagents.md +384 -0
- package/prompts/reference/superpowers/using-superpowers.md +117 -0
- package/prompts/reference/superpowers/writing-skills.md +655 -0
- package/prompts/refine/brainstorm.md +241 -0
- package/prompts/refine/design-output.md +126 -0
- package/prompts/refine/spec-document-reviewer.md +51 -0
- package/prompts/refine/update-artifacts.md +185 -0
- package/prompts/review/code-review.md +223 -0
- package/prompts/shared/code-reviewer-prompt.md +98 -0
- package/prompts/shared/dispatching-parallel-agents.md +143 -0
- package/prompts/shared/executing-plans.md +67 -0
- package/prompts/shared/implementer-prompt.md +115 -0
- package/prompts/shared/receiving-code-review.md +174 -0
- package/prompts/shared/spec-reviewer-prompt.md +63 -0
- package/prompts/test/tdd.md +236 -0
- package/prompts/test/verification.md +129 -0
- package/prompts/verify/ecc-security-reviewer.md +112 -0
- package/prompts/verify/verification.md +196 -0
- package/schemas/specflow/schema.yaml +48 -0
- package/skills/specflow-build/SKILL.md +129 -0
- package/skills/specflow-done/SKILL.md +68 -0
- package/skills/specflow-fix/SKILL.md +74 -0
- package/skills/specflow-plan/SKILL.md +82 -0
- package/skills/specflow-refine/SKILL.md +128 -0
- package/skills/specflow-review/SKILL.md +40 -0
- package/skills/specflow-scan/SKILL.md +48 -0
- package/skills/specflow-snap/SKILL.md +46 -0
- package/skills/specflow-test/SKILL.md +48 -0
- package/skills/specflow-verify/SKILL.md +77 -0
- package/templates/design.md +19 -0
- package/templates/proposal.md +23 -0
- package/templates/spec.md +42 -0
- package/templates/tasks.md +9 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { COMMAND_IDS } from './command-catalog.js';
|
|
2
|
+
export const CAPABILITY_MANIFEST = [
|
|
3
|
+
...COMMAND_IDS.map((id) => ({
|
|
4
|
+
id: `command.${id}`,
|
|
5
|
+
commandId: id,
|
|
6
|
+
level: 'command',
|
|
7
|
+
required: true,
|
|
8
|
+
})),
|
|
9
|
+
{ id: 'sub.scan.planned_v03_notice', commandId: 'scan', level: 'sub-capability', required: true },
|
|
10
|
+
{ id: 'sub.plan.first_iteration', commandId: 'plan', level: 'sub-capability', required: true },
|
|
11
|
+
{ id: 'sub.refine.multi_round', commandId: 'refine', level: 'sub-capability', required: true },
|
|
12
|
+
{ id: 'sub.refine.challenge_scope', commandId: 'refine', level: 'sub-capability', required: true },
|
|
13
|
+
{ id: 'sub.build.phase_a', commandId: 'build', level: 'sub-capability', required: true },
|
|
14
|
+
{ id: 'sub.build.phase_b', commandId: 'build', level: 'sub-capability', required: true },
|
|
15
|
+
{ id: 'sub.build.gap_detection', commandId: 'build', level: 'sub-capability', required: true },
|
|
16
|
+
{ id: 'sub.review.spec_regression', commandId: 'review', level: 'sub-capability', required: true },
|
|
17
|
+
{ id: 'sub.test.full_scope', commandId: 'test', level: 'sub-capability', required: true },
|
|
18
|
+
{ id: 'sub.verify.dual_validation', commandId: 'verify', level: 'sub-capability', required: true },
|
|
19
|
+
{ id: 'sub.done.archive_merge', commandId: 'done', level: 'sub-capability', required: true },
|
|
20
|
+
{ id: 'sub.fix.urgent_mode', commandId: 'fix', level: 'sub-capability', required: true },
|
|
21
|
+
{ id: 'sub.snap.posthoc', commandId: 'snap', level: 'sub-capability', required: true },
|
|
22
|
+
{ id: 'failure.scan.not_implemented_guard', commandId: 'scan', level: 'failure-path', required: true },
|
|
23
|
+
{ id: 'failure.build.phase_gate', commandId: 'build', level: 'failure-path', required: true },
|
|
24
|
+
{ id: 'failure.verify.explicit_skip_marker', commandId: 'verify', level: 'failure-path', required: true },
|
|
25
|
+
];
|
|
26
|
+
export const IDE_ASSET_MANIFEST = [
|
|
27
|
+
{
|
|
28
|
+
ide: 'claude',
|
|
29
|
+
requiredAssetKeys: [
|
|
30
|
+
'commandsDir',
|
|
31
|
+
'skillsDir',
|
|
32
|
+
'runtimePromptsDir',
|
|
33
|
+
'runtimeSchemasDir',
|
|
34
|
+
'runtimeTemplatesDir',
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
ide: 'cursor',
|
|
39
|
+
requiredAssetKeys: [
|
|
40
|
+
'commandsDir',
|
|
41
|
+
'skillsDir',
|
|
42
|
+
'rulesFile',
|
|
43
|
+
'runtimePromptsDir',
|
|
44
|
+
'runtimeSchemasDir',
|
|
45
|
+
'runtimeTemplatesDir',
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
ide: 'codex',
|
|
50
|
+
requiredAssetKeys: [
|
|
51
|
+
'skillsDir',
|
|
52
|
+
'agentsFile',
|
|
53
|
+
'runtimePromptsDir',
|
|
54
|
+
'runtimeSchemasDir',
|
|
55
|
+
'runtimeTemplatesDir',
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { copyDirRecursive, copyDirRecursiveRendered } from './asset-copy.js';
|
|
3
|
+
import { renderIdeContent } from './skill-renderer.js';
|
|
4
|
+
function ideRootDir(ide) {
|
|
5
|
+
if (ide === 'claude') {
|
|
6
|
+
return '.claude';
|
|
7
|
+
}
|
|
8
|
+
if (ide === 'cursor') {
|
|
9
|
+
return '.cursor';
|
|
10
|
+
}
|
|
11
|
+
return '.agents';
|
|
12
|
+
}
|
|
13
|
+
export async function copyRuntimeAssets(packageRoot, projectRoot, ide) {
|
|
14
|
+
const root = join(projectRoot, ideRootDir(ide), 'specflow');
|
|
15
|
+
const promptsSrc = join(packageRoot, 'prompts');
|
|
16
|
+
const promptsDest = join(root, 'prompts');
|
|
17
|
+
await copyDirRecursiveRendered(promptsSrc, promptsDest, (content) => renderIdeContent(content, ide));
|
|
18
|
+
await copyDirRecursive(join(packageRoot, 'schemas'), join(root, 'schemas'));
|
|
19
|
+
await copyDirRecursive(join(packageRoot, 'templates'), join(root, 'templates'));
|
|
20
|
+
return [`${ideRootDir(ide)}/specflow/prompts`, `${ideRootDir(ide)}/specflow/schemas`, `${ideRootDir(ide)}/specflow/templates`];
|
|
21
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IdeTarget } from './types.js';
|
|
2
|
+
export declare function renderIdeContent(content: string, ide: IdeTarget): string;
|
|
3
|
+
/** @deprecated Use renderIdeContent */
|
|
4
|
+
export declare const renderSkillContent: typeof renderIdeContent;
|
|
5
|
+
export declare function validateSkillFrontmatter(content: string, skillLabel: string): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { COMMAND_CATALOG } from './command-catalog.js';
|
|
2
|
+
function runtimePrefix(ide) {
|
|
3
|
+
if (ide === 'claude') {
|
|
4
|
+
return '.claude/specflow/';
|
|
5
|
+
}
|
|
6
|
+
if (ide === 'cursor') {
|
|
7
|
+
return '.cursor/specflow/';
|
|
8
|
+
}
|
|
9
|
+
return '.agents/specflow/';
|
|
10
|
+
}
|
|
11
|
+
function replaceRuntimePaths(content, ide) {
|
|
12
|
+
const prefix = runtimePrefix(ide);
|
|
13
|
+
return content
|
|
14
|
+
.replace(/\.claude\/specflow\//g, prefix)
|
|
15
|
+
.replace(/\.cursor\/specflow\//g, prefix)
|
|
16
|
+
.replace(/\.agents\/specflow\//g, prefix);
|
|
17
|
+
}
|
|
18
|
+
function replaceInvocationSyntax(content, ide) {
|
|
19
|
+
if (ide !== 'codex') {
|
|
20
|
+
return content;
|
|
21
|
+
}
|
|
22
|
+
let result = content;
|
|
23
|
+
for (const command of COMMAND_CATALOG) {
|
|
24
|
+
const slash = `/specflow:${command.id}`;
|
|
25
|
+
const bare = `specflow:${command.id}`;
|
|
26
|
+
const dollar = `$specflow-${command.id}`;
|
|
27
|
+
result = result.replaceAll(slash, dollar);
|
|
28
|
+
result = result.replace(new RegExp(`\\b${bare.replace(':', '\\:')}\\b`, 'g'), dollar);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function replaceScanInitHint(content, ide) {
|
|
33
|
+
const fullHint = 'creates `specflow/` and IDE assets under `.cursor/`, `.claude/`, and/or `.agents/`.';
|
|
34
|
+
if (ide === 'claude') {
|
|
35
|
+
return content.replace(fullHint, 'creates `specflow/` and IDE assets under `.claude/`.');
|
|
36
|
+
}
|
|
37
|
+
if (ide === 'cursor') {
|
|
38
|
+
return content.replace(fullHint, 'creates `specflow/` and IDE assets under `.cursor/`.');
|
|
39
|
+
}
|
|
40
|
+
return content.replace(fullHint, 'creates `specflow/` and IDE assets under `.agents/`.');
|
|
41
|
+
}
|
|
42
|
+
function replaceScanDispatchPaths(content, ide) {
|
|
43
|
+
const prefix = runtimePrefix(ide);
|
|
44
|
+
const dispatchPath = `${prefix}prompts/shared/dispatching-parallel-agents.md`;
|
|
45
|
+
const label = ide === 'codex' ? 'Codex' : ide === 'cursor' ? 'Cursor' : 'Claude';
|
|
46
|
+
const block = `- ${label}: \`${dispatchPath}\``;
|
|
47
|
+
return content.replace(/- Cursor: `\.cursor\/specflow\/prompts\/shared\/dispatching-parallel-agents\.md`\n- Claude: `\.claude\/specflow\/prompts\/shared\/dispatching-parallel-agents\.md`(?:\n- Codex: `\.agents\/specflow\/prompts\/shared\/dispatching-parallel-agents\.md`)?/, block);
|
|
48
|
+
}
|
|
49
|
+
function replaceToolDocReferences(content) {
|
|
50
|
+
return content
|
|
51
|
+
.replace('`references/copilot-tools.md`', '`reference/superpowers/copilot-tools.md`')
|
|
52
|
+
.replace('`references/codex-tools.md`', '`reference/superpowers/codex-tools.md`');
|
|
53
|
+
}
|
|
54
|
+
export function renderIdeContent(content, ide) {
|
|
55
|
+
let result = content;
|
|
56
|
+
result = replaceRuntimePaths(result, ide);
|
|
57
|
+
result = replaceInvocationSyntax(result, ide);
|
|
58
|
+
result = replaceScanInitHint(result, ide);
|
|
59
|
+
result = replaceScanDispatchPaths(result, ide);
|
|
60
|
+
result = replaceToolDocReferences(result);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
/** @deprecated Use renderIdeContent */
|
|
64
|
+
export const renderSkillContent = renderIdeContent;
|
|
65
|
+
export function validateSkillFrontmatter(content, skillLabel) {
|
|
66
|
+
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
67
|
+
if (!frontmatterMatch) {
|
|
68
|
+
throw new Error(`${skillLabel} missing YAML frontmatter`);
|
|
69
|
+
}
|
|
70
|
+
const frontmatter = frontmatterMatch[1];
|
|
71
|
+
if (!/^name:\s*.+/m.test(frontmatter)) {
|
|
72
|
+
throw new Error(`${skillLabel} missing frontmatter name`);
|
|
73
|
+
}
|
|
74
|
+
if (!/^description:\s*.+/m.test(frontmatter)) {
|
|
75
|
+
throw new Error(`${skillLabel} missing frontmatter description`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type IdeTarget = 'claude' | 'cursor' | 'codex';
|
|
2
|
+
export type InitIdeTarget = IdeTarget | 'both' | 'all';
|
|
3
|
+
export interface AssetState {
|
|
4
|
+
readonly exists: boolean;
|
|
5
|
+
readonly hash?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CapabilityReport {
|
|
8
|
+
readonly ide: IdeTarget;
|
|
9
|
+
readonly supportedCommandIds: readonly string[];
|
|
10
|
+
readonly supportedCapabilityIds: readonly string[];
|
|
11
|
+
readonly assets: Readonly<Record<string, AssetState>>;
|
|
12
|
+
}
|
|
13
|
+
export interface ParityDelta {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly severity: 'P0' | 'P1';
|
|
16
|
+
readonly message: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ParityStatus {
|
|
19
|
+
readonly ok: boolean;
|
|
20
|
+
readonly deltas: readonly ParityDelta[];
|
|
21
|
+
}
|
|
22
|
+
export interface GenerateContext {
|
|
23
|
+
readonly projectRoot: string;
|
|
24
|
+
readonly packageRoot: string;
|
|
25
|
+
readonly forceAssets?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface GenerateResult {
|
|
28
|
+
readonly ide: IdeTarget;
|
|
29
|
+
readonly generatedPaths: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
export interface Adapter {
|
|
32
|
+
readonly ide: IdeTarget;
|
|
33
|
+
generate(ctx: GenerateContext): Promise<GenerateResult>;
|
|
34
|
+
inspect(projectRoot: string): Promise<CapabilityReport>;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All valid lifecycle phases a change can be in.
|
|
3
|
+
*
|
|
4
|
+
* - `plan`: Initial phase, created by `specflow change new`.
|
|
5
|
+
* - `refined`: After `/specflow:refine` iterative deep review completes.
|
|
6
|
+
* - `built`: After `/specflow:build` Phase B execution finishes.
|
|
7
|
+
* - `archived`: Post-archive state; assigned automatically on successful archive.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CHANGE_PHASES: readonly ["plan", "refined", "built", "archived"];
|
|
10
|
+
/**
|
|
11
|
+
* Union type of valid lifecycle phases.
|
|
12
|
+
*/
|
|
13
|
+
export type ChangePhase = (typeof CHANGE_PHASES)[number];
|
|
14
|
+
/**
|
|
15
|
+
* Metadata stored in each change's .specflow.yaml file.
|
|
16
|
+
*/
|
|
17
|
+
export interface ChangeMetadata {
|
|
18
|
+
readonly schema: string;
|
|
19
|
+
readonly created: string;
|
|
20
|
+
readonly phase?: ChangePhase;
|
|
21
|
+
readonly [key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Reads change metadata from .specflow.yaml in the change directory.
|
|
25
|
+
*
|
|
26
|
+
* @param changeDir - Absolute path to the change directory
|
|
27
|
+
* @returns The parsed metadata, or null if the file does not exist
|
|
28
|
+
* @throws When the file exists but cannot be read or parsed
|
|
29
|
+
*/
|
|
30
|
+
export declare function readChangeMetadata(changeDir: string): Promise<ChangeMetadata | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Writes change metadata to .specflow.yaml in the change directory.
|
|
33
|
+
* Creates parent directories if they do not exist.
|
|
34
|
+
*
|
|
35
|
+
* @param changeDir - Absolute path to the change directory
|
|
36
|
+
* @param metadata - The metadata to write
|
|
37
|
+
*/
|
|
38
|
+
export declare function writeChangeMetadata(changeDir: string, metadata: ChangeMetadata): Promise<void>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
const METADATA_FILENAME = '.specflow.yaml';
|
|
6
|
+
/**
|
|
7
|
+
* All valid lifecycle phases a change can be in.
|
|
8
|
+
*
|
|
9
|
+
* - `plan`: Initial phase, created by `specflow change new`.
|
|
10
|
+
* - `refined`: After `/specflow:refine` iterative deep review completes.
|
|
11
|
+
* - `built`: After `/specflow:build` Phase B execution finishes.
|
|
12
|
+
* - `archived`: Post-archive state; assigned automatically on successful archive.
|
|
13
|
+
*/
|
|
14
|
+
export const CHANGE_PHASES = ['plan', 'refined', 'built', 'archived'];
|
|
15
|
+
/**
|
|
16
|
+
* Zod schema for validating change metadata.
|
|
17
|
+
*
|
|
18
|
+
* Uses `passthrough` so that unknown fields (e.g. user-added comments) survive
|
|
19
|
+
* round-tripping rather than being stripped silently.
|
|
20
|
+
*/
|
|
21
|
+
const changeMetadataSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
schema: z.string(),
|
|
24
|
+
created: z.string(),
|
|
25
|
+
phase: z.enum(CHANGE_PHASES).optional(),
|
|
26
|
+
})
|
|
27
|
+
.passthrough();
|
|
28
|
+
/**
|
|
29
|
+
* Formats a Zod error into a user-friendly message.
|
|
30
|
+
*
|
|
31
|
+
* - Invalid phase values get a canonical enum list.
|
|
32
|
+
* - Missing schema/created fields get the legacy "Invalid metadata format" message.
|
|
33
|
+
*/
|
|
34
|
+
function formatZodError(error, metaPath) {
|
|
35
|
+
const hasInvalidPhase = error.issues.some((issue) => issue.path[0] === 'phase' && issue.code === 'invalid_enum_value');
|
|
36
|
+
if (hasInvalidPhase) {
|
|
37
|
+
return new Error('Invalid phase in metadata: expected one of plan|refined|built|archived');
|
|
38
|
+
}
|
|
39
|
+
return new Error(`Invalid metadata format in ${metaPath}: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Reads change metadata from .specflow.yaml in the change directory.
|
|
43
|
+
*
|
|
44
|
+
* @param changeDir - Absolute path to the change directory
|
|
45
|
+
* @returns The parsed metadata, or null if the file does not exist
|
|
46
|
+
* @throws When the file exists but cannot be read or parsed
|
|
47
|
+
*/
|
|
48
|
+
export async function readChangeMetadata(changeDir) {
|
|
49
|
+
const metaPath = join(changeDir, METADATA_FILENAME);
|
|
50
|
+
let content;
|
|
51
|
+
try {
|
|
52
|
+
content = await fs.readFile(metaPath, 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
const parsed = yaml.load(content);
|
|
61
|
+
if (parsed === null || parsed === undefined || typeof parsed !== 'object') {
|
|
62
|
+
throw new Error(`Invalid metadata format in ${metaPath}`);
|
|
63
|
+
}
|
|
64
|
+
const result = changeMetadataSchema.safeParse(parsed);
|
|
65
|
+
if (!result.success) {
|
|
66
|
+
throw formatZodError(result.error, metaPath);
|
|
67
|
+
}
|
|
68
|
+
return result.data;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Writes change metadata to .specflow.yaml in the change directory.
|
|
72
|
+
* Creates parent directories if they do not exist.
|
|
73
|
+
*
|
|
74
|
+
* @param changeDir - Absolute path to the change directory
|
|
75
|
+
* @param metadata - The metadata to write
|
|
76
|
+
*/
|
|
77
|
+
export async function writeChangeMetadata(changeDir, metadata) {
|
|
78
|
+
await fs.mkdir(changeDir, { recursive: true });
|
|
79
|
+
const metaPath = join(changeDir, METADATA_FILENAME);
|
|
80
|
+
const content = yaml.dump(metadata);
|
|
81
|
+
await fs.writeFile(metaPath, content, 'utf-8');
|
|
82
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ChangeMetadata, ChangePhase } from './change-metadata.js';
|
|
2
|
+
export { CHANGE_PHASES } from './change-metadata.js';
|
|
3
|
+
export type { ChangePhase } from './change-metadata.js';
|
|
4
|
+
/**
|
|
5
|
+
* Validates that a change name uses only lowercase alphanumeric characters
|
|
6
|
+
* separated by single hyphens. Prevents path traversal and invalid directory names.
|
|
7
|
+
*
|
|
8
|
+
* @param name - The change name to validate
|
|
9
|
+
* @throws When the name does not match the required pattern
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateChangeName(name: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the relative path for a change directory (with trailing slash).
|
|
14
|
+
*
|
|
15
|
+
* @param changeName - Name of the change (kebab-case)
|
|
16
|
+
* @returns The relative path, e.g. "specflow/changes/my-feature/"
|
|
17
|
+
*/
|
|
18
|
+
export declare function getChangeDir(changeName: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Reads the .specflow.yaml metadata for a given change.
|
|
21
|
+
*
|
|
22
|
+
* @param changeName - Name of the change
|
|
23
|
+
* @param projectRoot - Absolute path to the project root
|
|
24
|
+
* @returns The parsed metadata, or null if the file does not exist
|
|
25
|
+
*/
|
|
26
|
+
export declare function getChangeMetadata(changeName: string, projectRoot: string): Promise<ChangeMetadata | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Writes .specflow.yaml metadata for a given change.
|
|
29
|
+
* Creates the change directory and parent directories if needed.
|
|
30
|
+
*
|
|
31
|
+
* @param changeName - Name of the change
|
|
32
|
+
* @param metadata - The metadata to write
|
|
33
|
+
* @param projectRoot - Absolute path to the project root
|
|
34
|
+
*/
|
|
35
|
+
export declare function writeChangeMetadata(changeName: string, metadata: ChangeMetadata, projectRoot: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Updates the `phase` field of an existing change's metadata, preserving all
|
|
38
|
+
* other fields. Throws if the change does not exist.
|
|
39
|
+
*
|
|
40
|
+
* @param changeName - Name of the change
|
|
41
|
+
* @param phase - The new phase value
|
|
42
|
+
* @param projectRoot - Absolute path to the project root (or archive root)
|
|
43
|
+
* @throws When the change does not exist or the name is invalid
|
|
44
|
+
*/
|
|
45
|
+
export declare function updatePhase(changeName: string, phase: ChangePhase, projectRoot: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Lists all change directory names under specflow/changes/.
|
|
48
|
+
* Returns an empty array if the directory does not exist or is empty.
|
|
49
|
+
*
|
|
50
|
+
* @param projectRoot - Absolute path to the project root
|
|
51
|
+
* @returns Array of change directory names
|
|
52
|
+
*/
|
|
53
|
+
export declare function listChanges(projectRoot: string): Promise<string[]>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readChangeMetadata, writeChangeMetadata as writeMetaInternal } from './change-metadata.js';
|
|
4
|
+
export { CHANGE_PHASES } from './change-metadata.js';
|
|
5
|
+
const CHANGES_REL_PATH = 'specflow/changes';
|
|
6
|
+
const VALID_CHANGE_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
7
|
+
/**
|
|
8
|
+
* Validates that a change name uses only lowercase alphanumeric characters
|
|
9
|
+
* separated by single hyphens. Prevents path traversal and invalid directory names.
|
|
10
|
+
*
|
|
11
|
+
* @param name - The change name to validate
|
|
12
|
+
* @throws When the name does not match the required pattern
|
|
13
|
+
*/
|
|
14
|
+
export function validateChangeName(name) {
|
|
15
|
+
if (!VALID_CHANGE_NAME.test(name)) {
|
|
16
|
+
throw new Error(`Invalid change name "${name}". Use lowercase alphanumeric with hyphens (e.g., "my-feature").`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Returns the relative path for a change directory (with trailing slash).
|
|
21
|
+
*
|
|
22
|
+
* @param changeName - Name of the change (kebab-case)
|
|
23
|
+
* @returns The relative path, e.g. "specflow/changes/my-feature/"
|
|
24
|
+
*/
|
|
25
|
+
export function getChangeDir(changeName) {
|
|
26
|
+
validateChangeName(changeName);
|
|
27
|
+
return `${CHANGES_REL_PATH}/${changeName}/`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reads the .specflow.yaml metadata for a given change.
|
|
31
|
+
*
|
|
32
|
+
* @param changeName - Name of the change
|
|
33
|
+
* @param projectRoot - Absolute path to the project root
|
|
34
|
+
* @returns The parsed metadata, or null if the file does not exist
|
|
35
|
+
*/
|
|
36
|
+
export async function getChangeMetadata(changeName, projectRoot) {
|
|
37
|
+
validateChangeName(changeName);
|
|
38
|
+
const changeDir = join(projectRoot, CHANGES_REL_PATH, changeName);
|
|
39
|
+
return await readChangeMetadata(changeDir);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Writes .specflow.yaml metadata for a given change.
|
|
43
|
+
* Creates the change directory and parent directories if needed.
|
|
44
|
+
*
|
|
45
|
+
* @param changeName - Name of the change
|
|
46
|
+
* @param metadata - The metadata to write
|
|
47
|
+
* @param projectRoot - Absolute path to the project root
|
|
48
|
+
*/
|
|
49
|
+
export async function writeChangeMetadata(changeName, metadata, projectRoot) {
|
|
50
|
+
validateChangeName(changeName);
|
|
51
|
+
const changeDir = join(projectRoot, CHANGES_REL_PATH, changeName);
|
|
52
|
+
await writeMetaInternal(changeDir, metadata);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Updates the `phase` field of an existing change's metadata, preserving all
|
|
56
|
+
* other fields. Throws if the change does not exist.
|
|
57
|
+
*
|
|
58
|
+
* @param changeName - Name of the change
|
|
59
|
+
* @param phase - The new phase value
|
|
60
|
+
* @param projectRoot - Absolute path to the project root (or archive root)
|
|
61
|
+
* @throws When the change does not exist or the name is invalid
|
|
62
|
+
*/
|
|
63
|
+
export async function updatePhase(changeName, phase, projectRoot) {
|
|
64
|
+
validateChangeName(changeName);
|
|
65
|
+
const changeDir = join(projectRoot, CHANGES_REL_PATH, changeName);
|
|
66
|
+
const existing = await readChangeMetadata(changeDir);
|
|
67
|
+
if (existing === null) {
|
|
68
|
+
throw new Error(`Change "${changeName}" not found at ${changeDir}`);
|
|
69
|
+
}
|
|
70
|
+
const updated = {
|
|
71
|
+
...existing,
|
|
72
|
+
phase,
|
|
73
|
+
};
|
|
74
|
+
await writeMetaInternal(changeDir, updated);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Lists all change directory names under specflow/changes/.
|
|
78
|
+
* Returns an empty array if the directory does not exist or is empty.
|
|
79
|
+
*
|
|
80
|
+
* @param projectRoot - Absolute path to the project root
|
|
81
|
+
* @returns Array of change directory names
|
|
82
|
+
*/
|
|
83
|
+
export async function listChanges(projectRoot) {
|
|
84
|
+
const changesDir = join(projectRoot, CHANGES_REL_PATH);
|
|
85
|
+
try {
|
|
86
|
+
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
87
|
+
return entries
|
|
88
|
+
.filter((entry) => entry.isDirectory())
|
|
89
|
+
.map((entry) => entry.name);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks whether a directory exists at the given path.
|
|
3
|
+
*
|
|
4
|
+
* @param dirPath - Absolute path to check
|
|
5
|
+
* @returns true if the path is an existing directory, false otherwise
|
|
6
|
+
*/
|
|
7
|
+
export declare function directoryExists(dirPath: string): Promise<boolean>;
|
|
8
|
+
/**
|
|
9
|
+
* Checks whether a file (not a directory) exists at the given path.
|
|
10
|
+
*
|
|
11
|
+
* @param filePath - Absolute path to check
|
|
12
|
+
* @returns true if the path is an existing file, false otherwise
|
|
13
|
+
*/
|
|
14
|
+
export declare function fileExists(filePath: string): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* Reads a file and returns its contents as a UTF-8 string.
|
|
17
|
+
*
|
|
18
|
+
* @param filePath - Absolute path to the file
|
|
19
|
+
* @returns The file contents
|
|
20
|
+
* @throws When the file does not exist or cannot be read
|
|
21
|
+
*/
|
|
22
|
+
export declare function readFile(filePath: string): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Writes content to a file, auto-creating parent directories as needed.
|
|
25
|
+
*
|
|
26
|
+
* @param filePath - Absolute path where the file should be written
|
|
27
|
+
* @param content - The string content to write
|
|
28
|
+
*/
|
|
29
|
+
export declare function writeFile(filePath: string, content: string): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Ensures a directory exists, creating it and all parent directories as needed.
|
|
32
|
+
*
|
|
33
|
+
* @param dirPath - Absolute path to the directory
|
|
34
|
+
*/
|
|
35
|
+
export declare function ensureDir(dirPath: string): Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Checks whether a directory exists at the given path.
|
|
5
|
+
*
|
|
6
|
+
* @param dirPath - Absolute path to check
|
|
7
|
+
* @returns true if the path is an existing directory, false otherwise
|
|
8
|
+
*/
|
|
9
|
+
export async function directoryExists(dirPath) {
|
|
10
|
+
try {
|
|
11
|
+
const stats = await fs.stat(dirPath);
|
|
12
|
+
return stats.isDirectory();
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Checks whether a file (not a directory) exists at the given path.
|
|
23
|
+
*
|
|
24
|
+
* @param filePath - Absolute path to check
|
|
25
|
+
* @returns true if the path is an existing file, false otherwise
|
|
26
|
+
*/
|
|
27
|
+
export async function fileExists(filePath) {
|
|
28
|
+
try {
|
|
29
|
+
const stats = await fs.stat(filePath);
|
|
30
|
+
return stats.isFile();
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Reads a file and returns its contents as a UTF-8 string.
|
|
41
|
+
*
|
|
42
|
+
* @param filePath - Absolute path to the file
|
|
43
|
+
* @returns The file contents
|
|
44
|
+
* @throws When the file does not exist or cannot be read
|
|
45
|
+
*/
|
|
46
|
+
export async function readFile(filePath) {
|
|
47
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Writes content to a file, auto-creating parent directories as needed.
|
|
51
|
+
*
|
|
52
|
+
* @param filePath - Absolute path where the file should be written
|
|
53
|
+
* @param content - The string content to write
|
|
54
|
+
*/
|
|
55
|
+
export async function writeFile(filePath, content) {
|
|
56
|
+
const dir = dirname(filePath);
|
|
57
|
+
await fs.mkdir(dir, { recursive: true });
|
|
58
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Ensures a directory exists, creating it and all parent directories as needed.
|
|
62
|
+
*
|
|
63
|
+
* @param dirPath - Absolute path to the directory
|
|
64
|
+
*/
|
|
65
|
+
export async function ensureDir(dirPath) {
|
|
66
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
67
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { directoryExists, fileExists, readFile, writeFile, ensureDir, } from './file-system.js';
|
|
2
|
+
export { readChangeMetadata, writeChangeMetadata as writeChangeMetadataToDir, CHANGE_PHASES, } from './change-metadata.js';
|
|
3
|
+
export type { ChangeMetadata, ChangePhase } from './change-metadata.js';
|
|
4
|
+
export { getChangeDir, getChangeMetadata, writeChangeMetadata, listChanges, updatePhase, } from './change-utils.js';
|
|
5
|
+
export { parseTask, toggleTask, countTasks, } from './task-progress.js';
|
|
6
|
+
export type { ParsedTask, TaskCount } from './task-progress.js';
|
|
7
|
+
export { findProjectRoot, requireProjectRoot, } from './project-root.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { directoryExists, fileExists, readFile, writeFile, ensureDir, } from './file-system.js';
|
|
2
|
+
export { readChangeMetadata, writeChangeMetadata as writeChangeMetadataToDir, CHANGE_PHASES, } from './change-metadata.js';
|
|
3
|
+
export { getChangeDir, getChangeMetadata, writeChangeMetadata, listChanges, updatePhase, } from './change-utils.js';
|
|
4
|
+
export { parseTask, toggleTask, countTasks, } from './task-progress.js';
|
|
5
|
+
export { findProjectRoot, requireProjectRoot, } from './project-root.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project root detection utility.
|
|
3
|
+
*
|
|
4
|
+
* Walks upward from a starting directory looking for `specflow/config.yaml`,
|
|
5
|
+
* similar to how `git` finds `.git/`. This lets the CLI work from any
|
|
6
|
+
* subdirectory within a specflow-initialized project.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Find the specflow project root by walking up from a starting directory.
|
|
10
|
+
*
|
|
11
|
+
* @param startDir - Directory to start searching from (defaults to cwd)
|
|
12
|
+
* @returns Absolute path to the project root, or null if not found
|
|
13
|
+
*/
|
|
14
|
+
export declare function findProjectRoot(startDir?: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve project root with a clear error if not found.
|
|
17
|
+
*
|
|
18
|
+
* @param startDir - Directory to start searching from (defaults to cwd)
|
|
19
|
+
* @returns Absolute path to the project root
|
|
20
|
+
* @throws Error with actionable message if no specflow project is detected
|
|
21
|
+
*/
|
|
22
|
+
export declare function requireProjectRoot(startDir?: string): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project root detection utility.
|
|
3
|
+
*
|
|
4
|
+
* Walks upward from a starting directory looking for `specflow/config.yaml`,
|
|
5
|
+
* similar to how `git` finds `.git/`. This lets the CLI work from any
|
|
6
|
+
* subdirectory within a specflow-initialized project.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
const CONFIG_MARKER = join('specflow', 'config.yaml');
|
|
11
|
+
/**
|
|
12
|
+
* Find the specflow project root by walking up from a starting directory.
|
|
13
|
+
*
|
|
14
|
+
* @param startDir - Directory to start searching from (defaults to cwd)
|
|
15
|
+
* @returns Absolute path to the project root, or null if not found
|
|
16
|
+
*/
|
|
17
|
+
export function findProjectRoot(startDir = process.cwd()) {
|
|
18
|
+
let current = resolve(startDir);
|
|
19
|
+
while (true) {
|
|
20
|
+
if (existsSync(join(current, CONFIG_MARKER))) {
|
|
21
|
+
return current;
|
|
22
|
+
}
|
|
23
|
+
const parent = dirname(current);
|
|
24
|
+
if (parent === current) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
current = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve project root with a clear error if not found.
|
|
32
|
+
*
|
|
33
|
+
* @param startDir - Directory to start searching from (defaults to cwd)
|
|
34
|
+
* @returns Absolute path to the project root
|
|
35
|
+
* @throws Error with actionable message if no specflow project is detected
|
|
36
|
+
*/
|
|
37
|
+
export function requireProjectRoot(startDir = process.cwd()) {
|
|
38
|
+
const root = findProjectRoot(startDir);
|
|
39
|
+
if (root === null) {
|
|
40
|
+
throw new Error(`No specflow project found at or above ${startDir}. ` +
|
|
41
|
+
`Run \`specflow init\` in your project root to initialize one.`);
|
|
42
|
+
}
|
|
43
|
+
return root;
|
|
44
|
+
}
|