dev-booster 1.10.0 → 1.12.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/src/index.js CHANGED
@@ -1,181 +1,274 @@
1
- import fs from 'fs'
2
- import path from 'path'
3
- import readline from 'readline/promises'
4
- import { fileURLToPath } from 'url'
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { execSync } from "child_process";
4
+ import readline from "readline/promises";
5
+ import { fileURLToPath } from "url";
5
6
 
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
8
 
8
9
  // Source: the `template/` folder shipped inside the npm package
9
- const TEMPLATE_DIR = path.resolve(__dirname, '..', 'template')
10
+ const TEMPLATE_DIR = path.resolve(__dirname, "..", "template");
10
11
 
11
12
  // Destination: the user's current working directory (where they ran npx)
12
- const TARGET_DIR = process.cwd()
13
- const args = process.argv.slice(2)
14
- const GITIGNORE_BLOCK = [
15
- '# DEV-BOOSTER',
16
- '.devbooster/',
17
- 'DEVBOOSTER_INIT.md',
18
- ]
19
- const GITIGNORE_MARKER = '# DEV-BOOSTER'
13
+ const TARGET_DIR = process.cwd();
14
+ const args = process.argv.slice(2);
15
+ const GITIGNORE_BLOCK = ["# DEV-BOOSTER", ".devbooster/", "DEVBOOSTER_INIT.md"];
16
+ const GITIGNORE_MARKER = "# DEV-BOOSTER";
20
17
  const IDE_BRIDGE_BLOCK = [
21
- '# 🤖 DEV BOOSTER — AGENTIC KIT BOOTSTRAP',
22
- '',
23
- 'Before any action, read `.devbooster/rules/PROTOCOL.md` for full governance rules.',
24
- ]
25
- const IDE_BRIDGE_MARKER = '.devbooster/rules/PROTOCOL.md'
26
- const IDE_BRIDGE_FALLBACK_FLAG = path.join('hub', 'flags', 'needs-ide-bridge')
18
+ "# 🤖 DEV BOOSTER — AGENTIC KIT BOOTSTRAP",
19
+ "",
20
+ "Before any action, read `.devbooster/rules/PROTOCOL.md` for full governance rules.",
21
+ ];
22
+ const IDE_BRIDGE_MARKER = ".devbooster/rules/PROTOCOL.md";
23
+ const IDE_BRIDGE_FALLBACK_FLAG = path.join("hub", "flags", "needs-ide-bridge");
27
24
  const IDE_BRIDGE_TARGETS = [
28
- '.rules',
29
- '.cursorrules',
30
- '.windsurfrules',
31
- '.clinerules',
32
- 'AGENT.md',
33
- 'AGENTS.md',
34
- 'CLAUDE.md',
35
- 'GEMINI.md',
36
- '.github/copilot-instructions.md',
37
- ]
25
+ ".rules",
26
+ ".cursorrules",
27
+ ".windsurfrules",
28
+ ".clinerules",
29
+ "AGENT.md",
30
+ "AGENTS.md",
31
+ "CLAUDE.md",
32
+ "GEMINI.md",
33
+ ".github/copilot-instructions.md",
34
+ ];
35
+
36
+ const PROTECTED_FILES = [
37
+ "DEVBOOSTER_INIT.md",
38
+ ".devbooster/rules/PROJECT.md",
39
+ ".devbooster/rules/FRONTEND.md",
40
+ ".devbooster/rules/BACKEND.md",
41
+ ".devbooster/rules/COMMERCIAL.md",
42
+ ".devbooster/rules/USER_PREFERENCES.md",
43
+ ];
44
+
45
+ function safeCopyFile(src, dest, dryRun = false) {
46
+ const relativeDest = path.relative(TARGET_DIR, dest);
47
+ if (fs.existsSync(dest) && PROTECTED_FILES.includes(relativeDest)) {
48
+ return false; // Preserved
49
+ }
50
+ if (!dryRun) {
51
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
52
+ fs.copyFileSync(src, dest);
53
+ }
54
+ return true; // Copied/Updated
55
+ }
56
+
57
+ function copyDirSafe(src, dest, dryRun = false) {
58
+ fs.mkdirSync(dest, { recursive: true });
59
+
60
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
61
+ if (entry.name === ".DS_Store") continue;
62
+
63
+ const srcPath = path.join(src, entry.name);
64
+ const destPath = path.join(dest, entry.name);
65
+
66
+ if (entry.isDirectory()) {
67
+ copyDirSafe(srcPath, destPath, dryRun);
68
+ } else {
69
+ safeCopyFile(srcPath, destPath, dryRun);
70
+ }
71
+ }
72
+ }
38
73
 
