create-harness-vibe-coding 0.1.8 → 0.1.10
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 +95 -1
- package/package.json +5 -2
- package/src/generator.js +347 -53
- package/src/index.js +212 -17
- package/templates/common/.claude/rules/ecc/common.md +9 -0
- package/templates/common/CLAUDE.md +5 -3
- package/templates/common/MEMORY.md +12 -8
- package/templates/common/SETUP.md +87 -2
- package/templates/common/docs/README.md +8 -0
- package/templates/common/docs/features/_template.md +24 -1
- package/templates/common/docs/harness/agent-workflow.md +3 -7
- package/templates/common/docs/harness/context-loading.md +3 -0
- package/templates/common/docs/harness/dispatch.md +3 -0
- package/templates/common/docs/harness/extension.md +12 -0
- package/templates/common/memory/agent-lessons-patterns.md +21 -0
- package/templates/common/memory/tool-usage-reflections.md +21 -0
- package/templates/common/memory/user-corrections-preferences.md +21 -0
- package/templates/common/scripts/validate-harness.mjs +99 -3
- package/templates/optional/catalog.json +43 -0
- package/templates/optional/skills/browser-e2e/.claude/skills/browser-e2e/SKILL.md +42 -0
- package/templates/optional/skills/browser-e2e/docs/workflows/browser-e2e.md +42 -0
- package/templates/optional/skills/github-pr-review/.claude/skills/github-pr-review/SKILL.md +40 -0
- package/templates/optional/skills/github-pr-review/docs/workflows/github-pr-review.md +28 -0
- package/templates/optional/skills/python-backend/.claude/skills/python-backend/SKILL.md +40 -0
- package/templates/optional/skills/python-backend/docs/workflows/python-backend.md +34 -0
- package/templates/optional/skills/ts-react-frontend/.claude/skills/ts-react-frontend/SKILL.md +43 -0
- package/templates/optional/skills/ts-react-frontend/docs/workflows/ts-react-frontend.md +35 -0
- package/templates/optional/skills/ui-ux-review/.claude/skills/ui-ux-review/SKILL.md +40 -0
- package/templates/optional/skills/ui-ux-review/docs/workflows/ui-ux-review.md +26 -0
package/README.md
CHANGED
|
@@ -130,6 +130,35 @@ my-project/
|
|
|
130
130
|
npx create-harness-vibe-coding@latest
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
### Existing Project
|
|
134
|
+
|
|
135
|
+
The scaffold is designed to be added to an existing repository without silently replacing project files.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Preview the write plan first. No files or directories are created.
|
|
139
|
+
npx create-harness-vibe-coding@latest my-app . -y --dry-run
|
|
140
|
+
|
|
141
|
+
# Preserve existing files and add only missing harness files.
|
|
142
|
+
npx create-harness-vibe-coding@latest my-app . -y --on-conflict skip
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
By default, conflicts fail before writing. This protects existing `CLAUDE.md`, `AGENTS.md`, `.claude/`, `.gitignore`, docs, and scripts from accidental replacement.
|
|
146
|
+
|
|
147
|
+
| Conflict mode | Meaning | Risk |
|
|
148
|
+
|---------------|---------|------|
|
|
149
|
+
| `fail` | Default. Stop if a target file already exists. | Safest for existing projects; requires a follow-up decision. |
|
|
150
|
+
| `skip` | Keep existing files and create only missing files. | Existing docs may need manual links to new harness docs or workflows. |
|
|
151
|
+
| `backup` | Rename the existing file to `<name>.harness-backup`, then write the scaffold file. | Review backups before deleting; repeated runs may need cleanup. |
|
|
152
|
+
| `overwrite` | Replace existing files with scaffold versions. | Destructive. Use only after reviewing `--dry-run` output or with explicit approval. |
|
|
153
|
+
|
|
154
|
+
Recommended bootstrap for agents:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
node bin/create-harness-vibe-coding.js my-app . -y --dry-run
|
|
158
|
+
node bin/create-harness-vibe-coding.js my-app . -y --on-conflict skip
|
|
159
|
+
node scripts/validate-harness.mjs
|
|
160
|
+
```
|
|
161
|
+
|
|
133
162
|
### Agent / CI/CD
|
|
134
163
|
|
|
135
164
|
Agents and automation can skip all prompts with `-y`:
|
|
@@ -143,16 +172,81 @@ npx create-harness-vibe-coding@latest my-app -y
|
|
|
143
172
|
|
|
144
173
|
# Named project, explicit directory
|
|
145
174
|
npx create-harness-vibe-coding@latest my-app ./dist/my-app -y
|
|
175
|
+
|
|
176
|
+
# CI-safe existing-project preview
|
|
177
|
+
npx create-harness-vibe-coding@latest my-app . -y --dry-run
|
|
178
|
+
|
|
179
|
+
# CI-safe existing-project add without replacing files
|
|
180
|
+
npx create-harness-vibe-coding@latest my-app . -y --on-conflict skip
|
|
146
181
|
```
|
|
147
182
|
|
|
148
183
|
| Flag | Purpose |
|
|
149
184
|
|------|---------|
|
|
150
185
|
| `-y`, `--yes` | Skip all prompts. Uses positional args or defaults. |
|
|
186
|
+
| `--dry-run` | Print the planned creates, skips, backups, overwrites, and conflicts without writing. |
|
|
187
|
+
| `--on-conflict <mode>` | Choose `fail`, `skip`, `backup`, or `overwrite` when files already exist. |
|
|
188
|
+
| `--list-options` | Print the optional workflow catalog and presets. |
|
|
189
|
+
| `--with <ids>` | Add optional workflows by comma-separated id. |
|
|
190
|
+
| `--without <ids>` | Remove optional workflows selected by `--preset` or `--with`. |
|
|
191
|
+
| `--preset <name>` | Add a named workflow preset such as `web-app` or `fullstack`. |
|
|
151
192
|
| `-h`, `--help` | Print usage and exit. |
|
|
152
193
|
|
|
153
194
|
> [!TIP]
|
|
154
195
|
> Agents should always pass `-y` to avoid hanging on interactive prompts.
|
|
155
|
-
> If the agent needs to discover the CLI surface first, run with `--help`.
|
|
196
|
+
> If the agent needs to discover the CLI surface first, run with `--help` and `--list-options`.
|
|
197
|
+
|
|
198
|
+
### Optional Workflows
|
|
199
|
+
|
|
200
|
+
Optional workflows are local template assets selected explicitly at generation time. They do not install package dependencies or fetch a remote marketplace.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# Show available optional workflow ids and presets
|
|
204
|
+
npx create-harness-vibe-coding@latest --list-options
|
|
205
|
+
|
|
206
|
+
# Add individual workflows
|
|
207
|
+
npx create-harness-vibe-coding@latest my-app -y --with browser-e2e,ts-react-frontend
|
|
208
|
+
|
|
209
|
+
# Add a preset for common web app work
|
|
210
|
+
npx create-harness-vibe-coding@latest my-app -y --preset web-app
|
|
211
|
+
|
|
212
|
+
# Add a broader frontend/backend/PR-review preset
|
|
213
|
+
npx create-harness-vibe-coding@latest my-app -y --preset fullstack
|
|
214
|
+
|
|
215
|
+
# Trim a preset without restating every selected workflow
|
|
216
|
+
npx create-harness-vibe-coding@latest my-app -y --preset fullstack --without github-pr-review
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Built-in optional workflow ids:
|
|
220
|
+
|
|
221
|
+
| Workflow | Use when |
|
|
222
|
+
|----------|----------|
|
|
223
|
+
| `browser-e2e` | Browser smoke tests, screenshots, traces, and UI evidence. |
|
|
224
|
+
| `ui-ux-review` | Screenshot-driven responsive, accessibility, and polish review. |
|
|
225
|
+
| `github-pr-review` | PR diff, checks, review findings, and CI evidence. |
|
|
226
|
+
| `python-backend` | Python API/backend work with unittest or pytest verification. |
|
|
227
|
+
| `ts-react-frontend` | TypeScript React work with typecheck, component tests, build, and browser smoke. |
|
|
228
|
+
|
|
229
|
+
Presets:
|
|
230
|
+
|
|
231
|
+
| Preset | Includes |
|
|
232
|
+
|--------|----------|
|
|
233
|
+
| `web-app` | `ts-react-frontend`, `browser-e2e`, `ui-ux-review` |
|
|
234
|
+
| `fullstack` | `ts-react-frontend`, `python-backend`, `browser-e2e`, `github-pr-review` |
|
|
235
|
+
|
|
236
|
+
### Verification
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Run repository tests
|
|
240
|
+
npm test
|
|
241
|
+
|
|
242
|
+
# Confirm optional workflow catalog output
|
|
243
|
+
node bin/create-harness-vibe-coding.js --list-options
|
|
244
|
+
|
|
245
|
+
# After generating a project, validate the harness from that project root
|
|
246
|
+
node scripts/validate-harness.mjs
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The harness validator checks scaffold consistency. It is not a full React, Playwright, Chrome DevTools Protocol, or browser matrix test suite.
|
|
156
250
|
|
|
157
251
|
### After scaffolding, tell Claude:
|
|
158
252
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-harness-vibe-coding",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Scaffold a 0-1 product harness for AI-assisted research, PRD, planning, architecture, build, test, and feedback loops",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
"templates/"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"start": "node src/index.js"
|
|
15
|
+
"start": "node src/index.js",
|
|
16
|
+
"test": "node --test tests/*.test.js",
|
|
17
|
+
"test:smoke": "node --test tests/cli-smoke.test.js",
|
|
18
|
+
"pack:smoke": "node --test tests/pack-smoke.test.js"
|
|
16
19
|
},
|
|
17
20
|
"dependencies": {
|
|
18
21
|
"@clack/prompts": "^0.7.0",
|
package/src/generator.js
CHANGED
|
@@ -1,36 +1,28 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import pc from 'picocolors';
|
|
5
4
|
|
|
6
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
6
|
const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates', 'common');
|
|
7
|
+
const OPTIONAL_DIR = path.resolve(__dirname, '..', 'templates', 'optional');
|
|
8
|
+
const OPTIONAL_CATALOG = path.join(OPTIONAL_DIR, 'catalog.json');
|
|
9
|
+
const VALID_CONFLICT_POLICIES = new Set(['fail', 'skip', 'backup', 'overwrite']);
|
|
10
|
+
const EMPTY_DIRS = [
|
|
11
|
+
'.claude/hooks',
|
|
12
|
+
'tests',
|
|
13
|
+
];
|
|
8
14
|
|
|
9
15
|
/**
|
|
10
|
-
*
|
|
16
|
+
* Replace {{vars}} in file contents.
|
|
11
17
|
*/
|
|
12
|
-
function
|
|
13
|
-
fs.
|
|
18
|
+
function renderTemplate(src, vars) {
|
|
19
|
+
let content = fs.readFileSync(src, 'utf-8');
|
|
14
20
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
for (const entry of entries) {
|
|
18
|
-
const srcPath = path.join(src, entry.name);
|
|
19
|
-
const destPath = path.join(dest, entry.name);
|
|
20
|
-
|
|
21
|
-
if (entry.isDirectory()) {
|
|
22
|
-
copyDir(srcPath, destPath, vars);
|
|
23
|
-
} else {
|
|
24
|
-
let content = fs.readFileSync(srcPath, 'utf-8');
|
|
25
|
-
|
|
26
|
-
// Replace template variables
|
|
27
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
28
|
-
content = content.replaceAll(`{{${key}}}`, value);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
fs.writeFileSync(destPath, content, 'utf-8');
|
|
32
|
-
}
|
|
21
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
22
|
+
content = content.replaceAll(`{{${key}}}`, value);
|
|
33
23
|
}
|
|
24
|
+
|
|
25
|
+
return content;
|
|
34
26
|
}
|
|
35
27
|
|
|
36
28
|
/**
|
|
@@ -52,53 +44,355 @@ function walkFiles(dir, base = dir) {
|
|
|
52
44
|
return results;
|
|
53
45
|
}
|
|
54
46
|
|
|
55
|
-
|
|
47
|
+
function normalizePath(filePath) {
|
|
48
|
+
return filePath.replace(/\\/g, '/');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getOptionalCatalog() {
|
|
52
|
+
if (!fs.existsSync(OPTIONAL_CATALOG)) {
|
|
53
|
+
return { skills: [], presets: {} };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return JSON.parse(fs.readFileSync(OPTIONAL_CATALOG, 'utf-8'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeOptionList(value) {
|
|
60
|
+
const values = Array.isArray(value) ? value : [value];
|
|
61
|
+
return values
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.flatMap(item => String(item).split(','))
|
|
64
|
+
.map(item => item.trim())
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveOptionalSelection({ withOptions = [], withoutOptions = [], preset = undefined }) {
|
|
69
|
+
const catalog = getOptionalCatalog();
|
|
70
|
+
const skillsById = new Map(catalog.skills.map(skill => [skill.id, skill]));
|
|
71
|
+
const selected = [];
|
|
72
|
+
const errors = [];
|
|
73
|
+
|
|
74
|
+
if (preset) {
|
|
75
|
+
if (!catalog.presets[preset]) {
|
|
76
|
+
errors.push(`Unknown preset "${preset}". Run --list-options to see available presets.`);
|
|
77
|
+
} else {
|
|
78
|
+
selected.push(...catalog.presets[preset]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
selected.push(...normalizeOptionList(withOptions));
|
|
83
|
+
|
|
84
|
+
const ids = [...new Set(selected)];
|
|
85
|
+
const withoutIds = [...new Set(normalizeOptionList(withoutOptions))];
|
|
86
|
+
for (const id of ids) {
|
|
87
|
+
if (!skillsById.has(id)) {
|
|
88
|
+
errors.push(`Unknown optional skill "${id}". Run --list-options to see available options.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const id of withoutIds) {
|
|
92
|
+
if (!skillsById.has(id)) {
|
|
93
|
+
errors.push(`Unknown optional skill in --without "${id}". Run --list-options to see available options.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const withoutSet = new Set(withoutIds);
|
|
98
|
+
const selectedIds = ids.filter(id => !withoutSet.has(id));
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
catalog,
|
|
102
|
+
selectedSkills: errors.length ? [] : selectedIds.map(id => skillsById.get(id)),
|
|
103
|
+
errors,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createCoreFileSpecs() {
|
|
108
|
+
const specs = walkFiles(TEMPLATES_DIR)
|
|
109
|
+
.map(file => normalizePath(file))
|
|
110
|
+
.sort()
|
|
111
|
+
.map(file => ({
|
|
112
|
+
dest: file,
|
|
113
|
+
src: path.join(TEMPLATES_DIR, ...file.split('/')),
|
|
114
|
+
type: 'common',
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
specs.push({ dest: 'tests/.gitkeep', src: undefined, type: 'empty' });
|
|
118
|
+
return specs;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function createOptionalFileSpecs(selectedSkills) {
|
|
122
|
+
const specs = [];
|
|
123
|
+
|
|
124
|
+
for (const skill of selectedSkills) {
|
|
125
|
+
for (const fileRoot of skill.files) {
|
|
126
|
+
const absRoot = path.join(OPTIONAL_DIR, ...fileRoot.split('/'));
|
|
127
|
+
for (const file of walkFiles(absRoot).map(normalizePath).sort()) {
|
|
128
|
+
specs.push({
|
|
129
|
+
dest: file,
|
|
130
|
+
src: path.join(absRoot, ...file.split('/')),
|
|
131
|
+
type: 'optional',
|
|
132
|
+
skillId: skill.id,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return specs;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createPlan(resolvedDir, fileSpecs) {
|
|
142
|
+
const plan = {
|
|
143
|
+
create: [],
|
|
144
|
+
skip: [],
|
|
145
|
+
overwrite: [],
|
|
146
|
+
backup: [],
|
|
147
|
+
conflict: [],
|
|
148
|
+
mkdir: [],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const fileSet = new Set(fileSpecs.map(spec => spec.dest));
|
|
152
|
+
|
|
153
|
+
const dirSet = new Set(['.']);
|
|
154
|
+
for (const file of fileSet) {
|
|
155
|
+
let dir = path.posix.dirname(file);
|
|
156
|
+
while (dir && dir !== '.') {
|
|
157
|
+
dirSet.add(dir);
|
|
158
|
+
dir = path.posix.dirname(dir);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const dir of EMPTY_DIRS) {
|
|
162
|
+
dirSet.add(dir);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const dir of [...dirSet].sort()) {
|
|
166
|
+
const absDir = dir === '.'
|
|
167
|
+
? resolvedDir
|
|
168
|
+
: path.join(resolvedDir, ...dir.split('/'));
|
|
169
|
+
if (!fs.existsSync(absDir)) {
|
|
170
|
+
plan.mkdir.push(dir === '.' ? './' : `${dir}/`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return plan;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function addFileActions(plan, fileSpecs, resolvedDir, onConflict) {
|
|
178
|
+
for (const { dest: file } of fileSpecs) {
|
|
179
|
+
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
180
|
+
|
|
181
|
+
if (!fs.existsSync(destPath)) {
|
|
182
|
+
plan.create.push(file);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (fs.statSync(destPath).isDirectory()) {
|
|
187
|
+
if (onConflict === 'skip') {
|
|
188
|
+
plan.skip.push(file);
|
|
189
|
+
} else {
|
|
190
|
+
plan.conflict.push(file);
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (onConflict === 'skip') {
|
|
196
|
+
plan.skip.push(file);
|
|
197
|
+
} else if (onConflict === 'backup') {
|
|
198
|
+
plan.backup.push(file);
|
|
199
|
+
} else if (onConflict === 'overwrite') {
|
|
200
|
+
plan.overwrite.push(file);
|
|
201
|
+
} else {
|
|
202
|
+
plan.conflict.push(file);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function createSummary(plan, { noWrites = false } = {}) {
|
|
208
|
+
return {
|
|
209
|
+
created: noWrites ? 0 : plan.create.length,
|
|
210
|
+
skipped: noWrites ? 0 : plan.skip.length,
|
|
211
|
+
overwritten: noWrites ? 0 : plan.overwrite.length,
|
|
212
|
+
backedUp: noWrites ? 0 : plan.backup.length,
|
|
213
|
+
conflicts: plan.conflict.length,
|
|
214
|
+
mkdir: noWrites ? 0 : plan.mkdir.length,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function nextBackupPath(destPath) {
|
|
219
|
+
const first = `${destPath}.harness-backup`;
|
|
220
|
+
if (!fs.existsSync(first)) {
|
|
221
|
+
return first;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (let index = 1; ; index += 1) {
|
|
225
|
+
const candidate = `${first}.${index}`;
|
|
226
|
+
if (!fs.existsSync(candidate)) {
|
|
227
|
+
return candidate;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function registerOptionalContent(file, content, selectedSkills) {
|
|
233
|
+
if (!selectedSkills.length) return content;
|
|
234
|
+
|
|
235
|
+
if (file === 'MEMORY.md') {
|
|
236
|
+
const lines = selectedSkills.map(skill => (
|
|
237
|
+
`- [${skill.id}](.claude/skills/${skill.id}/SKILL.md) - ${skill.description} Workflow: docs/workflows/${skill.id}.md`
|
|
238
|
+
));
|
|
239
|
+
return content.replace(
|
|
240
|
+
'Stack-specific skills can be added after the product shape is known.',
|
|
241
|
+
`Installed optional skills:\n\n${lines.join('\n')}\n\nStack-specific skills can be added after the product shape is known.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (file === 'docs/README.md') {
|
|
246
|
+
const lines = selectedSkills.map(skill => (
|
|
247
|
+
`- [${skill.title}](workflows/${skill.id}.md) - ${skill.description}`
|
|
248
|
+
));
|
|
249
|
+
return `${content.trimEnd()}\n\n## Installed Optional Workflows\n\n${lines.join('\n')}\n`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return content;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function registrationWarnings(plan, selectedSkills) {
|
|
256
|
+
if (!selectedSkills.length) return [];
|
|
257
|
+
|
|
258
|
+
const warnings = [];
|
|
259
|
+
if (plan.skip.includes('MEMORY.md')) {
|
|
260
|
+
warnings.push('MEMORY.md was skipped; manually register selected optional skills under #Skills.');
|
|
261
|
+
}
|
|
262
|
+
if (plan.skip.includes('docs/README.md')) {
|
|
263
|
+
warnings.push('docs/README.md was skipped; manually register selected optional workflow paths.');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return warnings;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function generate({
|
|
270
|
+
projectName,
|
|
271
|
+
targetDir,
|
|
272
|
+
dryRun = false,
|
|
273
|
+
onConflict = 'fail',
|
|
274
|
+
withOptions = [],
|
|
275
|
+
withoutOptions = [],
|
|
276
|
+
preset = undefined,
|
|
277
|
+
}) {
|
|
56
278
|
const created = [];
|
|
57
279
|
const errors = [];
|
|
280
|
+
const warnings = [];
|
|
58
281
|
|
|
59
282
|
// Resolve targetDir relative to cwd
|
|
60
283
|
const resolvedDir = path.resolve(process.cwd(), targetDir);
|
|
284
|
+
const vars = { projectName };
|
|
61
285
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
286
|
+
const optional = resolveOptionalSelection({ withOptions, withoutOptions, preset });
|
|
287
|
+
const fileSpecs = [
|
|
288
|
+
...createCoreFileSpecs(),
|
|
289
|
+
...createOptionalFileSpecs(optional.selectedSkills),
|
|
290
|
+
];
|
|
291
|
+
const specsByDest = new Map(fileSpecs.map(spec => [spec.dest, spec]));
|
|
292
|
+
const plan = createPlan(resolvedDir, fileSpecs);
|
|
293
|
+
|
|
294
|
+
if (!VALID_CONFLICT_POLICIES.has(onConflict)) {
|
|
295
|
+
errors.push(`Unknown conflict policy "${onConflict}". Use fail, skip, backup, or overwrite.`);
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
created,
|
|
299
|
+
errors,
|
|
300
|
+
plan,
|
|
301
|
+
summary: createSummary(plan, { noWrites: true }),
|
|
302
|
+
warnings,
|
|
303
|
+
};
|
|
68
304
|
}
|
|
69
305
|
|
|
70
|
-
|
|
306
|
+
if (optional.errors.length > 0) {
|
|
307
|
+
errors.push(...optional.errors);
|
|
308
|
+
return {
|
|
309
|
+
success: false,
|
|
310
|
+
created,
|
|
311
|
+
errors,
|
|
312
|
+
plan,
|
|
313
|
+
summary: createSummary(plan, { noWrites: true }),
|
|
314
|
+
warnings,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
71
317
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
318
|
+
addFileActions(plan, fileSpecs, resolvedDir, onConflict);
|
|
319
|
+
warnings.push(...registrationWarnings(plan, optional.selectedSkills));
|
|
320
|
+
|
|
321
|
+
if (dryRun) {
|
|
322
|
+
return {
|
|
323
|
+
success: true,
|
|
324
|
+
created,
|
|
325
|
+
errors,
|
|
326
|
+
plan,
|
|
327
|
+
summary: createSummary(plan),
|
|
328
|
+
warnings,
|
|
329
|
+
dryRun: true,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
86
332
|
|
|
87
|
-
|
|
88
|
-
|
|
333
|
+
if (plan.conflict.length > 0) {
|
|
334
|
+
errors.push(`Generation stopped because ${plan.conflict.length} file conflict(s) were found: ${plan.conflict.join(', ')}`);
|
|
335
|
+
return {
|
|
336
|
+
success: false,
|
|
337
|
+
created,
|
|
338
|
+
errors,
|
|
339
|
+
plan,
|
|
340
|
+
summary: createSummary(plan, { noWrites: true }),
|
|
341
|
+
warnings,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
for (const dir of plan.mkdir) {
|
|
347
|
+
const dirPath = dir === './'
|
|
348
|
+
? resolvedDir
|
|
349
|
+
: path.join(resolvedDir, ...dir.slice(0, -1).split('/'));
|
|
89
350
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
90
|
-
created.push(`${dir}
|
|
351
|
+
created.push(dir === './' ? './ (directory)' : `${dir} (directory)`);
|
|
91
352
|
}
|
|
92
353
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
354
|
+
for (const file of plan.backup) {
|
|
355
|
+
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
356
|
+
fs.renameSync(destPath, nextBackupPath(destPath));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const writableFiles = [
|
|
360
|
+
...plan.create,
|
|
361
|
+
...plan.overwrite,
|
|
362
|
+
...plan.backup,
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
for (const file of writableFiles) {
|
|
366
|
+
const spec = specsByDest.get(file);
|
|
367
|
+
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
368
|
+
let content = spec.type === 'empty'
|
|
369
|
+
? ''
|
|
370
|
+
: renderTemplate(spec.src, vars);
|
|
371
|
+
content = registerOptionalContent(file, content, optional.selectedSkills);
|
|
372
|
+
|
|
373
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
374
|
+
fs.writeFileSync(destPath, content, 'utf-8');
|
|
375
|
+
created.push(file);
|
|
376
|
+
}
|
|
97
377
|
|
|
98
|
-
return {
|
|
378
|
+
return {
|
|
379
|
+
success: true,
|
|
380
|
+
created,
|
|
381
|
+
errors,
|
|
382
|
+
plan,
|
|
383
|
+
summary: createSummary(plan),
|
|
384
|
+
warnings,
|
|
385
|
+
};
|
|
99
386
|
|
|
100
387
|
} catch (err) {
|
|
101
388
|
errors.push(err.message);
|
|
102
|
-
return {
|
|
389
|
+
return {
|
|
390
|
+
success: false,
|
|
391
|
+
created,
|
|
392
|
+
errors,
|
|
393
|
+
plan,
|
|
394
|
+
summary: createSummary(plan),
|
|
395
|
+
warnings,
|
|
396
|
+
};
|
|
103
397
|
}
|
|
104
398
|
}
|