@caiqueoak/flow 0.7.0 → 0.8.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/README.md +65 -43
- package/dist/artifacts/backlog.d.mts +16 -0
- package/dist/artifacts/backlog.d.mts.map +1 -0
- package/dist/artifacts/backlog.mjs +193 -0
- package/dist/artifacts/backlog.mjs.map +1 -0
- package/dist/artifacts/document.d.mts +6 -0
- package/dist/artifacts/document.d.mts.map +1 -0
- package/dist/artifacts/document.mjs +39 -0
- package/dist/artifacts/document.mjs.map +1 -0
- package/dist/artifacts/engineering.d.mts +5 -0
- package/dist/artifacts/engineering.d.mts.map +1 -0
- package/dist/artifacts/engineering.mjs +27 -0
- package/dist/artifacts/engineering.mjs.map +1 -0
- package/dist/artifacts/gates.d.mts +7 -0
- package/dist/artifacts/gates.d.mts.map +1 -0
- package/dist/artifacts/gates.mjs +42 -0
- package/dist/artifacts/gates.mjs.map +1 -0
- package/dist/artifacts/implementation-plan.d.mts +11 -0
- package/dist/artifacts/implementation-plan.d.mts.map +1 -0
- package/dist/artifacts/implementation-plan.mjs +30 -0
- package/dist/artifacts/implementation-plan.mjs.map +1 -0
- package/dist/artifacts/prd.d.mts +4 -0
- package/dist/artifacts/prd.d.mts.map +1 -0
- package/dist/artifacts/prd.mjs +13 -0
- package/dist/artifacts/prd.mjs.map +1 -0
- package/dist/artifacts/review.d.mts +11 -0
- package/dist/artifacts/review.d.mts.map +1 -0
- package/dist/artifacts/review.mjs +27 -0
- package/dist/artifacts/review.mjs.map +1 -0
- package/dist/artifacts/spec.d.mts +14 -0
- package/dist/artifacts/spec.d.mts.map +1 -0
- package/dist/artifacts/spec.mjs +50 -0
- package/dist/artifacts/spec.mjs.map +1 -0
- package/dist/artifacts/state.d.mts +44 -0
- package/dist/artifacts/state.d.mts.map +1 -0
- package/dist/artifacts/state.mjs +60 -0
- package/dist/artifacts/state.mjs.map +1 -0
- package/dist/artifacts/tasks.d.mts +10 -0
- package/dist/artifacts/tasks.d.mts.map +1 -0
- package/dist/artifacts/tasks.mjs +107 -0
- package/dist/artifacts/tasks.mjs.map +1 -0
- package/dist/artifacts/work-items.d.mts +21 -0
- package/dist/artifacts/work-items.d.mts.map +1 -0
- package/dist/artifacts/work-items.mjs +81 -0
- package/dist/artifacts/work-items.mjs.map +1 -0
- package/dist/cli/command-registry.d.mts +498 -0
- package/dist/cli/command-registry.d.mts.map +1 -0
- package/dist/cli/command-registry.mjs +230 -0
- package/dist/cli/command-registry.mjs.map +1 -0
- package/dist/cli.d.mts +3 -0
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +48 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/commands/doctor.d.mts +21 -0
- package/dist/commands/doctor.d.mts.map +1 -0
- package/dist/commands/doctor.mjs +79 -0
- package/dist/commands/doctor.mjs.map +1 -0
- package/dist/commands/gates.d.mts +14 -0
- package/dist/commands/gates.d.mts.map +1 -0
- package/dist/commands/gates.mjs +136 -0
- package/dist/commands/gates.mjs.map +1 -0
- package/dist/commands/graph.d.mts +11 -0
- package/dist/commands/graph.d.mts.map +1 -0
- package/dist/commands/graph.mjs +78 -0
- package/dist/commands/graph.mjs.map +1 -0
- package/dist/commands/init.d.mts +6 -0
- package/dist/commands/init.d.mts.map +1 -0
- package/dist/commands/init.mjs +126 -0
- package/dist/commands/init.mjs.map +1 -0
- package/dist/commands/migrate.d.mts +27 -0
- package/dist/commands/migrate.d.mts.map +1 -0
- package/dist/commands/migrate.mjs +379 -0
- package/dist/commands/migrate.mjs.map +1 -0
- package/dist/commands/operations.d.mts +13 -0
- package/dist/commands/operations.d.mts.map +1 -0
- package/dist/commands/operations.mjs +340 -0
- package/dist/commands/operations.mjs.map +1 -0
- package/dist/commands/route.d.mts +12 -0
- package/dist/commands/route.d.mts.map +1 -0
- package/dist/commands/route.mjs +45 -0
- package/dist/commands/route.mjs.map +1 -0
- package/dist/commands/schemas.d.mts +5 -0
- package/dist/commands/schemas.d.mts.map +1 -0
- package/dist/commands/schemas.mjs +14 -0
- package/dist/commands/schemas.mjs.map +1 -0
- package/dist/commands/status.d.mts +4 -0
- package/dist/commands/status.d.mts.map +1 -0
- package/dist/commands/status.mjs +18 -0
- package/dist/commands/status.mjs.map +1 -0
- package/dist/commands/sync.d.mts +18 -0
- package/dist/commands/sync.d.mts.map +1 -0
- package/dist/commands/sync.mjs +29 -0
- package/dist/commands/sync.mjs.map +1 -0
- package/dist/commands/trace.d.mts +30 -0
- package/dist/commands/trace.d.mts.map +1 -0
- package/dist/commands/trace.mjs +111 -0
- package/dist/commands/trace.mjs.map +1 -0
- package/dist/commands/validate.d.mts +8 -0
- package/dist/commands/validate.d.mts.map +1 -0
- package/dist/commands/validate.mjs +82 -0
- package/dist/commands/validate.mjs.map +1 -0
- package/dist/domain/contracts.d.mts +203 -0
- package/dist/domain/contracts.d.mts.map +1 -0
- package/dist/domain/contracts.mjs +126 -0
- package/dist/domain/contracts.mjs.map +1 -0
- package/dist/domain/types.d.ts +45 -0
- package/dist/domain/types.d.ts.map +1 -0
- package/dist/domain/types.js +2 -0
- package/dist/domain/types.js.map +1 -0
- package/dist/entry.d.ts +3 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +4 -0
- package/dist/entry.js.map +1 -0
- package/dist/shared/cli-io.d.mts +15 -0
- package/dist/shared/cli-io.d.mts.map +1 -0
- package/dist/shared/cli-io.mjs +83 -0
- package/dist/shared/cli-io.mjs.map +1 -0
- package/dist/shared/profiles.d.mts +17 -0
- package/dist/shared/profiles.d.mts.map +1 -0
- package/dist/shared/profiles.mjs +25 -0
- package/dist/shared/profiles.mjs.map +1 -0
- package/dist/shared/project-config.d.mts +21 -0
- package/dist/shared/project-config.d.mts.map +1 -0
- package/dist/shared/project-config.mjs +41 -0
- package/dist/shared/project-config.mjs.map +1 -0
- package/dist/shared/project-path.d.mts +3 -0
- package/dist/shared/project-path.d.mts.map +1 -0
- package/dist/shared/project-path.mjs +9 -0
- package/dist/shared/project-path.mjs.map +1 -0
- package/dist/shared/skill-installer.d.mts +2 -0
- package/dist/shared/skill-installer.d.mts.map +1 -0
- package/dist/shared/skill-installer.mjs +33 -0
- package/dist/shared/skill-installer.mjs.map +1 -0
- package/package.json +16 -8
- package/schemas/backlog.schema.json +85 -0
- package/schemas/config.schema.json +29 -0
- package/schemas/gates.schema.json +17 -0
- package/schemas/review.schema.json +28 -0
- package/schemas/tasks.schema.json +66 -0
- package/skills/flow/SKILL.md +10 -11
- package/skills/flow/build/step-01-execute-task.md +2 -2
- package/skills/flow/discovery/bmad-comparison.md +5 -0
- package/skills/flow/discovery/step-01-project.md +8 -3
- package/skills/flow/engineering/profiles/readability-first.md +1 -0
- package/skills/flow/engineering/step-02-synthesize.md +1 -1
- package/skills/flow/invariants.md +4 -4
- package/skills/flow/planning/step-01-create-tasks.md +3 -0
- package/skills/flow/planning/step-01-plan-work-item.md +2 -2
- package/skills/flow/review/step-01-review-work-item.md +1 -1
- package/skills/flow/specification/step-01-deepen-spec.md +7 -0
- package/src/artifacts/backlog.mjs +0 -157
- package/src/artifacts/document.mjs +0 -30
- package/src/artifacts/engineering.mjs +0 -26
- package/src/artifacts/gates.mjs +0 -28
- package/src/artifacts/implementation-plan.mjs +0 -27
- package/src/artifacts/prd.mjs +0 -12
- package/src/artifacts/state.mjs +0 -63
- package/src/artifacts/tasks.mjs +0 -90
- package/src/cli.mjs +0 -62
- package/src/commands/gates.mjs +0 -71
- package/src/commands/graph.mjs +0 -92
- package/src/commands/init.mjs +0 -111
- package/src/commands/migrate.mjs +0 -224
- package/src/commands/route.mjs +0 -140
- package/src/commands/status.mjs +0 -35
- package/src/commands/trace.mjs +0 -58
- package/src/commands/validate.mjs +0 -197
- package/src/shared/cli-io.mjs +0 -86
- package/src/shared/profiles.mjs +0 -27
- package/src/shared/project-config.mjs +0 -39
- package/src/shared/project-path.mjs +0 -10
- package/src/shared/skill-installer.mjs +0 -34
package/src/commands/route.mjs
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { info } from '../shared/cli-io.mjs';
|
|
4
|
-
import { projectRoot } from '../shared/project-path.mjs';
|
|
5
|
-
import { readConfig } from '../shared/project-config.mjs';
|
|
6
|
-
import { parseBacklog, deriveExecutionStatus } from '../artifacts/backlog.mjs';
|
|
7
|
-
import { parseTasks, qualifiedTaskId } from '../artifacts/tasks.mjs';
|
|
8
|
-
import { validateEngineeringDocument } from '../artifacts/engineering.mjs';
|
|
9
|
-
import { validatePrdDocument } from '../artifacts/prd.mjs';
|
|
10
|
-
import { validateImplementationPlan } from '../artifacts/implementation-plan.mjs';
|
|
11
|
-
import { parseState } from '../artifacts/state.mjs';
|
|
12
|
-
|
|
13
|
-
function step(phase, instruction, extra = {}) {
|
|
14
|
-
return { action: 'continue', phase, instruction, ...extra };
|
|
15
|
-
}
|
|
16
|
-
function awaitApproval(phase, instruction, extra = {}) {
|
|
17
|
-
return { action: 'stop', reason: 'consequential_decision', phase, instruction, ...extra };
|
|
18
|
-
}
|
|
19
|
-
function artifactApproval(file, validator, phase, draftInstruction, approvalInstruction) {
|
|
20
|
-
if (!fs.existsSync(file)) return step(phase, draftInstruction);
|
|
21
|
-
const result = validator(fs.readFileSync(file, 'utf8'));
|
|
22
|
-
if (result.errors.length) return step(phase, draftInstruction, { details: result.errors });
|
|
23
|
-
if (result.status !== 'approved') return awaitApproval(phase, approvalInstruction);
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
export function routeProject(root) {
|
|
27
|
-
const flow = path.join(root, '.flow');
|
|
28
|
-
const config = readConfig(root);
|
|
29
|
-
if (!config) return { action: 'stop', reason: 'unrecoverable_blocker', details: 'Run npx --no-install flow init.' };
|
|
30
|
-
if (config.schema_version !== 2)
|
|
31
|
-
return { action: 'stop', reason: 'unrecoverable_blocker', details: 'Run npx --no-install flow migrate.' };
|
|
32
|
-
const read = (relative) => fs.readFileSync(path.join(flow, relative), 'utf8');
|
|
33
|
-
const exists = (relative) => fs.existsSync(path.join(flow, relative));
|
|
34
|
-
const state = exists('state.yaml') ? parseState(read('state.yaml')) : null;
|
|
35
|
-
if (state?.migration.status === 'pending_reconciliation')
|
|
36
|
-
return step('migration_reconciliation', 'migration/step-01-reconcile.md', {
|
|
37
|
-
required_context: [
|
|
38
|
-
'backlog.yaml',
|
|
39
|
-
'docs/prd.md',
|
|
40
|
-
'docs/legacy-state.md',
|
|
41
|
-
'docs/legacy-decisions.md',
|
|
42
|
-
'docs/legacy-engineering.md'
|
|
43
|
-
]
|
|
44
|
-
.filter(exists)
|
|
45
|
-
.map((file) => `.flow/${file}`)
|
|
46
|
-
});
|
|
47
|
-
if (state?.stop_reason && state.stop_reason !== 'finished')
|
|
48
|
-
return { action: 'stop', reason: state.stop_reason, phase: state.execution.phase, step: state.execution.step };
|
|
49
|
-
const productRoute = artifactApproval(
|
|
50
|
-
path.join(flow, 'docs/prd.md'),
|
|
51
|
-
validatePrdDocument,
|
|
52
|
-
'discovery',
|
|
53
|
-
'discovery/step-01-project.md',
|
|
54
|
-
'discovery/step-02-await-approval.md'
|
|
55
|
-
);
|
|
56
|
-
if (productRoute) return productRoute;
|
|
57
|
-
const engineeringRoute = artifactApproval(
|
|
58
|
-
path.join(flow, 'docs/engineering.md'),
|
|
59
|
-
validateEngineeringDocument,
|
|
60
|
-
'engineering',
|
|
61
|
-
'engineering/step-02-synthesize.md',
|
|
62
|
-
'engineering/step-05-present.md'
|
|
63
|
-
);
|
|
64
|
-
if (engineeringRoute) return engineeringRoute;
|
|
65
|
-
const engineeringText = read('docs/engineering.md');
|
|
66
|
-
const planning = () =>
|
|
67
|
-
step('backlog_planning', 'planning/step-01-plan-work-item.md', {
|
|
68
|
-
required_context: ['.flow/docs/prd.md', '.flow/docs/engineering.md']
|
|
69
|
-
});
|
|
70
|
-
if (!exists('backlog.yaml')) return planning();
|
|
71
|
-
const backlog = parseBacklog(read('backlog.yaml'));
|
|
72
|
-
for (const item of backlog.work_items) {
|
|
73
|
-
if (!exists(`work-items/${item.folder}/spec.md`) || !exists(`work-items/${item.folder}/tasks.yaml`))
|
|
74
|
-
return planning();
|
|
75
|
-
parseTasks(read(`work-items/${item.folder}/tasks.yaml`), { expectedWorkItem: item.id });
|
|
76
|
-
}
|
|
77
|
-
const byId = new Map(backlog.work_items.map((item) => [item.id, item]));
|
|
78
|
-
const active = backlog.work_items.find((item) => item.state === 'in_progress');
|
|
79
|
-
if (active && deriveExecutionStatus(active, byId).status === 'blocked')
|
|
80
|
-
return {
|
|
81
|
-
action: 'stop',
|
|
82
|
-
reason: 'dependency_blocked',
|
|
83
|
-
work_item: active.id,
|
|
84
|
-
details: deriveExecutionStatus(active, byId).reasons
|
|
85
|
-
};
|
|
86
|
-
const item =
|
|
87
|
-
active ??
|
|
88
|
-
backlog.work_items
|
|
89
|
-
.filter((item) => deriveExecutionStatus(item, byId).status === 'ready')
|
|
90
|
-
.sort((a, b) => a.priority - b.priority || Number(a.id.slice(1)) - Number(b.id.slice(1)))[0];
|
|
91
|
-
if (!item)
|
|
92
|
-
return {
|
|
93
|
-
action: 'stop',
|
|
94
|
-
reason: backlog.work_items.every((item) => item.state === 'completed') ? 'finished' : 'external_action'
|
|
95
|
-
};
|
|
96
|
-
const base = `work-items/${item.folder}`;
|
|
97
|
-
const context = [
|
|
98
|
-
'.flow/docs/prd.md',
|
|
99
|
-
'.flow/docs/engineering.md',
|
|
100
|
-
`.flow/${base}/spec.md`,
|
|
101
|
-
`.flow/${base}/tasks.yaml`
|
|
102
|
-
];
|
|
103
|
-
const tasks = parseTasks(read(`${base}/tasks.yaml`), { expectedWorkItem: item.id });
|
|
104
|
-
const extra = { work_item: item.id, required_context: context };
|
|
105
|
-
const prepare = () => step('work_item_plan_approval', 'planning/step-02-prepare-plan.md', extra);
|
|
106
|
-
if (!exists(`${base}/implementation-plan.md`)) return prepare();
|
|
107
|
-
const plan = validateImplementationPlan(read(`${base}/implementation-plan.md`), {
|
|
108
|
-
workItem: item.id,
|
|
109
|
-
engineeringText,
|
|
110
|
-
specText: read(`${base}/spec.md`)
|
|
111
|
-
});
|
|
112
|
-
if (plan.errors.length) return prepare();
|
|
113
|
-
if (plan.status !== 'approved')
|
|
114
|
-
return awaitApproval('work_item_plan_approval', 'planning/step-03-await-approval.md', extra);
|
|
115
|
-
context.push(`.flow/${base}/implementation-plan.md`);
|
|
116
|
-
const taskById = new Map(tasks.tasks.map((task) => [task.id, task]));
|
|
117
|
-
const task =
|
|
118
|
-
tasks.tasks.find((task) => task.state === 'in_progress') ??
|
|
119
|
-
tasks.tasks
|
|
120
|
-
.filter(
|
|
121
|
-
(task) => task.state === 'pending' && task.depends_on.every((id) => taskById.get(id).state === 'completed')
|
|
122
|
-
)
|
|
123
|
-
.sort((a, b) => Number(a.id.slice(1)) - Number(b.id.slice(1)))[0];
|
|
124
|
-
if (task)
|
|
125
|
-
return step('implementation', 'build/step-01-execute-task.md', {
|
|
126
|
-
...extra,
|
|
127
|
-
task: qualifiedTaskId(item.id, task.id)
|
|
128
|
-
});
|
|
129
|
-
if (tasks.tasks.every((task) => task.state === 'completed'))
|
|
130
|
-
return step('work_item_review', 'review/step-01-review-work-item.md', extra);
|
|
131
|
-
return step('reconcile', 'reconcile/step-01-reconcile.md', extra);
|
|
132
|
-
}
|
|
133
|
-
export function runRoute({ args }) {
|
|
134
|
-
const result = routeProject(projectRoot(args));
|
|
135
|
-
info(
|
|
136
|
-
args.includes('--json')
|
|
137
|
-
? JSON.stringify(result, null, 2)
|
|
138
|
-
: `${result.action}: ${result.phase ?? result.reason}${result.work_item ? ` ${result.work_item}` : ''}`
|
|
139
|
-
);
|
|
140
|
-
}
|
package/src/commands/status.mjs
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { info } from '../shared/cli-io.mjs';
|
|
4
|
-
import { projectRoot } from '../shared/project-path.mjs';
|
|
5
|
-
import { deriveExecutionStatus, parseBacklog } from '../artifacts/backlog.mjs';
|
|
6
|
-
import { routeProject } from './route.mjs';
|
|
7
|
-
|
|
8
|
-
export function runStatus({ args }) {
|
|
9
|
-
const root = projectRoot(args);
|
|
10
|
-
if (!fs.existsSync(path.join(root, '.flow', 'backlog.yaml')))
|
|
11
|
-
return info(`Next: ${routeProject(root).phase ?? 'setup'}`);
|
|
12
|
-
const backlog = parseBacklog(fs.readFileSync(path.join(root, '.flow', 'backlog.yaml'), 'utf8'));
|
|
13
|
-
const byId = new Map(backlog.work_items.map((item) => [item.id, item]));
|
|
14
|
-
const groups = new Map(['in_progress', 'ready', 'blocked', 'completed'].map((status) => [status, []]));
|
|
15
|
-
for (const item of backlog.work_items) {
|
|
16
|
-
const derived = deriveExecutionStatus(item, byId);
|
|
17
|
-
groups.get(derived.status).push({
|
|
18
|
-
...item,
|
|
19
|
-
reasons: derived.reasons,
|
|
20
|
-
blockers: item.blockers.filter((blocker) => blocker.status === 'unresolved')
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
const lines = [];
|
|
24
|
-
for (const [status, items] of groups) {
|
|
25
|
-
if (!items.length) continue;
|
|
26
|
-
lines.push(status.replace('_', ' ').replace(/^./, (c) => c.toUpperCase()));
|
|
27
|
-
for (const item of items) {
|
|
28
|
-
const deps = item.reasons.filter((r) => r.type === 'dependency').map((r) => r.ref);
|
|
29
|
-
lines.push(` ${item.id} — ${item.title}${deps.length ? ` ← ${deps.join(', ')}` : ''}`);
|
|
30
|
-
for (const blocker of item.blockers) lines.push(` blocker: ${blocker.description} [${blocker.id}]`);
|
|
31
|
-
}
|
|
32
|
-
lines.push('');
|
|
33
|
-
}
|
|
34
|
-
info(lines.join('\n').trim());
|
|
35
|
-
}
|
package/src/commands/trace.mjs
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { info, fail } from '../shared/cli-io.mjs';
|
|
3
|
-
import { projectRoot } from '../shared/project-path.mjs';
|
|
4
|
-
|
|
5
|
-
export function traceTask(root, qualifiedTaskId) {
|
|
6
|
-
if (!/^W\d{3,}-T\d{3,}$/.test(qualifiedTaskId))
|
|
7
|
-
fail(`invalid qualified task ID '${qualifiedTaskId}'. Expected W015-T003.`);
|
|
8
|
-
return traceTasks(root, [qualifiedTaskId]).get(qualifiedTaskId);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function traceTasks(root, qualifiedTaskIds) {
|
|
12
|
-
for (const qualifiedTaskId of qualifiedTaskIds)
|
|
13
|
-
if (!/^W\d{3,}-T\d{3,}$/.test(qualifiedTaskId))
|
|
14
|
-
fail(`invalid qualified task ID '${qualifiedTaskId}'. Expected W015-T003.`);
|
|
15
|
-
const requested = new Set(qualifiedTaskIds);
|
|
16
|
-
const matches = new Map([...requested].map((task) => [task, []]));
|
|
17
|
-
let output;
|
|
18
|
-
try {
|
|
19
|
-
output = execFileSync('git', ['log', 'HEAD', '--format=%H%x1f%s%x1f%B%x1e'], {
|
|
20
|
-
cwd: root,
|
|
21
|
-
encoding: 'utf8',
|
|
22
|
-
maxBuffer: 50 * 1024 * 1024
|
|
23
|
-
});
|
|
24
|
-
} catch {
|
|
25
|
-
fail('git history is unavailable; task traceability requires a Git repository.');
|
|
26
|
-
}
|
|
27
|
-
for (const entry of output
|
|
28
|
-
.split('\x1e')
|
|
29
|
-
.filter(Boolean)
|
|
30
|
-
.map((record) => {
|
|
31
|
-
const [sha, subject, ...bodyParts] = record.replace(/^\n+|\n+$/g, '').split('\x1f');
|
|
32
|
-
return { sha, subject, body: bodyParts.join('\x1f') };
|
|
33
|
-
})) {
|
|
34
|
-
const workItems = new Set([...entry.body.matchAll(/^Flow-Work-Item:\s*(W\d{3,})\s*$/gm)].map((match) => match[1]));
|
|
35
|
-
for (const match of entry.body.matchAll(/^Flow-Task:\s*(W\d{3,}-T\d{3,})\s*$/gm)) {
|
|
36
|
-
const task = match[1];
|
|
37
|
-
if (requested.has(task) && workItems.has(task.split('-')[0])) matches.get(task).push(entry);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return new Map(
|
|
41
|
-
[...requested].map((task) => {
|
|
42
|
-
const commits = matches.get(task);
|
|
43
|
-
if (!commits.length) return [task, { task, commits, status: 'missing' }];
|
|
44
|
-
if (commits.length > 1) return [task, { task, commits, status: 'ambiguous' }];
|
|
45
|
-
return [task, { task, commit: commits[0], commits, status: 'resolved' }];
|
|
46
|
-
})
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function runTrace({ args }) {
|
|
51
|
-
const task = args.find((arg) => !arg.startsWith('--'));
|
|
52
|
-
if (!task) fail('flow trace requires a qualified task ID, e.g. W015-T003.');
|
|
53
|
-
const result = traceTask(projectRoot(args), task);
|
|
54
|
-
if (args.includes('--json')) return info(JSON.stringify(result, null, 2));
|
|
55
|
-
if (result.status === 'missing') fail(`No reachable commit declares Flow-Task: ${task}.`);
|
|
56
|
-
if (result.status === 'ambiguous') fail(`Multiple reachable commits declare Flow-Task: ${task}.`);
|
|
57
|
-
info(`${task} -> ${result.commit.sha}\n${result.commit.subject}`);
|
|
58
|
-
}
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { info, fail } from '../shared/cli-io.mjs';
|
|
4
|
-
import { projectRoot } from '../shared/project-path.mjs';
|
|
5
|
-
import { readConfig } from '../shared/project-config.mjs';
|
|
6
|
-
import { parseBacklog, deriveExecutionStatus } from '../artifacts/backlog.mjs';
|
|
7
|
-
import { parseTasks, qualifiedTaskId } from '../artifacts/tasks.mjs';
|
|
8
|
-
import { parseState } from '../artifacts/state.mjs';
|
|
9
|
-
import { parseGates } from '../artifacts/gates.mjs';
|
|
10
|
-
import { validateEngineeringDocument } from '../artifacts/engineering.mjs';
|
|
11
|
-
import { validatePrdDocument } from '../artifacts/prd.mjs';
|
|
12
|
-
import { validateImplementationPlan } from '../artifacts/implementation-plan.mjs';
|
|
13
|
-
import { traceTasks } from './trace.mjs';
|
|
14
|
-
import { generateGraphMarkdown } from './graph.mjs';
|
|
15
|
-
import { evaluateGates } from './gates.mjs';
|
|
16
|
-
|
|
17
|
-
export function validateProject(
|
|
18
|
-
root,
|
|
19
|
-
{ preCommitTask = null, skipTrace = false, evaluateConfiguredGates = false, onGateResults } = {}
|
|
20
|
-
) {
|
|
21
|
-
const findings = [];
|
|
22
|
-
const error = (code, message) => findings.push({ level: 'error', code, message });
|
|
23
|
-
const flow = path.join(root, '.flow');
|
|
24
|
-
const exists = (relative) => fs.existsSync(path.join(flow, relative));
|
|
25
|
-
const hasExactRootEntry = (name) => fs.readdirSync(flow).includes(name);
|
|
26
|
-
const read = (relative) => fs.readFileSync(path.join(flow, relative), 'utf8');
|
|
27
|
-
let config;
|
|
28
|
-
try {
|
|
29
|
-
config = readConfig(root);
|
|
30
|
-
} catch (failure) {
|
|
31
|
-
error('CONFIG', failure.message);
|
|
32
|
-
}
|
|
33
|
-
if (!config || config.schema_version !== 2) {
|
|
34
|
-
error('CONFIG', 'Initialize Flow or migrate the existing project.');
|
|
35
|
-
return findings;
|
|
36
|
-
}
|
|
37
|
-
if (
|
|
38
|
-
config.engineering.profile !== 'flow/readability-first@1' ||
|
|
39
|
-
!['improve', 'preserve', 'not_applicable'].includes(config.engineering.existing_code_policy)
|
|
40
|
-
)
|
|
41
|
-
error('CONFIG', 'Invalid engineering bootstrap preferences.');
|
|
42
|
-
for (const name of ['STATE.md', 'DECISIONS.md', 'SUMMARY.md', 'BACKLOG.yaml', 'PRD.md', 'ENGINEERING.md', 'GRAPH.md'])
|
|
43
|
-
if (hasExactRootEntry(name)) error('LEGACY', `${name} must be migrated.`);
|
|
44
|
-
let state;
|
|
45
|
-
try {
|
|
46
|
-
state = exists('state.yaml') ? parseState(read('state.yaml')) : null;
|
|
47
|
-
} catch (failure) {
|
|
48
|
-
error('STATE', failure.message);
|
|
49
|
-
}
|
|
50
|
-
const migrationPending = state?.migration.status === 'pending_reconciliation';
|
|
51
|
-
const backlogRequired = [
|
|
52
|
-
'backlog_planning',
|
|
53
|
-
'work_item_plan_approval',
|
|
54
|
-
'implementation',
|
|
55
|
-
'work_item_review',
|
|
56
|
-
'complete'
|
|
57
|
-
].includes(state?.execution.phase);
|
|
58
|
-
if (!exists('backlog.yaml')) {
|
|
59
|
-
if (backlogRequired) error('MISSING', 'Missing backlog.yaml.');
|
|
60
|
-
return findings;
|
|
61
|
-
}
|
|
62
|
-
let backlog;
|
|
63
|
-
try {
|
|
64
|
-
backlog = parseBacklog(read('backlog.yaml'));
|
|
65
|
-
} catch (failure) {
|
|
66
|
-
error('BACKLOG', failure.message);
|
|
67
|
-
return findings;
|
|
68
|
-
}
|
|
69
|
-
const anyStarted = backlog.work_items.some((item) => item.state !== 'pending');
|
|
70
|
-
if (!migrationPending) {
|
|
71
|
-
for (const [name, validator] of [
|
|
72
|
-
['prd.md', validatePrdDocument],
|
|
73
|
-
['engineering.md', validateEngineeringDocument]
|
|
74
|
-
]) {
|
|
75
|
-
if (!exists(`docs/${name}`)) {
|
|
76
|
-
error('MISSING', `Missing docs/${name}.`);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
const document = validator(read(`docs/${name}`));
|
|
80
|
-
for (const message of document.errors) error('DOCUMENT', `${name}: ${message}`);
|
|
81
|
-
if (anyStarted && document.status !== 'approved')
|
|
82
|
-
error('APPROVAL', `${name} must be approved before implementation.`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const byId = new Map(backlog.work_items.map((item) => [item.id, item]));
|
|
86
|
-
let activeTasks = 0;
|
|
87
|
-
const traceCandidates = [];
|
|
88
|
-
for (const item of backlog.work_items) {
|
|
89
|
-
const base = `work-items/${item.folder}`;
|
|
90
|
-
const missing = ['spec.md', 'tasks.yaml'].filter((name) => !exists(`${base}/${name}`));
|
|
91
|
-
if (missing.length) {
|
|
92
|
-
if (!(migrationPending && item.state === 'pending'))
|
|
93
|
-
error('WORK_ITEM', `${item.id} is missing ${missing.join(', ')}.`);
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
let tasks;
|
|
97
|
-
try {
|
|
98
|
-
tasks = parseTasks(read(`${base}/tasks.yaml`), { expectedWorkItem: item.id });
|
|
99
|
-
} catch (failure) {
|
|
100
|
-
error('TASKS', failure.message);
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
const historical = tasks.tasks.length > 0 && tasks.tasks.every((task) => task.implementation === 'legacy');
|
|
104
|
-
activeTasks += tasks.tasks.filter((task) => task.state === 'in_progress').length;
|
|
105
|
-
if (tasks.tasks.some((task) => task.state === 'in_progress') && item.state !== 'in_progress')
|
|
106
|
-
error('STATE', `${item.id}: active task requires active work item.`);
|
|
107
|
-
if (item.state === 'completed' && tasks.tasks.some((task) => task.state !== 'completed'))
|
|
108
|
-
error('STATE', `${item.id}: completed work has incomplete tasks.`);
|
|
109
|
-
if (item.state === 'in_progress' && deriveExecutionStatus(item, byId).status === 'blocked')
|
|
110
|
-
error('STATE', `${item.id}: active work is blocked.`);
|
|
111
|
-
if (!historical && !migrationPending) {
|
|
112
|
-
const spec = read(`${base}/spec.md`);
|
|
113
|
-
for (const heading of [
|
|
114
|
-
'## Status',
|
|
115
|
-
'## Goal',
|
|
116
|
-
'## Scope',
|
|
117
|
-
'## Non-goals',
|
|
118
|
-
'## Requirements',
|
|
119
|
-
'## Acceptance criteria',
|
|
120
|
-
'## Decisions'
|
|
121
|
-
])
|
|
122
|
-
if (!spec.split(/\r?\n/).includes(heading)) error('SPEC', `${item.id} missing ${heading}.`);
|
|
123
|
-
const planPath = `${base}/implementation-plan.md`;
|
|
124
|
-
if (!exists(planPath)) {
|
|
125
|
-
if (item.state !== 'pending') error('PLAN', `${item.id} requires an approved implementation plan.`);
|
|
126
|
-
} else if (exists('docs/engineering.md')) {
|
|
127
|
-
const plan = validateImplementationPlan(read(planPath), {
|
|
128
|
-
workItem: item.id,
|
|
129
|
-
engineeringText: read('docs/engineering.md'),
|
|
130
|
-
specText: spec,
|
|
131
|
-
checkRevisions: item.state !== 'completed'
|
|
132
|
-
});
|
|
133
|
-
for (const message of plan.errors) error('PLAN', `${item.id}: ${message}`);
|
|
134
|
-
if (item.state !== 'pending' && plan.status !== 'approved') error('PLAN', `${item.id}: plan is not approved.`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
for (const task of tasks.tasks)
|
|
138
|
-
if (task.state === 'completed' && task.implementation === 'commit' && !skipTrace) {
|
|
139
|
-
const qualified = qualifiedTaskId(item.id, task.id);
|
|
140
|
-
if (qualified !== preCommitTask) traceCandidates.push(qualified);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
if (traceCandidates.length) {
|
|
144
|
-
try {
|
|
145
|
-
for (const [qualified, result] of traceTasks(root, traceCandidates))
|
|
146
|
-
if (result.status !== 'resolved')
|
|
147
|
-
error('TRACE', `${qualified}: expected exactly one HEAD-reachable commit with both Flow trailers.`);
|
|
148
|
-
} catch (failure) {
|
|
149
|
-
for (const qualified of traceCandidates) error('TRACE', `${qualified}: ${failure.message}`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
if (activeTasks > 1) error('STATE', 'Only one mutating task may be active across the project.');
|
|
153
|
-
if (exists('gates.yaml')) {
|
|
154
|
-
try {
|
|
155
|
-
parseGates(read('gates.yaml'));
|
|
156
|
-
if (!migrationPending && evaluateConfiguredGates) {
|
|
157
|
-
const gateResults = evaluateGates(root);
|
|
158
|
-
onGateResults?.(gateResults);
|
|
159
|
-
for (const gate of gateResults)
|
|
160
|
-
if (gate.blocking && ['failed', 'unsupported'].includes(gate.status))
|
|
161
|
-
error('GATE', `${gate.id}: ${gate.status}`);
|
|
162
|
-
}
|
|
163
|
-
} catch (failure) {
|
|
164
|
-
error('GATES', failure.message);
|
|
165
|
-
}
|
|
166
|
-
} else if (!migrationPending) error('MISSING', 'Missing gates.yaml.');
|
|
167
|
-
if (!exists('docs/graph.md') || read('docs/graph.md') !== generateGraphMarkdown(read('backlog.yaml')))
|
|
168
|
-
error('GRAPH', 'Graph is stale; run npx --no-install flow graph.');
|
|
169
|
-
return findings;
|
|
170
|
-
}
|
|
171
|
-
export function runValidate({ args }) {
|
|
172
|
-
const index = args.indexOf('--pre-commit');
|
|
173
|
-
const includeGates = args.includes('--gates');
|
|
174
|
-
let gateResults = [];
|
|
175
|
-
const findings = validateProject(projectRoot(args), {
|
|
176
|
-
preCommitTask: index < 0 ? null : args[index + 1],
|
|
177
|
-
skipTrace: args.includes('--skip-trace'),
|
|
178
|
-
evaluateConfiguredGates: includeGates,
|
|
179
|
-
onGateResults: (results) => {
|
|
180
|
-
gateResults = results;
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
if (args.includes('--json')) {
|
|
184
|
-
info(
|
|
185
|
-
JSON.stringify(
|
|
186
|
-
{ valid: findings.length === 0, findings, ...(includeGates ? { gates: gateResults } : {}) },
|
|
187
|
-
null,
|
|
188
|
-
2
|
|
189
|
-
)
|
|
190
|
-
);
|
|
191
|
-
if (findings.length) process.exitCode = 1;
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (!findings.length) return info('Flow project is valid.');
|
|
195
|
-
for (const finding of findings) info(`${finding.code}: ${finding.message}`);
|
|
196
|
-
fail(`${findings.length} validation finding(s).`);
|
|
197
|
-
}
|
package/src/shared/cli-io.mjs
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import readline from 'node:readline/promises';
|
|
2
|
-
import { stdin as inputStream, stdout as outputStream } from 'node:process';
|
|
3
|
-
|
|
4
|
-
export class CliError extends Error {}
|
|
5
|
-
export function fail(message, code = 1) {
|
|
6
|
-
const error = new CliError(message);
|
|
7
|
-
error.exitCode = code;
|
|
8
|
-
throw error;
|
|
9
|
-
}
|
|
10
|
-
export function info(message = '') {
|
|
11
|
-
console.log(message);
|
|
12
|
-
}
|
|
13
|
-
function abortedPromptError() {
|
|
14
|
-
const error = new Error('Prompt aborted.');
|
|
15
|
-
error.code = 'ABORT_ERR';
|
|
16
|
-
return error;
|
|
17
|
-
}
|
|
18
|
-
function question(readlineInterface, message) {
|
|
19
|
-
return new Promise((resolve, reject) => {
|
|
20
|
-
let settled = false;
|
|
21
|
-
const finish = (callback, value) => {
|
|
22
|
-
if (settled) return;
|
|
23
|
-
settled = true;
|
|
24
|
-
readlineInterface.removeListener('close', onClose);
|
|
25
|
-
callback(value);
|
|
26
|
-
};
|
|
27
|
-
const onClose = () => finish(reject, abortedPromptError());
|
|
28
|
-
readlineInterface.once('close', onClose);
|
|
29
|
-
readlineInterface.question(message).then(
|
|
30
|
-
(answer) => finish(resolve, answer),
|
|
31
|
-
(error) => finish(reject, error)
|
|
32
|
-
);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
export async function promptText(message, defaultValue = '') {
|
|
36
|
-
const rl = readline.createInterface({ input: inputStream, output: outputStream });
|
|
37
|
-
try {
|
|
38
|
-
const answer = (await question(rl, `${message}${defaultValue ? ` [${defaultValue}]` : ''}: `)).trim();
|
|
39
|
-
return answer || defaultValue;
|
|
40
|
-
} finally {
|
|
41
|
-
rl.close();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
export async function promptSelect({ title, options, defaultIndex = 0 }) {
|
|
45
|
-
info(title);
|
|
46
|
-
options.forEach((option, index) =>
|
|
47
|
-
info(
|
|
48
|
-
` ${index === defaultIndex ? '›' : ' '} ${index + 1}. ${option.label}${option.description ? `\n ${option.description}` : ''}`
|
|
49
|
-
)
|
|
50
|
-
);
|
|
51
|
-
const rl = readline.createInterface({ input: inputStream, output: outputStream });
|
|
52
|
-
try {
|
|
53
|
-
while (true) {
|
|
54
|
-
const answer = (await question(rl, `Selection [${defaultIndex + 1}]: `)).trim();
|
|
55
|
-
const index = answer ? Number.parseInt(answer, 10) - 1 : defaultIndex;
|
|
56
|
-
if (Number.isInteger(index) && index >= 0 && index < options.length) return options[index].value;
|
|
57
|
-
info('Choose a valid number.');
|
|
58
|
-
}
|
|
59
|
-
} finally {
|
|
60
|
-
rl.close();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
export async function promptMultiSelect({ title, options }) {
|
|
64
|
-
info(title);
|
|
65
|
-
options.forEach((option, index) => info(` [ ] ${index + 1}. ${option.label}`));
|
|
66
|
-
info(' (Select multiple with comma-separated numbers, e.g. 1,2)');
|
|
67
|
-
const rl = readline.createInterface({ input: inputStream, output: outputStream });
|
|
68
|
-
try {
|
|
69
|
-
while (true) {
|
|
70
|
-
const answer = (await question(rl, 'Selection: ')).trim();
|
|
71
|
-
const indices = [
|
|
72
|
-
...new Set(
|
|
73
|
-
answer
|
|
74
|
-
.split(',')
|
|
75
|
-
.map((value) => Number.parseInt(value.trim(), 10))
|
|
76
|
-
.filter(Number.isInteger)
|
|
77
|
-
)
|
|
78
|
-
];
|
|
79
|
-
if (indices.length && indices.every((index) => index >= 1 && index <= options.length))
|
|
80
|
-
return indices.map((index) => options[index - 1].value);
|
|
81
|
-
info('Choose one or more valid numbers.');
|
|
82
|
-
}
|
|
83
|
-
} finally {
|
|
84
|
-
rl.close();
|
|
85
|
-
}
|
|
86
|
-
}
|
package/src/shared/profiles.mjs
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import { parse } from 'yaml';
|
|
3
|
-
const text = fs.readFileSync(
|
|
4
|
-
new URL('../../skills/flow/engineering/profiles/readability-first.md', import.meta.url),
|
|
5
|
-
'utf8'
|
|
6
|
-
);
|
|
7
|
-
export function parseProfileFrontmatter(text) {
|
|
8
|
-
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
9
|
-
if (!match) throw new Error('Missing YAML frontmatter in Flow profile.');
|
|
10
|
-
return parse(match[1]);
|
|
11
|
-
}
|
|
12
|
-
export const READABILITY_FIRST_PROFILE = parseProfileFrontmatter(text);
|
|
13
|
-
export const ENGINEERING_PROFILES = {
|
|
14
|
-
'readability-first': READABILITY_FIRST_PROFILE,
|
|
15
|
-
[READABILITY_FIRST_PROFILE.id]: READABILITY_FIRST_PROFILE
|
|
16
|
-
};
|
|
17
|
-
export const BROWNFIELD_POLICIES = {
|
|
18
|
-
improve: {
|
|
19
|
-
label: 'Improve existing structure — Recommended',
|
|
20
|
-
description:
|
|
21
|
-
'Preserve behavior and external contracts; recommend clearer structure where justified. Does not authorize refactoring.'
|
|
22
|
-
},
|
|
23
|
-
preserve: {
|
|
24
|
-
label: 'Keep existing structure',
|
|
25
|
-
description: 'Retain consistent conventions unless a concrete problem warrants an approved change.'
|
|
26
|
-
}
|
|
27
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { parse, stringify } from 'yaml';
|
|
4
|
-
|
|
5
|
-
export function configPath(root) {
|
|
6
|
-
return path.join(root, '.flow', 'config.yaml');
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function defaultConfig() {
|
|
10
|
-
return {
|
|
11
|
-
schema_version: 2,
|
|
12
|
-
runtimes: [],
|
|
13
|
-
engineering: { profile: 'flow/readability-first@1', existing_code_policy: 'not_applicable' }
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function readConfig(root) {
|
|
18
|
-
const file = configPath(root);
|
|
19
|
-
if (!fs.existsSync(file)) return null;
|
|
20
|
-
const raw = parse(fs.readFileSync(file, 'utf8')) ?? {};
|
|
21
|
-
const config = defaultConfig();
|
|
22
|
-
config.schema_version = raw.schema_version ?? 1;
|
|
23
|
-
config.runtimes = Array.isArray(raw.runtimes) ? raw.runtimes : [];
|
|
24
|
-
config.engineering = { ...config.engineering, ...(raw.engineering ?? {}) };
|
|
25
|
-
return config;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function writeConfig(root, config) {
|
|
29
|
-
const normalized = {
|
|
30
|
-
schema_version: 2,
|
|
31
|
-
runtimes: config.runtimes ?? [],
|
|
32
|
-
engineering: {
|
|
33
|
-
profile: config.engineering?.profile ?? defaultConfig().engineering.profile,
|
|
34
|
-
existing_code_policy: config.engineering?.existing_code_policy ?? 'not_applicable'
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
fs.mkdirSync(path.join(root, '.flow'), { recursive: true });
|
|
38
|
-
fs.writeFileSync(configPath(root), stringify(normalized, { lineWidth: 0 }), 'utf8');
|
|
39
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
|
-
export function valueAfter(args, name) {
|
|
4
|
-
const index = args.indexOf(name);
|
|
5
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function projectRoot(args) {
|
|
9
|
-
return path.resolve(valueAfter(args, '--path') || process.cwd());
|
|
10
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
function copyDirectory(source, target) {
|
|
5
|
-
fs.mkdirSync(target, { recursive: true });
|
|
6
|
-
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
7
|
-
const sourcePath = path.join(source, entry.name);
|
|
8
|
-
const targetPath = path.join(target, entry.name);
|
|
9
|
-
if (entry.isDirectory()) copyDirectory(sourcePath, targetPath);
|
|
10
|
-
else fs.copyFileSync(sourcePath, targetPath);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function installRuntimeSkill(root, runtime, packageRoot) {
|
|
15
|
-
if (
|
|
16
|
-
typeof runtime.skills_path !== 'string' ||
|
|
17
|
-
!runtime.skills_path ||
|
|
18
|
-
path.isAbsolute(runtime.skills_path) ||
|
|
19
|
-
runtime.skills_path.split(/[\\/]/).includes('..')
|
|
20
|
-
)
|
|
21
|
-
throw new Error('Runtime skills_path must remain project-local.');
|
|
22
|
-
const target = path.join(root, runtime.skills_path, 'flow');
|
|
23
|
-
let ancestor = root;
|
|
24
|
-
for (const segment of path.relative(root, target).split(path.sep)) {
|
|
25
|
-
ancestor = path.join(ancestor, segment);
|
|
26
|
-
if (fs.existsSync(ancestor) && fs.lstatSync(ancestor).isSymbolicLink())
|
|
27
|
-
throw new Error('Refusing a symlinked runtime path.');
|
|
28
|
-
}
|
|
29
|
-
if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink())
|
|
30
|
-
throw new Error('Refusing to overwrite a symlinked skill.');
|
|
31
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
32
|
-
copyDirectory(path.join(packageRoot, 'skills', 'flow'), target);
|
|
33
|
-
return target;
|
|
34
|
-
}
|