39
74
  function copyDir(src, dest) {
40
- fs.mkdirSync(dest, { recursive: true })
75
+ fs.mkdirSync(dest, { recursive: true });
41
76
 
42
77
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
43
- if (entry.name === '.DS_Store') continue
78
+ if (entry.name === ".DS_Store") continue;
44
79
 
45
- const srcPath = path.join(src, entry.name)
46
- const destPath = path.join(dest, entry.name)
80
+ const srcPath = path.join(src, entry.name);
81
+ const destPath = path.join(dest, entry.name);
47
82
 
48
83
  if (entry.isDirectory()) {
49
- copyDir(srcPath, destPath)
84
+ copyDir(srcPath, destPath);
50
85
  } else {
51
- fs.copyFileSync(srcPath, destPath)
86
+ fs.copyFileSync(srcPath, destPath);
52
87
  }
53
88
  }
54
89
  }
55
90
 
56
91
  function removeDirContents(targetDir) {
57
- if (!fs.existsSync(targetDir)) return
92
+ if (!fs.existsSync(targetDir)) return;
58
93
 
59
94
  for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) {
60
- const entryPath = path.join(targetDir, entry.name)
61
- fs.rmSync(entryPath, { recursive: true, force: true })
95
+ const entryPath = path.join(targetDir, entry.name);
96
+ fs.rmSync(entryPath, { recursive: true, force: true });
62
97
  }
63
98
  }
64
99
 
65
100
  function syncDir(src, dest) {
66
- fs.mkdirSync(dest, { recursive: true })
67
- removeDirContents(dest)
68
- copyDir(src, dest)
101
+ fs.mkdirSync(dest, { recursive: true });
102
+ removeDirContents(dest);
103
+ copyDir(src, dest);
69
104
  }
70
105
 
71
106
  function isUpdateMode() {
72
- return args.includes('--update') || args.includes('update')
107
+ return args.includes("--update") || args.includes("update");
73
108
  }
74
109
 
75
110
  function isDryRunMode() {
76
- return args.includes('--dry-run') || args.includes('dry-run')
111
+ return args.includes("--dry-run") || args.includes("dry-run");
77
112
  }
78
113
 
79
114
  function ensureTrailingNewline(content) {
80
- return content.endsWith('\n') ? content : `${content}\n`
115
+ return content.endsWith("\n") ? content : `${content}\n`;
81
116
  }
82
117
 
83
118
  function appendUniqueBlock(filePath, blockLines, marker, dryRun = false) {
84
- const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : ''
119
+ const existing = fs.existsSync(filePath)
120
+ ? fs.readFileSync(filePath, "utf8")
121
+ : "";
85
122
  if (existing.includes(marker)) {
86
- return false
123
+ return false;
87
124
  }
88
125
 
89
126
  if (!dryRun) {
90
- const trimmedEnd = existing.replace(/\s*$/, '')
91
- const prefix = trimmedEnd.length > 0 ? `${trimmedEnd}\n\n` : ''
92
- const nextContent = `${prefix}${blockLines.join('\n')}\n`
93
- fs.mkdirSync(path.dirname(filePath), { recursive: true })
94
- fs.writeFileSync(filePath, nextContent)
127
+ const trimmedEnd = existing.replace(/\s*$/, "");
128
+ const prefix = trimmedEnd.length > 0 ? `${trimmedEnd}\n\n` : "";
129
+ const nextContent = `${prefix}${blockLines.join("\n")}\n`;
130
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
131
+ fs.writeFileSync(filePath, nextContent);
95
132
  }
96
- return true
133
+ return true;
97
134
  }
98
135
 
