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.
Files changed (29) hide show
  1. package/README.md +95 -1
  2. package/package.json +5 -2
  3. package/src/generator.js +347 -53
  4. package/src/index.js +212 -17
  5. package/templates/common/.claude/rules/ecc/common.md +9 -0
  6. package/templates/common/CLAUDE.md +5 -3
  7. package/templates/common/MEMORY.md +12 -8
  8. package/templates/common/SETUP.md +87 -2
  9. package/templates/common/docs/README.md +8 -0
  10. package/templates/common/docs/features/_template.md +24 -1
  11. package/templates/common/docs/harness/agent-workflow.md +3 -7
  12. package/templates/common/docs/harness/context-loading.md +3 -0
  13. package/templates/common/docs/harness/dispatch.md +3 -0
  14. package/templates/common/docs/harness/extension.md +12 -0
  15. package/templates/common/memory/agent-lessons-patterns.md +21 -0
  16. package/templates/common/memory/tool-usage-reflections.md +21 -0
  17. package/templates/common/memory/user-corrections-preferences.md +21 -0
  18. package/templates/common/scripts/validate-harness.mjs +99 -3
  19. package/templates/optional/catalog.json +43 -0
  20. package/templates/optional/skills/browser-e2e/.claude/skills/browser-e2e/SKILL.md +42 -0
  21. package/templates/optional/skills/browser-e2e/docs/workflows/browser-e2e.md +42 -0
  22. package/templates/optional/skills/github-pr-review/.claude/skills/github-pr-review/SKILL.md +40 -0
  23. package/templates/optional/skills/github-pr-review/docs/workflows/github-pr-review.md +28 -0
  24. package/templates/optional/skills/python-backend/.claude/skills/python-backend/SKILL.md +40 -0
  25. package/templates/optional/skills/python-backend/docs/workflows/python-backend.md +34 -0
  26. package/templates/optional/skills/ts-react-frontend/.claude/skills/ts-react-frontend/SKILL.md +43 -0
  27. package/templates/optional/skills/ts-react-frontend/docs/workflows/ts-react-frontend.md +35 -0
  28. package/templates/optional/skills/ui-ux-review/.claude/skills/ui-ux-review/SKILL.md +40 -0
  29. 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.8",
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
- * Copy a directory recursively, replacing {{vars}} in file contents.
16
+ * Replace {{vars}} in file contents.
11
17
  */
12
- function copyDir(src, dest, vars) {
13
- fs.mkdirSync(dest, { recursive: true });
18
+ function renderTemplate(src, vars) {
19
+ let content = fs.readFileSync(src, 'utf-8');
14
20
 
15
- const entries = fs.readdirSync(src, { withFileTypes: true });
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
- export function generate({ projectName, targetDir }) {
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
- // Check if target exists and is non-empty
63
- if (fs.existsSync(resolvedDir)) {
64
- const existing = fs.readdirSync(resolvedDir).filter(f => f !== '.git');
65
- if (existing.length > 0) {
66
- console.log(pc.yellow(`Directory "${targetDir}" already exists and is not empty. Files may be overwritten.`));
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
- const vars = { projectName };
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
- try {
73
- // 1. Copy all template files
74
- console.log(pc.cyan('Copying templates...'));
75
- copyDir(TEMPLATES_DIR, resolvedDir, vars);
76
-
77
- // List all created files from templates
78
- created.push(...walkFiles(TEMPLATES_DIR).map(f => f.replace(/\\/g, '/')));
79
-
80
- // 2. Create empty directories
81
- console.log(pc.cyan('Creating placeholder directories...'));
82
- const emptyDirs = [
83
- '.claude/hooks',
84
- 'tests',
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
- for (const dir of emptyDirs) {
88
- const dirPath = path.join(resolvedDir, dir);
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}/ (empty)`);
351
+ created.push(dir === './' ? './ (directory)' : `${dir} (directory)`);
91
352
  }
92
353
 
93
- // 3. Create .gitkeep in tests/
94
- const gitkeepPath = path.join(resolvedDir, 'tests', '.gitkeep');
95
- fs.writeFileSync(gitkeepPath, '', 'utf-8');
96
- created.push('tests/.gitkeep');
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 { success: true, created, errors };
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 { success: false, created, errors };
389
+ return {
390
+ success: false,
391
+ created,
392
+ errors,
393
+ plan,
394
+ summary: createSummary(plan),
395
+ warnings,
396
+ };
103
397
  }
104
398
  }