create-harness-vibe-coding 0.6.3 → 0.6.4
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/package.json +1 -1
- package/src/generator.js +466 -466
- package/src/index.js +355 -355
- package/templates/common/.claude/agents/architect.md +31 -35
- package/templates/common/.claude/agents/context-master.md +0 -1
- package/templates/common/.claude/agents/debugger.md +0 -1
- package/templates/common/.claude/agents/docs-researcher.md +41 -43
- package/templates/common/.claude/agents/implementer.md +0 -1
- package/templates/common/.claude/agents/memory-master.md +0 -1
- package/templates/common/.claude/agents/planner.md +0 -1
- package/templates/common/.claude/agents/researcher.md +0 -1
- package/templates/common/.claude/agents/reviewer.md +34 -35
- package/templates/common/.claude/agents/test-writer.md +0 -1
- package/templates/common/.claude/agents/verifier.md +0 -1
- package/templates/common/.claude/commands/wf-max.md +7 -0
- package/templates/common/.claude/commands/{update.md → wf-update.md} +4 -0
- package/templates/common/.claude/commands/wf.md +10 -3
- package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +2 -1
- package/templates/common/.claude/skills/wf-max/SKILL.md +29 -70
- package/templates/common/.claude/skills/{readme-optimizer → wf-readme}/SKILL.md +1 -1
- package/templates/common/.claude/skills/wf-review/SKILL.md +50 -50
- package/templates/common/.claude/skills/wf-update/SKILL.md +58 -58
- package/templates/common/CLAUDE.md +77 -76
- package/templates/common/MEMORY.md +73 -76
- package/templates/common/README.md +1 -1
- package/templates/common/SETUP.md +273 -341
- package/templates/common/docs/README.md +131 -145
- package/templates/common/docs/harness/WF.md +13 -1
- package/templates/common/docs/harness/agent-workflow.md +94 -94
- package/templates/common/docs/harness/architecture.md +1 -1
- package/templates/common/docs/harness/context-loading.md +104 -108
- package/templates/common/docs/harness/extension.md +70 -79
- package/templates/common/docs/harness/lifecycle.md +33 -33
- package/templates/common/docs/harness/subagents.md +1 -1
- package/templates/common/docs/research/PRD.md +65 -65
- package/templates/common/docs/research/README.md +169 -169
- package/templates/common/scripts/validate-harness.mjs +439 -460
- package/templates/optional/skills/browser-e2e/.claude/skills/browser-e2e/SKILL.md +42 -42
- package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +30 -0
- package/templates/optional/skills/browser-e2e/docs/workflows/browser-e2e.md +1 -1
- package/templates/optional/skills/github-pr-review/.claude/skills/github-pr-review/SKILL.md +40 -40
- package/templates/optional/skills/python-backend/.claude/skills/python-backend/SKILL.md +40 -40
- package/templates/optional/skills/ts-react-frontend/.claude/skills/ts-react-frontend/SKILL.md +43 -43
- package/templates/optional/skills/ui-ux-review/.claude/skills/ui-ux-review/SKILL.md +40 -40
- package/templates/common/.claude/skills/harness-build-loop/SKILL.md +0 -23
- package/templates/common/.claude/skills/harness-context/SKILL.md +0 -26
- package/templates/common/.claude/skills/harness-lifecycle/SKILL.md +0 -20
- package/templates/common/.claude/skills/harness-research/SKILL.md +0 -30
- package/templates/common/.claude/skills/harness-router/SKILL.md +0 -16
- package/templates/common/.claude/skills/wf-mode/SKILL.md +0 -55
- package/templates/common/docs/domain/ports.md +0 -76
- package/templates/common/docs/features/_template.md +0 -177
- package/templates/common/docs/harness/PLAN.md +0 -52
- package/templates/common/docs/harness/data-flow.md +0 -59
- package/templates/common/docs/harness/state-machines.md +0 -58
- /package/templates/common/.claude/commands/{learn.md → wf-learn.md} +0 -0
package/src/generator.js
CHANGED
|
@@ -1,466 +1,466 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = path.dirname(__filename);
|
|
7
|
-
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8'));
|
|
8
|
-
const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates', 'common');
|
|
9
|
-
const OPTIONAL_DIR = path.resolve(__dirname, '..', 'templates', 'optional');
|
|
10
|
-
const OPTIONAL_CATALOG = path.join(OPTIONAL_DIR, 'catalog.json');
|
|
11
|
-
const VALID_CONFLICT_POLICIES = new Set(['fail', 'skip', 'backup', 'overwrite']);
|
|
12
|
-
const EMPTY_DIRS = [
|
|
13
|
-
'.claude/hooks',
|
|
14
|
-
'tests',
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
function harnessDest(file) {
|
|
18
|
-
if (file === '.harness-version') return 'Harness/.harness-version';
|
|
19
|
-
if (file === 'SETUP.md') return 'Harness/SETUP.md';
|
|
20
|
-
if (file === 'MEMORY.md') return 'Harness/MEMORY.md';
|
|
21
|
-
if (file === 'scripts/validate-harness.mjs') return 'Harness/scripts/validate-harness.mjs';
|
|
22
|
-
if (file.startsWith('memory/')) return `Harness/${file}`;
|
|
23
|
-
if (file === 'docs/README.md') return 'Harness/README.md';
|
|
24
|
-
if (file.startsWith('docs/harness/')) return file.replace(/^docs\/harness\//, 'Harness/');
|
|
25
|
-
if (file.startsWith('docs/research/')) return file.replace(/^docs\/research\//, 'Harness/research/');
|
|
26
|
-
if (file.startsWith('docs/domain/')) return file.replace(/^docs\/domain\//, 'Harness/domain/');
|
|
27
|
-
if (file.startsWith('docs/features/')) return file.replace(/^docs\/features\//, 'Harness/features/');
|
|
28
|
-
if (file.startsWith('docs/workflows/')) return file.replace(/^docs\/workflows\//, 'Harness/workflows/');
|
|
29
|
-
if (file.startsWith('docs/tasks/')) return file.replace(/^docs\/tasks\//, 'Harness/tasks/');
|
|
30
|
-
return file;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Replace {{vars}} in file contents.
|
|
35
|
-
*/
|
|
36
|
-
function renderTemplate(src, vars) {
|
|
37
|
-
let content = fs.readFileSync(src, 'utf-8');
|
|
38
|
-
|
|
39
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
40
|
-
content = content.replaceAll(`{{${key}}}`, value);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return content;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Walk a directory and return relative paths of all files.
|
|
48
|
-
*/
|
|
49
|
-
function walkFiles(dir, base = dir) {
|
|
50
|
-
const results = [];
|
|
51
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
52
|
-
|
|
53
|
-
for (const entry of entries) {
|
|
54
|
-
const full = path.join(dir, entry.name);
|
|
55
|
-
if (entry.isDirectory()) {
|
|
56
|
-
results.push(...walkFiles(full, base));
|
|
57
|
-
} else {
|
|
58
|
-
results.push(path.relative(base, full));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return results;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function normalizePath(filePath) {
|
|
66
|
-
return filePath.replace(/\\/g, '/');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function getOptionalCatalog() {
|
|
70
|
-
if (!fs.existsSync(OPTIONAL_CATALOG)) {
|
|
71
|
-
return { skills: [], presets: {} };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return JSON.parse(fs.readFileSync(OPTIONAL_CATALOG, 'utf-8'));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function normalizeOptionList(value) {
|
|
78
|
-
const values = Array.isArray(value) ? value : [value];
|
|
79
|
-
return values
|
|
80
|
-
.filter(Boolean)
|
|
81
|
-
.flatMap(item => String(item).split(','))
|
|
82
|
-
.map(item => item.trim())
|
|
83
|
-
.filter(Boolean);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function resolveOptionalSelection({ withOptions = [], withoutOptions = [], preset = undefined }) {
|
|
87
|
-
const catalog = getOptionalCatalog();
|
|
88
|
-
const skillsById = new Map(catalog.skills.map(skill => [skill.id, skill]));
|
|
89
|
-
const selected = [];
|
|
90
|
-
const errors = [];
|
|
91
|
-
|
|
92
|
-
if (preset) {
|
|
93
|
-
if (!catalog.presets[preset]) {
|
|
94
|
-
errors.push(`Unknown preset "${preset}". Run --list-options to see available presets.`);
|
|
95
|
-
} else {
|
|
96
|
-
selected.push(...catalog.presets[preset]);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
selected.push(...normalizeOptionList(withOptions));
|
|
101
|
-
|
|
102
|
-
const ids = [...new Set(selected)];
|
|
103
|
-
const withoutIds = [...new Set(normalizeOptionList(withoutOptions))];
|
|
104
|
-
for (const id of ids) {
|
|
105
|
-
if (!skillsById.has(id)) {
|
|
106
|
-
errors.push(`Unknown optional skill "${id}". Run --list-options to see available options.`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
for (const id of withoutIds) {
|
|
110
|
-
if (!skillsById.has(id)) {
|
|
111
|
-
errors.push(`Unknown optional skill in --without "${id}". Run --list-options to see available options.`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const withoutSet = new Set(withoutIds);
|
|
116
|
-
const selectedIds = ids.filter(id => !withoutSet.has(id));
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
catalog,
|
|
120
|
-
selectedSkills: errors.length ? [] : selectedIds.map(id => skillsById.get(id)),
|
|
121
|
-
errors,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function createCoreFileSpecs() {
|
|
126
|
-
const specs = walkFiles(TEMPLATES_DIR)
|
|
127
|
-
.map(file => normalizePath(file))
|
|
128
|
-
.sort()
|
|
129
|
-
.map(file => ({
|
|
130
|
-
dest: harnessDest(file),
|
|
131
|
-
src: path.join(TEMPLATES_DIR, ...file.split('/')),
|
|
132
|
-
type: 'common',
|
|
133
|
-
}));
|
|
134
|
-
|
|
135
|
-
specs.push({ dest: 'tests/.gitkeep', src: undefined, type: 'empty' });
|
|
136
|
-
return specs;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function createOptionalFileSpecs(selectedSkills) {
|
|
140
|
-
const specs = [];
|
|
141
|
-
|
|
142
|
-
for (const skill of selectedSkills) {
|
|
143
|
-
for (const fileRoot of skill.files) {
|
|
144
|
-
const absRoot = path.join(OPTIONAL_DIR, ...fileRoot.split('/'));
|
|
145
|
-
for (const file of walkFiles(absRoot).map(normalizePath).sort()) {
|
|
146
|
-
specs.push({
|
|
147
|
-
dest: harnessDest(file),
|
|
148
|
-
src: path.join(absRoot, ...file.split('/')),
|
|
149
|
-
type: 'optional',
|
|
150
|
-
skillId: skill.id,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return specs;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function duplicateDests(fileSpecs) {
|
|
160
|
-
const seen = new Set();
|
|
161
|
-
const duplicates = new Set();
|
|
162
|
-
|
|
163
|
-
for (const spec of fileSpecs) {
|
|
164
|
-
if (seen.has(spec.dest)) {
|
|
165
|
-
duplicates.add(spec.dest);
|
|
166
|
-
} else {
|
|
167
|
-
seen.add(spec.dest);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return [...duplicates].sort();
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function createPlan(resolvedDir, fileSpecs) {
|
|
175
|
-
const plan = {
|
|
176
|
-
create: [],
|
|
177
|
-
skip: [],
|
|
178
|
-
overwrite: [],
|
|
179
|
-
backup: [],
|
|
180
|
-
conflict: [],
|
|
181
|
-
mkdir: [],
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const fileSet = new Set(fileSpecs.map(spec => spec.dest));
|
|
185
|
-
|
|
186
|
-
const dirSet = new Set(['.']);
|
|
187
|
-
for (const file of fileSet) {
|
|
188
|
-
let dir = path.posix.dirname(file);
|
|
189
|
-
while (dir && dir !== '.') {
|
|
190
|
-
dirSet.add(dir);
|
|
191
|
-
dir = path.posix.dirname(dir);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
for (const dir of EMPTY_DIRS) {
|
|
195
|
-
dirSet.add(dir);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
for (const dir of [...dirSet].sort()) {
|
|
199
|
-
const absDir = dir === '.'
|
|
200
|
-
? resolvedDir
|
|
201
|
-
: path.join(resolvedDir, ...dir.split('/'));
|
|
202
|
-
if (fs.existsSync(absDir)) {
|
|
203
|
-
if (!fs.statSync(absDir).isDirectory()) {
|
|
204
|
-
plan.conflict.push(dir === '.' ? './' : `${dir}/`);
|
|
205
|
-
}
|
|
206
|
-
} else {
|
|
207
|
-
plan.mkdir.push(dir === '.' ? './' : `${dir}/`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return plan;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function addFileActions(plan, fileSpecs, resolvedDir, onConflict) {
|
|
215
|
-
for (const { dest: file } of fileSpecs) {
|
|
216
|
-
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
217
|
-
|
|
218
|
-
if (!fs.existsSync(destPath)) {
|
|
219
|
-
plan.create.push(file);
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (fs.statSync(destPath).isDirectory()) {
|
|
224
|
-
if (onConflict === 'skip') {
|
|
225
|
-
plan.skip.push(file);
|
|
226
|
-
} else {
|
|
227
|
-
plan.conflict.push(file);
|
|
228
|
-
}
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (onConflict === 'skip') {
|
|
233
|
-
plan.skip.push(file);
|
|
234
|
-
} else if (onConflict === 'backup') {
|
|
235
|
-
plan.backup.push(file);
|
|
236
|
-
} else if (onConflict === 'overwrite') {
|
|
237
|
-
plan.overwrite.push(file);
|
|
238
|
-
} else {
|
|
239
|
-
plan.conflict.push(file);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function createSummary(plan, { noWrites = false } = {}) {
|
|
245
|
-
return {
|
|
246
|
-
created: noWrites ? 0 : plan.create.length,
|
|
247
|
-
skipped: noWrites ? 0 : plan.skip.length,
|
|
248
|
-
overwritten: noWrites ? 0 : plan.overwrite.length,
|
|
249
|
-
backedUp: noWrites ? 0 : plan.backup.length,
|
|
250
|
-
conflicts: plan.conflict.length,
|
|
251
|
-
mkdir: noWrites ? 0 : plan.mkdir.length,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function nextBackupPath(destPath) {
|
|
256
|
-
const first = `${destPath}.harness-backup`;
|
|
257
|
-
if (!fs.existsSync(first)) {
|
|
258
|
-
return first;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
for (let index = 1; ; index += 1) {
|
|
262
|
-
const candidate = `${first}.${index}`;
|
|
263
|
-
if (!fs.existsSync(candidate)) {
|
|
264
|
-
return candidate;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function registerOptionalContent(file, content, selectedSkills) {
|
|
270
|
-
if (!selectedSkills.length) return content;
|
|
271
|
-
|
|
272
|
-
if (file === 'Harness/MEMORY.md') {
|
|
273
|
-
const lines = selectedSkills.map(skill => (
|
|
274
|
-
`- [${skill.id}](../.claude/skills/${skill.id}/SKILL.md) - ${skill.description} Workflow: [workflows/${skill.id}.md](workflows/${skill.id}.md)`
|
|
275
|
-
));
|
|
276
|
-
return content.replace(
|
|
277
|
-
'Stack-specific skills can be added after the product shape is known.',
|
|
278
|
-
`Installed optional skills:\n\n${lines.join('\n')}\n\nStack-specific skills can be added after the product shape is known.`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (file === 'Harness/README.md') {
|
|
283
|
-
const lines = selectedSkills.map(skill => (
|
|
284
|
-
`- [${skill.title}](workflows/${skill.id}.md) - ${skill.description}`
|
|
285
|
-
));
|
|
286
|
-
return `${content.trimEnd()}\n\n## Installed Optional Workflows\n\n${lines.join('\n')}\n`;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return content;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function registrationWarnings(plan, selectedSkills) {
|
|
293
|
-
const warnings = [];
|
|
294
|
-
|
|
295
|
-
if (selectedSkills.length) {
|
|
296
|
-
if (plan.skip.includes('Harness/MEMORY.md')) {
|
|
297
|
-
warnings.push('Harness/MEMORY.md was skipped; manually register selected optional skills under #Skills.');
|
|
298
|
-
}
|
|
299
|
-
if (plan.skip.includes('Harness/README.md')) {
|
|
300
|
-
warnings.push('Harness/README.md was skipped; manually register selected optional workflow paths.');
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (plan.skip.includes('README.md')) {
|
|
305
|
-
warnings.push('README.md was skipped; keep project run, build, test, and git conventions there, not in CLAUDE.md.');
|
|
306
|
-
}
|
|
307
|
-
if (plan.skip.includes('CLAUDE.md') || plan.conflict.includes('CLAUDE.md')) {
|
|
308
|
-
warnings.push('CLAUDE.md already exists; ask the user to confirm refactoring or merging it with the Harness root-entry contract before editing.');
|
|
309
|
-
}
|
|
310
|
-
if (plan.skip.includes('AGENTS.md')) {
|
|
311
|
-
warnings.push('AGENTS.md was skipped; ask for user consent before merging or replacing the project agent entry contract.');
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return warnings;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export function generate({
|
|
318
|
-
projectName,
|
|
319
|
-
targetDir,
|
|
320
|
-
dryRun = false,
|
|
321
|
-
onConflict = 'fail',
|
|
322
|
-
withOptions = [],
|
|
323
|
-
withoutOptions = [],
|
|
324
|
-
preset = undefined,
|
|
325
|
-
}) {
|
|
326
|
-
const created = [];
|
|
327
|
-
const errors = [];
|
|
328
|
-
const warnings = [];
|
|
329
|
-
|
|
330
|
-
// Resolve targetDir relative to cwd
|
|
331
|
-
const resolvedDir = path.resolve(process.cwd(), targetDir);
|
|
332
|
-
const vars = {
|
|
333
|
-
projectName,
|
|
334
|
-
generatorVersion: pkg.version,
|
|
335
|
-
generatedTimestamp: new Date().toISOString(),
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
const optional = resolveOptionalSelection({ withOptions, withoutOptions, preset });
|
|
339
|
-
const fileSpecs = [
|
|
340
|
-
...createCoreFileSpecs(),
|
|
341
|
-
...createOptionalFileSpecs(optional.selectedSkills),
|
|
342
|
-
];
|
|
343
|
-
const specsByDest = new Map(fileSpecs.map(spec => [spec.dest, spec]));
|
|
344
|
-
const plan = createPlan(resolvedDir, fileSpecs);
|
|
345
|
-
const duplicateDestinations = duplicateDests(fileSpecs);
|
|
346
|
-
|
|
347
|
-
if (!VALID_CONFLICT_POLICIES.has(onConflict)) {
|
|
348
|
-
errors.push(`Unknown conflict policy "${onConflict}". Use fail, skip, backup, or overwrite.`);
|
|
349
|
-
return {
|
|
350
|
-
success: false,
|
|
351
|
-
created,
|
|
352
|
-
errors,
|
|
353
|
-
plan,
|
|
354
|
-
summary: createSummary(plan, { noWrites: true }),
|
|
355
|
-
warnings,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (optional.errors.length > 0) {
|
|
360
|
-
errors.push(...optional.errors);
|
|
361
|
-
return {
|
|
362
|
-
success: false,
|
|
363
|
-
created,
|
|
364
|
-
errors,
|
|
365
|
-
plan,
|
|
366
|
-
summary: createSummary(plan, { noWrites: true }),
|
|
367
|
-
warnings,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (duplicateDestinations.length > 0) {
|
|
372
|
-
errors.push(`Generation stopped because duplicate template destination(s) were found: ${duplicateDestinations.join(', ')}`);
|
|
373
|
-
return {
|
|
374
|
-
success: false,
|
|
375
|
-
created,
|
|
376
|
-
errors,
|
|
377
|
-
plan,
|
|
378
|
-
summary: createSummary(plan, { noWrites: true }),
|
|
379
|
-
warnings,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
addFileActions(plan, fileSpecs, resolvedDir, onConflict);
|
|
384
|
-
warnings.push(...registrationWarnings(plan, optional.selectedSkills));
|
|
385
|
-
|
|
386
|
-
if (dryRun) {
|
|
387
|
-
return {
|
|
388
|
-
success: true,
|
|
389
|
-
created,
|
|
390
|
-
errors,
|
|
391
|
-
plan,
|
|
392
|
-
summary: createSummary(plan),
|
|
393
|
-
warnings,
|
|
394
|
-
dryRun: true,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (plan.conflict.length > 0) {
|
|
399
|
-
errors.push(`Generation stopped because ${plan.conflict.length} file conflict(s) were found: ${plan.conflict.join(', ')}`);
|
|
400
|
-
if (plan.conflict.includes('CLAUDE.md')) {
|
|
401
|
-
errors.push('CLAUDE.md already exists. It is the root agent entry contract; ask the user to confirm refactoring or merging it with the Harness entry contract before editing, backing up, or overwriting.');
|
|
402
|
-
}
|
|
403
|
-
return {
|
|
404
|
-
success: false,
|
|
405
|
-
created,
|
|
406
|
-
errors,
|
|
407
|
-
plan,
|
|
408
|
-
summary: createSummary(plan, { noWrites: true }),
|
|
409
|
-
warnings,
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
try {
|
|
414
|
-
for (const dir of plan.mkdir) {
|
|
415
|
-
const dirPath = dir === './'
|
|
416
|
-
? resolvedDir
|
|
417
|
-
: path.join(resolvedDir, ...dir.slice(0, -1).split('/'));
|
|
418
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
419
|
-
created.push(dir === './' ? './ (directory)' : `${dir} (directory)`);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
for (const file of plan.backup) {
|
|
423
|
-
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
424
|
-
fs.renameSync(destPath, nextBackupPath(destPath));
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const writableFiles = [
|
|
428
|
-
...plan.create,
|
|
429
|
-
...plan.overwrite,
|
|
430
|
-
...plan.backup,
|
|
431
|
-
];
|
|
432
|
-
|
|
433
|
-
for (const file of writableFiles) {
|
|
434
|
-
const spec = specsByDest.get(file);
|
|
435
|
-
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
436
|
-
let content = spec.type === 'empty'
|
|
437
|
-
? ''
|
|
438
|
-
: renderTemplate(spec.src, vars);
|
|
439
|
-
content = registerOptionalContent(file, content, optional.selectedSkills);
|
|
440
|
-
|
|
441
|
-
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
442
|
-
fs.writeFileSync(destPath, content, 'utf-8');
|
|
443
|
-
created.push(file);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
return {
|
|
447
|
-
success: true,
|
|
448
|
-
created,
|
|
449
|
-
errors,
|
|
450
|
-
plan,
|
|
451
|
-
summary: createSummary(plan),
|
|
452
|
-
warnings,
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
} catch (err) {
|
|
456
|
-
errors.push(err.message);
|
|
457
|
-
return {
|
|
458
|
-
success: false,
|
|
459
|
-
created,
|
|
460
|
-
errors,
|
|
461
|
-
plan,
|
|
462
|
-
summary: createSummary(plan),
|
|
463
|
-
warnings,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8'));
|
|
8
|
+
const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates', 'common');
|
|
9
|
+
const OPTIONAL_DIR = path.resolve(__dirname, '..', 'templates', 'optional');
|
|
10
|
+
const OPTIONAL_CATALOG = path.join(OPTIONAL_DIR, 'catalog.json');
|
|
11
|
+
const VALID_CONFLICT_POLICIES = new Set(['fail', 'skip', 'backup', 'overwrite']);
|
|
12
|
+
const EMPTY_DIRS = [
|
|
13
|
+
'.claude/hooks',
|
|
14
|
+
'tests',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function harnessDest(file) {
|
|
18
|
+
if (file === '.harness-version') return 'Harness/.harness-version';
|
|
19
|
+
if (file === 'SETUP.md') return 'Harness/SETUP.md';
|
|
20
|
+
if (file === 'MEMORY.md') return 'Harness/MEMORY.md';
|
|
21
|
+
if (file === 'scripts/validate-harness.mjs') return 'Harness/scripts/validate-harness.mjs';
|
|
22
|
+
if (file.startsWith('memory/')) return `Harness/${file}`;
|
|
23
|
+
if (file === 'docs/README.md') return 'Harness/README.md';
|
|
24
|
+
if (file.startsWith('docs/harness/')) return file.replace(/^docs\/harness\//, 'Harness/');
|
|
25
|
+
if (file.startsWith('docs/research/')) return file.replace(/^docs\/research\//, 'Harness/research/');
|
|
26
|
+
if (file.startsWith('docs/domain/')) return file.replace(/^docs\/domain\//, 'Harness/domain/');
|
|
27
|
+
if (file.startsWith('docs/features/')) return file.replace(/^docs\/features\//, 'Harness/features/');
|
|
28
|
+
if (file.startsWith('docs/workflows/')) return file.replace(/^docs\/workflows\//, 'Harness/workflows/');
|
|
29
|
+
if (file.startsWith('docs/tasks/')) return file.replace(/^docs\/tasks\//, 'Harness/tasks/');
|
|
30
|
+
return file;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Replace {{vars}} in file contents.
|
|
35
|
+
*/
|
|
36
|
+
function renderTemplate(src, vars) {
|
|
37
|
+
let content = fs.readFileSync(src, 'utf-8');
|
|
38
|
+
|
|
39
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
40
|
+
content = content.replaceAll(`{{${key}}}`, value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return content;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Walk a directory and return relative paths of all files.
|
|
48
|
+
*/
|
|
49
|
+
function walkFiles(dir, base = dir) {
|
|
50
|
+
const results = [];
|
|
51
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
52
|
+
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const full = path.join(dir, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
results.push(...walkFiles(full, base));
|
|
57
|
+
} else {
|
|
58
|
+
results.push(path.relative(base, full));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizePath(filePath) {
|
|
66
|
+
return filePath.replace(/\\/g, '/');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getOptionalCatalog() {
|
|
70
|
+
if (!fs.existsSync(OPTIONAL_CATALOG)) {
|
|
71
|
+
return { skills: [], presets: {} };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return JSON.parse(fs.readFileSync(OPTIONAL_CATALOG, 'utf-8'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeOptionList(value) {
|
|
78
|
+
const values = Array.isArray(value) ? value : [value];
|
|
79
|
+
return values
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.flatMap(item => String(item).split(','))
|
|
82
|
+
.map(item => item.trim())
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveOptionalSelection({ withOptions = [], withoutOptions = [], preset = undefined }) {
|
|
87
|
+
const catalog = getOptionalCatalog();
|
|
88
|
+
const skillsById = new Map(catalog.skills.map(skill => [skill.id, skill]));
|
|
89
|
+
const selected = [];
|
|
90
|
+
const errors = [];
|
|
91
|
+
|
|
92
|
+
if (preset) {
|
|
93
|
+
if (!catalog.presets[preset]) {
|
|
94
|
+
errors.push(`Unknown preset "${preset}". Run --list-options to see available presets.`);
|
|
95
|
+
} else {
|
|
96
|
+
selected.push(...catalog.presets[preset]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
selected.push(...normalizeOptionList(withOptions));
|
|
101
|
+
|
|
102
|
+
const ids = [...new Set(selected)];
|
|
103
|
+
const withoutIds = [...new Set(normalizeOptionList(withoutOptions))];
|
|
104
|
+
for (const id of ids) {
|
|
105
|
+
if (!skillsById.has(id)) {
|
|
106
|
+
errors.push(`Unknown optional skill "${id}". Run --list-options to see available options.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const id of withoutIds) {
|
|
110
|
+
if (!skillsById.has(id)) {
|
|
111
|
+
errors.push(`Unknown optional skill in --without "${id}". Run --list-options to see available options.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const withoutSet = new Set(withoutIds);
|
|
116
|
+
const selectedIds = ids.filter(id => !withoutSet.has(id));
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
catalog,
|
|
120
|
+
selectedSkills: errors.length ? [] : selectedIds.map(id => skillsById.get(id)),
|
|
121
|
+
errors,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function createCoreFileSpecs() {
|
|
126
|
+
const specs = walkFiles(TEMPLATES_DIR)
|
|
127
|
+
.map(file => normalizePath(file))
|
|
128
|
+
.sort()
|
|
129
|
+
.map(file => ({
|
|
130
|
+
dest: harnessDest(file),
|
|
131
|
+
src: path.join(TEMPLATES_DIR, ...file.split('/')),
|
|
132
|
+
type: 'common',
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
specs.push({ dest: 'tests/.gitkeep', src: undefined, type: 'empty' });
|
|
136
|
+
return specs;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createOptionalFileSpecs(selectedSkills) {
|
|
140
|
+
const specs = [];
|
|
141
|
+
|
|
142
|
+
for (const skill of selectedSkills) {
|
|
143
|
+
for (const fileRoot of skill.files) {
|
|
144
|
+
const absRoot = path.join(OPTIONAL_DIR, ...fileRoot.split('/'));
|
|
145
|
+
for (const file of walkFiles(absRoot).map(normalizePath).sort()) {
|
|
146
|
+
specs.push({
|
|
147
|
+
dest: harnessDest(file),
|
|
148
|
+
src: path.join(absRoot, ...file.split('/')),
|
|
149
|
+
type: 'optional',
|
|
150
|
+
skillId: skill.id,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return specs;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function duplicateDests(fileSpecs) {
|
|
160
|
+
const seen = new Set();
|
|
161
|
+
const duplicates = new Set();
|
|
162
|
+
|
|
163
|
+
for (const spec of fileSpecs) {
|
|
164
|
+
if (seen.has(spec.dest)) {
|
|
165
|
+
duplicates.add(spec.dest);
|
|
166
|
+
} else {
|
|
167
|
+
seen.add(spec.dest);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return [...duplicates].sort();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function createPlan(resolvedDir, fileSpecs) {
|
|
175
|
+
const plan = {
|
|
176
|
+
create: [],
|
|
177
|
+
skip: [],
|
|
178
|
+
overwrite: [],
|
|
179
|
+
backup: [],
|
|
180
|
+
conflict: [],
|
|
181
|
+
mkdir: [],
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const fileSet = new Set(fileSpecs.map(spec => spec.dest));
|
|
185
|
+
|
|
186
|
+
const dirSet = new Set(['.']);
|
|
187
|
+
for (const file of fileSet) {
|
|
188
|
+
let dir = path.posix.dirname(file);
|
|
189
|
+
while (dir && dir !== '.') {
|
|
190
|
+
dirSet.add(dir);
|
|
191
|
+
dir = path.posix.dirname(dir);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const dir of EMPTY_DIRS) {
|
|
195
|
+
dirSet.add(dir);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const dir of [...dirSet].sort()) {
|
|
199
|
+
const absDir = dir === '.'
|
|
200
|
+
? resolvedDir
|
|
201
|
+
: path.join(resolvedDir, ...dir.split('/'));
|
|
202
|
+
if (fs.existsSync(absDir)) {
|
|
203
|
+
if (!fs.statSync(absDir).isDirectory()) {
|
|
204
|
+
plan.conflict.push(dir === '.' ? './' : `${dir}/`);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
plan.mkdir.push(dir === '.' ? './' : `${dir}/`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return plan;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function addFileActions(plan, fileSpecs, resolvedDir, onConflict) {
|
|
215
|
+
for (const { dest: file } of fileSpecs) {
|
|
216
|
+
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
217
|
+
|
|
218
|
+
if (!fs.existsSync(destPath)) {
|
|
219
|
+
plan.create.push(file);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (fs.statSync(destPath).isDirectory()) {
|
|
224
|
+
if (onConflict === 'skip') {
|
|
225
|
+
plan.skip.push(file);
|
|
226
|
+
} else {
|
|
227
|
+
plan.conflict.push(file);
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (onConflict === 'skip') {
|
|
233
|
+
plan.skip.push(file);
|
|
234
|
+
} else if (onConflict === 'backup') {
|
|
235
|
+
plan.backup.push(file);
|
|
236
|
+
} else if (onConflict === 'overwrite') {
|
|
237
|
+
plan.overwrite.push(file);
|
|
238
|
+
} else {
|
|
239
|
+
plan.conflict.push(file);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function createSummary(plan, { noWrites = false } = {}) {
|
|
245
|
+
return {
|
|
246
|
+
created: noWrites ? 0 : plan.create.length,
|
|
247
|
+
skipped: noWrites ? 0 : plan.skip.length,
|
|
248
|
+
overwritten: noWrites ? 0 : plan.overwrite.length,
|
|
249
|
+
backedUp: noWrites ? 0 : plan.backup.length,
|
|
250
|
+
conflicts: plan.conflict.length,
|
|
251
|
+
mkdir: noWrites ? 0 : plan.mkdir.length,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function nextBackupPath(destPath) {
|
|
256
|
+
const first = `${destPath}.harness-backup`;
|
|
257
|
+
if (!fs.existsSync(first)) {
|
|
258
|
+
return first;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (let index = 1; ; index += 1) {
|
|
262
|
+
const candidate = `${first}.${index}`;
|
|
263
|
+
if (!fs.existsSync(candidate)) {
|
|
264
|
+
return candidate;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function registerOptionalContent(file, content, selectedSkills) {
|
|
270
|
+
if (!selectedSkills.length) return content;
|
|
271
|
+
|
|
272
|
+
if (file === 'Harness/MEMORY.md') {
|
|
273
|
+
const lines = selectedSkills.map(skill => (
|
|
274
|
+
`- [${skill.id}](../.claude/skills/${skill.id}/SKILL.md) - ${skill.description} Workflow: [workflows/${skill.id}.md](workflows/${skill.id}.md)`
|
|
275
|
+
));
|
|
276
|
+
return content.replace(
|
|
277
|
+
'Stack-specific skills can be added after the product shape is known.',
|
|
278
|
+
`Installed optional skills:\n\n${lines.join('\n')}\n\nStack-specific skills can be added after the product shape is known.`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (file === 'Harness/README.md') {
|
|
283
|
+
const lines = selectedSkills.map(skill => (
|
|
284
|
+
`- [${skill.title}](workflows/${skill.id}.md) - ${skill.description}`
|
|
285
|
+
));
|
|
286
|
+
return `${content.trimEnd()}\n\n## Installed Optional Workflows\n\n${lines.join('\n')}\n`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return content;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function registrationWarnings(plan, selectedSkills) {
|
|
293
|
+
const warnings = [];
|
|
294
|
+
|
|
295
|
+
if (selectedSkills.length) {
|
|
296
|
+
if (plan.skip.includes('Harness/MEMORY.md')) {
|
|
297
|
+
warnings.push('Harness/MEMORY.md was skipped; manually register selected optional skills under #Skills.');
|
|
298
|
+
}
|
|
299
|
+
if (plan.skip.includes('Harness/README.md')) {
|
|
300
|
+
warnings.push('Harness/README.md was skipped; manually register selected optional workflow paths.');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (plan.skip.includes('README.md')) {
|
|
305
|
+
warnings.push('README.md was skipped; keep project run, build, test, and git conventions there, not in CLAUDE.md.');
|
|
306
|
+
}
|
|
307
|
+
if (plan.skip.includes('CLAUDE.md') || plan.conflict.includes('CLAUDE.md')) {
|
|
308
|
+
warnings.push('CLAUDE.md already exists; ask the user to confirm refactoring or merging it with the Harness root-entry contract before editing.');
|
|
309
|
+
}
|
|
310
|
+
if (plan.skip.includes('AGENTS.md')) {
|
|
311
|
+
warnings.push('AGENTS.md was skipped; ask for user consent before merging or replacing the project agent entry contract.');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return warnings;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function generate({
|
|
318
|
+
projectName,
|
|
319
|
+
targetDir,
|
|
320
|
+
dryRun = false,
|
|
321
|
+
onConflict = 'fail',
|
|
322
|
+
withOptions = [],
|
|
323
|
+
withoutOptions = [],
|
|
324
|
+
preset = undefined,
|
|
325
|
+
}) {
|
|
326
|
+
const created = [];
|
|
327
|
+
const errors = [];
|
|
328
|
+
const warnings = [];
|
|
329
|
+
|
|
330
|
+
// Resolve targetDir relative to cwd
|
|
331
|
+
const resolvedDir = path.resolve(process.cwd(), targetDir);
|
|
332
|
+
const vars = {
|
|
333
|
+
projectName,
|
|
334
|
+
generatorVersion: pkg.version,
|
|
335
|
+
generatedTimestamp: new Date().toISOString(),
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const optional = resolveOptionalSelection({ withOptions, withoutOptions, preset });
|
|
339
|
+
const fileSpecs = [
|
|
340
|
+
...createCoreFileSpecs(),
|
|
341
|
+
...createOptionalFileSpecs(optional.selectedSkills),
|
|
342
|
+
];
|
|
343
|
+
const specsByDest = new Map(fileSpecs.map(spec => [spec.dest, spec]));
|
|
344
|
+
const plan = createPlan(resolvedDir, fileSpecs);
|
|
345
|
+
const duplicateDestinations = duplicateDests(fileSpecs);
|
|
346
|
+
|
|
347
|
+
if (!VALID_CONFLICT_POLICIES.has(onConflict)) {
|
|
348
|
+
errors.push(`Unknown conflict policy "${onConflict}". Use fail, skip, backup, or overwrite.`);
|
|
349
|
+
return {
|
|
350
|
+
success: false,
|
|
351
|
+
created,
|
|
352
|
+
errors,
|
|
353
|
+
plan,
|
|
354
|
+
summary: createSummary(plan, { noWrites: true }),
|
|
355
|
+
warnings,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (optional.errors.length > 0) {
|
|
360
|
+
errors.push(...optional.errors);
|
|
361
|
+
return {
|
|
362
|
+
success: false,
|
|
363
|
+
created,
|
|
364
|
+
errors,
|
|
365
|
+
plan,
|
|
366
|
+
summary: createSummary(plan, { noWrites: true }),
|
|
367
|
+
warnings,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (duplicateDestinations.length > 0) {
|
|
372
|
+
errors.push(`Generation stopped because duplicate template destination(s) were found: ${duplicateDestinations.join(', ')}`);
|
|
373
|
+
return {
|
|
374
|
+
success: false,
|
|
375
|
+
created,
|
|
376
|
+
errors,
|
|
377
|
+
plan,
|
|
378
|
+
summary: createSummary(plan, { noWrites: true }),
|
|
379
|
+
warnings,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
addFileActions(plan, fileSpecs, resolvedDir, onConflict);
|
|
384
|
+
warnings.push(...registrationWarnings(plan, optional.selectedSkills));
|
|
385
|
+
|
|
386
|
+
if (dryRun) {
|
|
387
|
+
return {
|
|
388
|
+
success: true,
|
|
389
|
+
created,
|
|
390
|
+
errors,
|
|
391
|
+
plan,
|
|
392
|
+
summary: createSummary(plan),
|
|
393
|
+
warnings,
|
|
394
|
+
dryRun: true,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (plan.conflict.length > 0) {
|
|
399
|
+
errors.push(`Generation stopped because ${plan.conflict.length} file conflict(s) were found: ${plan.conflict.join(', ')}`);
|
|
400
|
+
if (plan.conflict.includes('CLAUDE.md')) {
|
|
401
|
+
errors.push('CLAUDE.md already exists. It is the root agent entry contract; ask the user to confirm refactoring or merging it with the Harness entry contract before editing, backing up, or overwriting.');
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
success: false,
|
|
405
|
+
created,
|
|
406
|
+
errors,
|
|
407
|
+
plan,
|
|
408
|
+
summary: createSummary(plan, { noWrites: true }),
|
|
409
|
+
warnings,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
for (const dir of plan.mkdir) {
|
|
415
|
+
const dirPath = dir === './'
|
|
416
|
+
? resolvedDir
|
|
417
|
+
: path.join(resolvedDir, ...dir.slice(0, -1).split('/'));
|
|
418
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
419
|
+
created.push(dir === './' ? './ (directory)' : `${dir} (directory)`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
for (const file of plan.backup) {
|
|
423
|
+
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
424
|
+
fs.renameSync(destPath, nextBackupPath(destPath));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const writableFiles = [
|
|
428
|
+
...plan.create,
|
|
429
|
+
...plan.overwrite,
|
|
430
|
+
...plan.backup,
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
for (const file of writableFiles) {
|
|
434
|
+
const spec = specsByDest.get(file);
|
|
435
|
+
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
436
|
+
let content = spec.type === 'empty'
|
|
437
|
+
? ''
|
|
438
|
+
: renderTemplate(spec.src, vars);
|
|
439
|
+
content = registerOptionalContent(file, content, optional.selectedSkills);
|
|
440
|
+
|
|
441
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
442
|
+
fs.writeFileSync(destPath, content, 'utf-8');
|
|
443
|
+
created.push(file);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
success: true,
|
|
448
|
+
created,
|
|
449
|
+
errors,
|
|
450
|
+
plan,
|
|
451
|
+
summary: createSummary(plan),
|
|
452
|
+
warnings,
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
} catch (err) {
|
|
456
|
+
errors.push(err.message);
|
|
457
|
+
return {
|
|
458
|
+
success: false,
|
|
459
|
+
created,
|
|
460
|
+
errors,
|
|
461
|
+
plan,
|
|
462
|
+
summary: createSummary(plan),
|
|
463
|
+
warnings,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|