99
136
  async function askYesNo(question, defaultYes = true) {
100
137
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
101
- return defaultYes
138
+ return defaultYes;
102
139
  }
103
140
 
104
141
  const rl = readline.createInterface({
105
142
  input: process.stdin,
106
143
  output: process.stdout,
107
- })
144
+ });
108
145
 
109
146
  try {
110
- const suffix = defaultYes ? ' (Y/n) ' : ' (y/N) '
111
- const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase()
112
-
113
- if (!answer) return defaultYes
114
- if (answer === 'y' || answer === 'yes') return true
115
- if (answer === 'n' || answer === 'no') return false
116
- return defaultYes
147
+ const suffix = defaultYes ? " (Y/n) " : " (y/N) ";
148
+ const answer = (await rl.question(`${question}${suffix}`))
149
+ .trim()
150
+ .toLowerCase();
151
+
152
+ if (!answer) return defaultYes;
153
+ if (answer === "y" || answer === "yes") return true;
154
+ if (answer === "n" || answer === "no") return false;
155
+ return defaultYes;
117
156
  } finally {
118
- rl.close()
157
+ rl.close();
119
158
  }
120
159
  }
121
160
 
122
161
  async function maybeAddDevBoosterToGitignore(dryRun) {
123
- const shouldIgnore = await askYesNo('Add Dev Booster to your .gitignore?', true)
162
+ const shouldIgnore = await askYesNo(
163
+ "Add Dev Booster to your .gitignore?",
164
+ true,
165
+ );
124
166
 
125
167
  if (!shouldIgnore) {
126
- console.log('▸ .gitignore')
127
- console.log(' status: skipped by user\n')
128
- return
168
+ console.log("▸ .gitignore");
169
+ console.log(" status: skipped by user\n");
170
+ return;
129
171
  }
130
172
 
131
- const gitignorePath = path.join(TARGET_DIR, '.gitignore')
132
- const changed = appendUniqueBlock(gitignorePath, GITIGNORE_BLOCK, GITIGNORE_MARKER, dryRun)
173
+ const gitignorePath = path.join(TARGET_DIR, ".gitignore");
174
+ const changed = appendUniqueBlock(
175
+ gitignorePath,
176
+ GITIGNORE_BLOCK,
177
+ GITIGNORE_MARKER,
178
+ dryRun,
179
+ );
133
180
 
134
- console.log('▸ .gitignore')
181
+ console.log("▸ .gitignore");
135
182
  if (changed) {
136
- console.log(` status: ${dryRun ? 'would be updated' : 'updated'}`)
137
- console.log(' entries: # DEV-BOOSTER, .devbooster/, DEVBOOSTER_INIT.md\n')
183
+ console.log(` status: ${dryRun ? "would be updated" : "updated"}`);
184
+ console.log(" entries: # DEV-BOOSTER, .devbooster/, DEVBOOSTER_INIT.md\n");
138
185
  } else {
139
- console.log(' status: already configured')
140
- console.log(' entries: # DEV-BOOSTER, .devbooster/, DEVBOOSTER_INIT.md\n')
186
+ console.log(" status: already configured");
187
+ console.log(" entries: # DEV-BOOSTER, .devbooster/, DEVBOOSTER_INIT.md\n");
141
188
  }
142
189
  }
143
190
 
144
191
  function writeIdeBridgeFallbackFlag() {
145
- const flagPath = path.join(TARGET_DIR, '.devbooster', IDE_BRIDGE_FALLBACK_FLAG)
146
- fs.mkdirSync(path.dirname(flagPath), { recursive: true })
147
- fs.writeFileSync(flagPath, 'create AGENTS.md fallback bridge\n')
192
+ const flagPath = path.join(
193
+ TARGET_DIR,
194
+ ".devbooster",
195
+ IDE_BRIDGE_FALLBACK_FLAG,
196
+ );
197
+ fs.mkdirSync(path.dirname(flagPath), { recursive: true });
198
+ fs.writeFileSync(flagPath, "create AGENTS.md fallback bridge\n");
148
199
  }
149
200
 
150
201
  function clearIdeBridgeFallbackFlag() {
151
- const flagPath = path.join(TARGET_DIR, '.devbooster', IDE_BRIDGE_FALLBACK_FLAG)
202
+ const flagPath = path.join(
203
+ TARGET_DIR,
204
+ ".devbooster",
205
+ IDE_BRIDGE_FALLBACK_FLAG,
206
+ );
152
207
  if (fs.existsSync(flagPath)) {
153
- fs.rmSync(flagPath, { force: true })
208
+ fs.rmSync(flagPath, { force: true });
154
209
  }
155
210
  }
156
211
 
157
212
  function setupIdeBridgeFiles(dryRun) {
158
- const touchedFiles = []
213
+ const touchedFiles = [];
159
214
 
160
215
  for (const relativeTarget of IDE_BRIDGE_TARGETS) {
161
- const targetPath = path.join(TARGET_DIR, relativeTarget)
162
- if (!fs.existsSync(targetPath)) continue
163
-
164
- const wouldChange = appendUniqueBlock(targetPath, IDE_BRIDGE_BLOCK, IDE_BRIDGE_MARKER, dryRun)
165
- if (wouldChange) touchedFiles.push(relativeTarget)
216
+ const targetPath = path.join(TARGET_DIR, relativeTarget);
217
+ if (!fs.existsSync(targetPath)) continue;
218
+
219
+ const wouldChange = appendUniqueBlock(
220
+ targetPath,
221
+ IDE_BRIDGE_BLOCK,
222
+ IDE_BRIDGE_MARKER,
223
+ dryRun,
224
+ );
225
+ if (wouldChange) touchedFiles.push(relativeTarget);
166
226
  }
167
227
 
168
- console.log('▸ IDE bridge files')
228
+ console.log("▸ IDE bridge files");
169
229
  if (touchedFiles.length > 0) {
170
- if (!dryRun) clearIdeBridgeFallbackFlag()
171
- console.log(` status: bridge instructions ${dryRun ? 'would be appended' : 'appended'} where applicable`)
172
- console.log(` files: ${touchedFiles.join(', ')}\n`)
173
- return
230
+ if (!dryRun) clearIdeBridgeFallbackFlag();
231
+ console.log(
232
+ ` status: bridge instructions ${dryRun ? "would be appended" : "appended"} where applicable`,
233
+ );
234
+ console.log(` files: ${touchedFiles.join(", ")}\n`);
235
+ return;
174
236
  }
175
237
 
176
- if (!dryRun) writeIdeBridgeFallbackFlag()
177
- console.log(' status: no known IDE instruction file found')
178
- console.log(` action: bootstrap fallback flag ${dryRun ? 'would be created' : 'created'} for DEVBOOSTER_INIT.md\n`)
238
+ if (!dryRun) writeIdeBridgeFallbackFlag();
239
+ console.log(" status: no known IDE instruction file found");
240
+ console.log(
241
+ ` action: bootstrap fallback flag ${dryRun ? "would be created" : "created"} for DEVBOOSTER_INIT.md\n`,
242
+ );
243
+ }
244
+
245
+ function checkLocalInstall() {
246
+ try {
247
+ const localPkg = path.join(
248
+ TARGET_DIR,
249
+ "node_modules",
250
+ "dev-booster",
251
+ "package.json",
252
+ );
253
+ if (fs.existsSync(localPkg)) {
254
+ const pkg = JSON.parse(fs.readFileSync(localPkg, "utf8"));
255
+ console.log(
256
+ `\n⚠️ Dev Booster detected as a local dependency (npm i dev-booster) version ${pkg.version}.`,
257
+ );
258
+ console.log(" Removing local dependency to avoid version conflicts...");
259
+ console.log();
260
+ execSync("npm uninstall dev-booster", {
261
+ cwd: TARGET_DIR,
262
+ stdio: "inherit",
263
+ });
264
+ console.log("\n✔ Local dependency removed successfully.");
265
+ console.log(
266
+ " Always use npx --yes dev-booster@latest to get the latest version.\n",
267
+ );
268
+ }
269
+ } catch {
270
+ // Silently ignore if we cannot read the local package or uninstall fails
271
+ }
179
272
  }
180
273
 
181
274
  function printHeader(subtitle) {
@@ -184,45 +277,77 @@ function printHeader(subtitle) {
184
277
  │ DEV BOOSTER │
185
278
  │ ${subtitle.padEnd(44)} │
186
279
  ╰──────────────────────────────────────────────╯
187
- `)
280
+ `);
188
281
  }
189
282
 
190
283
  async function runInstall() {
191
- const dryRun = isDryRunMode()
192
- printHeader('agentic kit installer')
193
- if (dryRun) console.log('🔍 [DRY RUN MODE] No real changes will be made.\n')
194
-
195
- // 1. Copy the full .devbooster/ kit to the user's project
196
- const agentSrc = path.join(TEMPLATE_DIR, '.devbooster')
197
- const agentDest = path.join(TARGET_DIR, '.devbooster')
198
-
199
- if (fs.existsSync(agentDest)) {
200
- console.log('▸ .devbooster/')
201
- console.log(' status: already exists in this project')
202
- console.log(' action: copy skipped to avoid overwrite')
203
- console.log(' tip: rename or remove the folder if you want a fresh install\n')
284
+ const dryRun = isDryRunMode();
285
+ printHeader("agentic kit installer");
286
+ if (dryRun) console.log("🔍 [DRY RUN MODE] No real changes will be made.\n");
287
+
288
+ const agentSrc = path.join(TEMPLATE_DIR, ".devbooster");
289
+ const agentDest = path.join(TARGET_DIR, ".devbooster");
290
+
291
+ const boostersCount = fs.existsSync(path.join(agentSrc, "boosters"))
292
+ ? fs
293
+ .readdirSync(path.join(agentSrc, "boosters"))
294
+ .filter((f) => f !== ".DS_Store" && f !== "templates").length
295
+ : 28;
296
+ const hubCount = fs.existsSync(path.join(agentSrc, "hub", "scripts"))
297
+ ? fs
298
+ .readdirSync(path.join(agentSrc, "hub", "scripts"))
299
+ .filter((f) => f !== ".DS_Store").length
300
+ : 21;
301
+
302
+ const isOverlap = fs.existsSync(agentDest);
303
+
304
+ if (isOverlap) {
305
+ console.log("▸ Existing Kit Detected:");
306
+ console.log(
307
+ ` ├── boosters/ ➔ ${boostersCount} expert activators updated successfully`,
308
+ );
309
+ console.log(
310
+ ` ├── hub/ ➔ ${hubCount} operational scripts updated successfully`,
311
+ );
312
+ console.log(
313
+ " └── rules/ ➔ Core rules updated, project rules preserved",
314
+ );
315
+ console.log(" ✔ PROTOCOL.md (updated)");
316
+ console.log(" ✔ GUIDE.md (updated)");
317
+ console.log(" ✔ TRIGGERS.md (updated)");
318
+ console.log(
319
+ " ℹ PROJECT.md, FRONTEND.md, BACKEND.md, etc. (preserved)\n",
320
+ );
204
321
  } else {
205
- if (!dryRun) copyDir(agentSrc, agentDest)
206
- console.log('▸ .devbooster/')
207
- console.log(` status: ${dryRun ? 'would be installed' : 'installed successfully'}\n`)
322
+ console.log("▸ Installing Kit Structure:");
323
+ console.log(
324
+ ` ├── boosters/ ➔ ${boostersCount} expert activators created successfully`,
325
+ );
326
+ console.log(
327
+ ` ├── hub/ ➔ ${hubCount} operational scripts created successfully`,
328
+ );
329
+ console.log(" └── rules/ ➔ Core rules and templates initialized");
330
+ console.log(" ✔ 8 rules created\n");
208
331
  }
209
332
 
210
- // 2. Drop DEVBOOSTER_INIT.md at the root of the user's project
211
- const initSrc = path.join(TEMPLATE_DIR, 'DEVBOOSTER_INIT.md')
212
- const initDest = path.join(TARGET_DIR, 'DEVBOOSTER_INIT.md')
333
+ copyDirSafe(agentSrc, agentDest, dryRun);
213
334
 
214
- if (fs.existsSync(initDest)) {
215
- console.log('▸ DEVBOOSTER_INIT.md')
216
- console.log(' status: already exists in this project')
217
- console.log(' action: creation skipped\n')
335
+ // 2. Drop DEVBOOSTER_INIT.md at the root of the user's project safely
336
+ const initSrc = path.join(TEMPLATE_DIR, "DEVBOOSTER_INIT.md");
337
+ const initDest = path.join(TARGET_DIR, "DEVBOOSTER_INIT.md");
338
+
339
+ const copiedInit = safeCopyFile(initSrc, initDest, dryRun);
340
+ console.log("▸ DEVBOOSTER_INIT.md");
341
+ if (copiedInit) {
342
+ console.log(
343
+ ` status: ${dryRun ? "would be created" : "created"} at project root\n`,
344
+ );
218
345
  } else {
219
- if (!dryRun) fs.copyFileSync(initSrc, initDest)
220
- console.log('▸ DEVBOOSTER_INIT.md')
221
- console.log(` status: ${dryRun ? 'would be created' : 'created'} at project root\n`)
346
+ console.log(" status: already exists (preserved)\n");
222
347
  }
223
348
 
224
- await maybeAddDevBoosterToGitignore(dryRun)
225
- setupIdeBridgeFiles(dryRun)
349
+ await maybeAddDevBoosterToGitignore(dryRun);
350
+ setupIdeBridgeFiles(dryRun);
226
351
 
227
352
  console.log(`
228
353
  ╭──────────────────────────────────────────────╮
@@ -234,58 +359,84 @@ Open your AI assistant in this project and send:
234
359
  "Read DEVBOOSTER_INIT.md and execute all bootstrap steps."
235
360
 
236
361
  The kit will configure itself based on your project's stack.
237
- `)
362
+ `);
238
363
  }
239
364
 
240
365
  function runUpdate() {
241
- const dryRun = isDryRunMode()
242
- printHeader('safe kit update')
243
- if (dryRun) console.log('🔍 [DRY RUN MODE] No real changes will be made.\n')
366
+ const dryRun = isDryRunMode();
367
+ printHeader("safe kit update");
368
+ if (dryRun) console.log("🔍 [DRY RUN MODE] No real changes will be made.\n");
244
369
 
245
- const kitRoot = path.join(TARGET_DIR, '.devbooster')
246
- const templateRoot = path.join(TEMPLATE_DIR, '.devbooster')
370
+ const kitRoot = path.join(TARGET_DIR, ".devbooster");
371
+ const templateRoot = path.join(TEMPLATE_DIR, ".devbooster");
247
372
 
248
373
  if (!fs.existsSync(kitRoot)) {
249
- console.log('▸ .devbooster/')
250
- console.log(' status: not found in this project')
251
- console.log(' action: run `npx dev-booster` first to install the kit\n')
252
- return
374
+ console.log("▸ .devbooster/");
375
+ console.log(" status: not found in this project");
376
+ console.log(" action: run `npx dev-booster` first to install the kit\n");
377
+ return;
253
378
  }
254
379
 
380
+ const boostersCount = fs.existsSync(path.join(templateRoot, "boosters"))
381
+ ? fs
382
+ .readdirSync(path.join(templateRoot, "boosters"))
383
+ .filter((f) => f !== ".DS_Store" && f !== "templates").length
384
+ : 28;
385
+ const hubCount = fs.existsSync(path.join(templateRoot, "hub", "scripts"))
386
+ ? fs
387
+ .readdirSync(path.join(templateRoot, "hub", "scripts"))
388
+ .filter((f) => f !== ".DS_Store").length
389
+ : 21;
390
+
391
+ // 1. Sync boosters and hub (fully wiped and overwritten)
255
392
  const updateTargets = [
256
- { name: 'boosters', src: path.join(templateRoot, 'boosters'), dest: path.join(kitRoot, 'boosters') },
257
- { name: 'hub', src: path.join(templateRoot, 'hub'), dest: path.join(kitRoot, 'hub') },
258
- ]
259
-
260
- const coreFiles = [
261
- { name: 'MANIFEST.md', src: path.join(templateRoot, 'MANIFEST.md'), dest: path.join(kitRoot, 'MANIFEST.md') },
262
- { name: 'rules/GUIDE.md', src: path.join(templateRoot, 'rules', 'GUIDE.md'), dest: path.join(kitRoot, 'rules', 'GUIDE.md') },
263
- { name: 'rules/PROTOCOL.md', src: path.join(templateRoot, 'rules', 'PROTOCOL.md'), dest: path.join(kitRoot, 'rules', 'PROTOCOL.md') },
264
- ]
393
+ {
394
+ name: "boosters",
395
+ src: path.join(templateRoot, "boosters"),
396
+ dest: path.join(kitRoot, "boosters"),
397
+ },
398
+ {
399
+ name: "hub",
400
+ src: path.join(templateRoot, "hub"),
401
+ dest: path.join(kitRoot, "hub"),
402
+ },
403
+ ];
404
+
405
+ console.log("▸ Updating Kit Core:");
265
406
 
266
407
  for (const target of updateTargets) {
267
- if (!dryRun) syncDir(target.src, target.dest)
268
- console.log(`▸ .devbooster/${target.name}/`)
269
- console.log(` status: ${dryRun ? 'would be updated' : 'updated successfully'}`)
408
+ if (!dryRun) syncDir(target.src, target.dest);
270
409
  }
271
-
272
- for (const file of coreFiles) {
273
- if (!dryRun && fs.existsSync(file.src)) {
274
- fs.mkdirSync(path.dirname(file.dest), { recursive: true })
275
- fs.copyFileSync(file.src, file.dest)
276
- }
277
- console.log(`▸ .devbooster/${file.name}`)
278
- console.log(` status: ${dryRun ? 'would be updated' : 'updated successfully'}`)
279
- }
280
-
281
- console.log('')
282
- console.log('▸ rules/ (Whitelabel files)')
283
- console.log(' status: preserved')
284
- console.log(' action: user-customized rules were not overwritten')
285
- console.log('')
286
- console.log('▸ DEVBOOSTER_INIT.md')
287
- console.log(' status: preserved')
288
- console.log(' action: no local files were overwritten\n')
410
+ console.log(
411
+ ` ├── boosters/ ➔ ${boostersCount} expert activators synced & updated`,
412
+ );
413
+ console.log(
414
+ ` ├── hub/ ➔ ${hubCount} operational scripts synced & updated`,
415
+ );
416
+
417
+ // 2. Safe-copy rules directory recursively (overwriting core rules, preserving custom ones)
418
+ const rulesSrc = path.join(templateRoot, "rules");
419
+ const rulesDest = path.join(kitRoot, "rules");
420
+ copyDirSafe(rulesSrc, rulesDest, dryRun);
421
+
422
+ // 3. Safe-copy core files
423
+ safeCopyFile(
424
+ path.join(templateRoot, "MANIFEST.md"),
425
+ path.join(kitRoot, "MANIFEST.md"),
426
+ dryRun,
427
+ );
428
+
429
+ console.log(" ├── MANIFEST.md ➔ Inventory updated");
430
+ console.log(
431
+ " └── rules/ ➔ Core rules updated, customized rules preserved",
432
+ );
433
+ console.log(" ✔ PROTOCOL.md (updated)");
434
+ console.log(" ✔ GUIDE.md (updated)");
435
+ console.log(" ✔ TRIGGERS.md (updated)");
436
+ console.log(" ℹ Whitelabel stack rules (preserved)\n");
437
+
438
+ console.log("▸ DEVBOOSTER_INIT.md");
439
+ console.log(" status: preserved (no changes made)\n");
289
440
 
290
441
  console.log(`
291
442
  ╭──────────────────────────────────────────────╮
@@ -294,14 +445,16 @@ function runUpdate() {
294
445
 
295
446
  Review the updated kit files in your project and, if you want,
296
447
  reopen your AI assistant to keep working with the refreshed boosters.
297
- `)
448
+ `);
298
449
  }
299
450
 
300
451
  export async function run() {
452
+ checkLocalInstall();
453
+
301
454
  if (isUpdateMode()) {
302
- runUpdate()
303
- return
455
+ runUpdate();
456
+ return;
304
457
  }
305
458
 
306
- await runInstall()
459
+ await runInstall();
307
460
  }