safeword 0.8.7 → 0.8.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 (40) hide show
  1. package/dist/{check-PPVIEF3Q.js → check-4HX4SNVV.js} +27 -27
  2. package/dist/check-4HX4SNVV.js.map +1 -0
  3. package/dist/{chunk-W66Z3C5H.js → chunk-FJYRWU2V.js} +5 -5
  4. package/dist/chunk-FJYRWU2V.js.map +1 -0
  5. package/dist/chunk-POPS3ZRQ.js +1219 -0
  6. package/dist/chunk-POPS3ZRQ.js.map +1 -0
  7. package/dist/cli.js +5 -9
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{diff-S3ICSYQY.js → diff-7QIV6Z5B.js} +14 -16
  10. package/dist/diff-7QIV6Z5B.js.map +1 -0
  11. package/dist/index.d.ts +2 -7
  12. package/dist/reset-RP7AGR2Y.js +73 -0
  13. package/dist/reset-RP7AGR2Y.js.map +1 -0
  14. package/dist/setup-ZYRPDTQI.js +91 -0
  15. package/dist/setup-ZYRPDTQI.js.map +1 -0
  16. package/dist/upgrade-K2FFESUH.js +76 -0
  17. package/dist/upgrade-K2FFESUH.js.map +1 -0
  18. package/package.json +2 -2
  19. package/templates/SAFEWORD.md +5 -2
  20. package/templates/commands/cleanup-zombies.md +48 -0
  21. package/templates/guides/cli-reference.md +9 -11
  22. package/templates/guides/zombie-process-cleanup.md +40 -24
  23. package/templates/scripts/cleanup-zombies.sh +222 -0
  24. package/dist/check-PPVIEF3Q.js.map +0 -1
  25. package/dist/chunk-34PU3QZI.js +0 -1047
  26. package/dist/chunk-34PU3QZI.js.map +0 -1
  27. package/dist/chunk-3OK3NQEW.js +0 -476
  28. package/dist/chunk-3OK3NQEW.js.map +0 -1
  29. package/dist/chunk-BFBUEJDH.js +0 -88
  30. package/dist/chunk-BFBUEJDH.js.map +0 -1
  31. package/dist/chunk-W66Z3C5H.js.map +0 -1
  32. package/dist/diff-S3ICSYQY.js.map +0 -1
  33. package/dist/reset-ZST2SGZ2.js +0 -74
  34. package/dist/reset-ZST2SGZ2.js.map +0 -1
  35. package/dist/setup-ANAIEP3D.js +0 -100
  36. package/dist/setup-ANAIEP3D.js.map +0 -1
  37. package/dist/sync-V6D7QTMO.js +0 -9
  38. package/dist/sync-V6D7QTMO.js.map +0 -1
  39. package/dist/upgrade-QFIGWZ5I.js +0 -76
  40. package/dist/upgrade-QFIGWZ5I.js.map +0 -1
@@ -0,0 +1,1219 @@
1
+ import {
2
+ VERSION
3
+ } from "./chunk-ORQHKDT2.js";
4
+
5
+ // src/utils/fs.ts
6
+ import {
7
+ chmodSync,
8
+ existsSync,
9
+ mkdirSync,
10
+ readdirSync,
11
+ readFileSync,
12
+ rmdirSync,
13
+ rmSync,
14
+ writeFileSync
15
+ } from "fs";
16
+ import nodePath from "path";
17
+ import { fileURLToPath } from "url";
18
+ var __dirname = nodePath.dirname(fileURLToPath(import.meta.url));
19
+ function getTemplatesDirectory() {
20
+ const knownTemplateFile = "SAFEWORD.md";
21
+ const candidates = [
22
+ nodePath.join(__dirname, "..", "templates"),
23
+ // From dist/ (flat bundled)
24
+ nodePath.join(__dirname, "..", "..", "templates"),
25
+ // From src/utils/ or dist/utils/
26
+ nodePath.join(__dirname, "templates")
27
+ // Direct sibling (unlikely but safe)
28
+ ];
29
+ for (const candidate of candidates) {
30
+ if (existsSync(nodePath.join(candidate, knownTemplateFile))) {
31
+ return candidate;
32
+ }
33
+ }
34
+ throw new Error("Templates directory not found");
35
+ }
36
+ function exists(path) {
37
+ return existsSync(path);
38
+ }
39
+ function ensureDirectory(path) {
40
+ if (!existsSync(path)) {
41
+ mkdirSync(path, { recursive: true });
42
+ }
43
+ }
44
+ function readFile(path) {
45
+ return readFileSync(path, "utf8");
46
+ }
47
+ function readFileSafe(path) {
48
+ if (!existsSync(path)) return void 0;
49
+ return readFileSync(path, "utf8");
50
+ }
51
+ function writeFile(path, content) {
52
+ ensureDirectory(nodePath.dirname(path));
53
+ writeFileSync(path, content);
54
+ }
55
+ function remove(path) {
56
+ if (existsSync(path)) {
57
+ rmSync(path, { recursive: true, force: true });
58
+ }
59
+ }
60
+ function removeIfEmpty(path) {
61
+ if (!existsSync(path)) return false;
62
+ try {
63
+ rmdirSync(path);
64
+ return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+ function makeScriptsExecutable(dirPath) {
70
+ if (!existsSync(dirPath)) return;
71
+ for (const file of readdirSync(dirPath)) {
72
+ if (file.endsWith(".sh")) {
73
+ chmodSync(nodePath.join(dirPath, file), 493);
74
+ }
75
+ }
76
+ }
77
+ function readJson(path) {
78
+ const content = readFileSafe(path);
79
+ if (!content) return void 0;
80
+ try {
81
+ return JSON.parse(content);
82
+ } catch {
83
+ return void 0;
84
+ }
85
+ }
86
+ function writeJson(path, data) {
87
+ writeFile(path, `${JSON.stringify(data, void 0, 2)}
88
+ `);
89
+ }
90
+
91
+ // src/reconcile.ts
92
+ import nodePath2 from "path";
93
+ var HUSKY_DIR = ".husky";
94
+ function shouldSkipForNonGit(path, isGitRepo2) {
95
+ return path.startsWith(HUSKY_DIR) && !isGitRepo2;
96
+ }
97
+ function planMissingDirectories(directories, cwd, isGitRepo2) {
98
+ const actions = [];
99
+ const created = [];
100
+ for (const dir of directories) {
101
+ if (shouldSkipForNonGit(dir, isGitRepo2)) continue;
102
+ if (!exists(nodePath2.join(cwd, dir))) {
103
+ actions.push({ type: "mkdir", path: dir });
104
+ created.push(dir);
105
+ }
106
+ }
107
+ return { actions, created };
108
+ }
109
+ function planTextPatches(patches, cwd, isGitRepo2) {
110
+ const actions = [];
111
+ for (const [filePath, definition] of Object.entries(patches)) {
112
+ if (shouldSkipForNonGit(filePath, isGitRepo2)) continue;
113
+ const content = readFileSafe(nodePath2.join(cwd, filePath)) ?? "";
114
+ if (!content.includes(definition.marker)) {
115
+ actions.push({ type: "text-patch", path: filePath, definition });
116
+ }
117
+ }
118
+ return actions;
119
+ }
120
+ function planOwnedFileWrites(files, ctx) {
121
+ const actions = [];
122
+ const created = [];
123
+ for (const [filePath, definition] of Object.entries(files)) {
124
+ if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;
125
+ const content = resolveFileContent(definition, ctx);
126
+ actions.push({ type: "write", path: filePath, content });
127
+ created.push(filePath);
128
+ }
129
+ return { actions, created };
130
+ }
131
+ function planManagedFileWrites(files, ctx) {
132
+ const actions = [];
133
+ const created = [];
134
+ for (const [filePath, definition] of Object.entries(files)) {
135
+ if (exists(nodePath2.join(ctx.cwd, filePath))) continue;
136
+ const content = resolveFileContent(definition, ctx);
137
+ actions.push({ type: "write", path: filePath, content });
138
+ created.push(filePath);
139
+ }
140
+ return { actions, created };
141
+ }
142
+ function planTextPatchesWithCreation(patches, ctx, wouldCreate) {
143
+ const actions = [];
144
+ for (const [filePath, definition] of Object.entries(patches)) {
145
+ if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;
146
+ actions.push({ type: "text-patch", path: filePath, definition });
147
+ if (definition.createIfMissing && !exists(nodePath2.join(ctx.cwd, filePath))) {
148
+ wouldCreate.push(filePath);
149
+ }
150
+ }
151
+ return actions;
152
+ }
153
+ function planExistingDirectoriesRemoval(directories, cwd) {
154
+ const actions = [];
155
+ const removed = [];
156
+ for (const dir of directories) {
157
+ if (exists(nodePath2.join(cwd, dir))) {
158
+ actions.push({ type: "rmdir", path: dir });
159
+ removed.push(dir);
160
+ }
161
+ }
162
+ return { actions, removed };
163
+ }
164
+ function planExistingFilesRemoval(files, cwd) {
165
+ const actions = [];
166
+ const removed = [];
167
+ for (const filePath of files) {
168
+ if (exists(nodePath2.join(cwd, filePath))) {
169
+ actions.push({ type: "rm", path: filePath });
170
+ removed.push(filePath);
171
+ }
172
+ }
173
+ return { actions, removed };
174
+ }
175
+ function getClaudeParentDirectoryForCleanup(filePath) {
176
+ if (!filePath.startsWith(".claude/")) return void 0;
177
+ const parentDirectory = filePath.slice(0, Math.max(0, filePath.lastIndexOf("/")));
178
+ if (!parentDirectory || parentDirectory === ".claude" || parentDirectory === ".claude/skills" || parentDirectory === ".claude/commands") {
179
+ return void 0;
180
+ }
181
+ return parentDirectory;
182
+ }
183
+ async function reconcile(schema, mode, ctx, options) {
184
+ const dryRun = options?.dryRun ?? false;
185
+ const plan = computePlan(schema, mode, ctx);
186
+ if (dryRun) {
187
+ return {
188
+ actions: plan.actions,
189
+ applied: false,
190
+ created: plan.wouldCreate,
191
+ updated: plan.wouldUpdate,
192
+ removed: plan.wouldRemove,
193
+ packagesToInstall: plan.packagesToInstall,
194
+ packagesToRemove: plan.packagesToRemove
195
+ };
196
+ }
197
+ const result = executePlan(plan, ctx);
198
+ return {
199
+ actions: plan.actions,
200
+ applied: true,
201
+ created: result.created,
202
+ updated: result.updated,
203
+ removed: result.removed,
204
+ packagesToInstall: plan.packagesToInstall,
205
+ packagesToRemove: plan.packagesToRemove
206
+ };
207
+ }
208
+ function planDeprecatedFilesRemoval(deprecatedFiles, cwd) {
209
+ const actions = [];
210
+ const removed = [];
211
+ for (const filePath of deprecatedFiles) {
212
+ if (exists(nodePath2.join(cwd, filePath))) {
213
+ actions.push({ type: "rm", path: filePath });
214
+ removed.push(filePath);
215
+ }
216
+ }
217
+ return { actions, removed };
218
+ }
219
+ function computePlan(schema, mode, ctx) {
220
+ switch (mode) {
221
+ case "install": {
222
+ return computeInstallPlan(schema, ctx);
223
+ }
224
+ case "upgrade": {
225
+ return computeUpgradePlan(schema, ctx);
226
+ }
227
+ case "uninstall": {
228
+ return computeUninstallPlan(schema, ctx, false);
229
+ }
230
+ case "uninstall-full": {
231
+ return computeUninstallPlan(schema, ctx, true);
232
+ }
233
+ default: {
234
+ const _exhaustiveCheck = mode;
235
+ return _exhaustiveCheck;
236
+ }
237
+ }
238
+ }
239
+ function computeInstallPlan(schema, ctx) {
240
+ const actions = [];
241
+ const wouldCreate = [];
242
+ const allDirectories = [...schema.ownedDirs, ...schema.sharedDirs, ...schema.preservedDirs];
243
+ const dirs = planMissingDirectories(allDirectories, ctx.cwd, ctx.isGitRepo);
244
+ actions.push(...dirs.actions);
245
+ wouldCreate.push(...dirs.created);
246
+ const owned = planOwnedFileWrites(schema.ownedFiles, ctx);
247
+ actions.push(...owned.actions);
248
+ wouldCreate.push(...owned.created);
249
+ const managed = planManagedFileWrites(schema.managedFiles, ctx);
250
+ actions.push(...managed.actions);
251
+ wouldCreate.push(...managed.created);
252
+ const chmodPaths = [".safeword/hooks", ".safeword/hooks/cursor", ".safeword/lib", ".safeword/scripts"];
253
+ if (ctx.isGitRepo) chmodPaths.push(HUSKY_DIR);
254
+ actions.push({ type: "chmod", paths: chmodPaths });
255
+ for (const [filePath, definition] of Object.entries(schema.jsonMerges)) {
256
+ actions.push({ type: "json-merge", path: filePath, definition });
257
+ }
258
+ actions.push(...planTextPatchesWithCreation(schema.textPatches, ctx, wouldCreate));
259
+ const packagesToInstall = computePackagesToInstall(schema, ctx.projectType, ctx.developmentDeps, ctx.isGitRepo);
260
+ return { actions, wouldCreate, wouldUpdate: [], wouldRemove: [], packagesToInstall, packagesToRemove: [] };
261
+ }
262
+ function computeUpgradePlan(schema, ctx) {
263
+ const actions = [];
264
+ const wouldCreate = [];
265
+ const wouldUpdate = [];
266
+ const allDirectories = [...schema.ownedDirs, ...schema.sharedDirs, ...schema.preservedDirs];
267
+ const missingDirectories = planMissingDirectories(allDirectories, ctx.cwd, ctx.isGitRepo);
268
+ actions.push(...missingDirectories.actions);
269
+ wouldCreate.push(...missingDirectories.created);
270
+ for (const [filePath, definition] of Object.entries(schema.ownedFiles)) {
271
+ if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;
272
+ const fullPath = nodePath2.join(ctx.cwd, filePath);
273
+ const newContent = resolveFileContent(definition, ctx);
274
+ if (!fileNeedsUpdate(fullPath, newContent)) continue;
275
+ actions.push({ type: "write", path: filePath, content: newContent });
276
+ if (exists(fullPath)) {
277
+ wouldUpdate.push(filePath);
278
+ } else {
279
+ wouldCreate.push(filePath);
280
+ }
281
+ }
282
+ for (const [filePath, definition] of Object.entries(schema.managedFiles)) {
283
+ const fullPath = nodePath2.join(ctx.cwd, filePath);
284
+ const newContent = resolveFileContent(definition, ctx);
285
+ if (!exists(fullPath)) {
286
+ actions.push({ type: "write", path: filePath, content: newContent });
287
+ wouldCreate.push(filePath);
288
+ }
289
+ }
290
+ const deprecatedFiles = planDeprecatedFilesRemoval(schema.deprecatedFiles, ctx.cwd);
291
+ actions.push(...deprecatedFiles.actions);
292
+ const wouldRemove = deprecatedFiles.removed;
293
+ const deprecatedDirectories = planExistingDirectoriesRemoval(schema.deprecatedDirs, ctx.cwd);
294
+ actions.push(...deprecatedDirectories.actions);
295
+ wouldRemove.push(...deprecatedDirectories.removed);
296
+ const chmodPathsUpgrade = [
297
+ ".safeword/hooks",
298
+ ".safeword/hooks/cursor",
299
+ ".safeword/lib",
300
+ ".safeword/scripts"
301
+ ];
302
+ actions.push({ type: "chmod", paths: chmodPathsUpgrade });
303
+ for (const [filePath, definition] of Object.entries(schema.jsonMerges)) {
304
+ actions.push({ type: "json-merge", path: filePath, definition });
305
+ }
306
+ actions.push(...planTextPatches(schema.textPatches, ctx.cwd, ctx.isGitRepo));
307
+ const packagesToInstall = computePackagesToInstall(
308
+ schema,
309
+ ctx.projectType,
310
+ ctx.developmentDeps,
311
+ ctx.isGitRepo
312
+ );
313
+ const packagesToRemove = schema.deprecatedPackages.filter((pkg) => pkg in ctx.developmentDeps);
314
+ return {
315
+ actions,
316
+ wouldCreate,
317
+ wouldUpdate,
318
+ wouldRemove,
319
+ packagesToInstall,
320
+ packagesToRemove
321
+ };
322
+ }
323
+ function computeUninstallPlan(schema, ctx, full) {
324
+ const actions = [];
325
+ const wouldRemove = [];
326
+ const ownedFiles = planExistingFilesRemoval(Object.keys(schema.ownedFiles), ctx.cwd);
327
+ actions.push(...ownedFiles.actions);
328
+ wouldRemove.push(...ownedFiles.removed);
329
+ const directoriesToCleanup = /* @__PURE__ */ new Set();
330
+ for (const filePath of ownedFiles.removed) {
331
+ const parentDirectory = getClaudeParentDirectoryForCleanup(filePath);
332
+ if (parentDirectory) directoriesToCleanup.add(parentDirectory);
333
+ }
334
+ const cleanupDirectories = planExistingDirectoriesRemoval([...directoriesToCleanup], ctx.cwd);
335
+ actions.push(...cleanupDirectories.actions);
336
+ wouldRemove.push(...cleanupDirectories.removed);
337
+ for (const [filePath, definition] of Object.entries(schema.jsonMerges)) {
338
+ actions.push({ type: "json-unmerge", path: filePath, definition });
339
+ }
340
+ for (const [filePath, definition] of Object.entries(schema.textPatches)) {
341
+ const fullPath = nodePath2.join(ctx.cwd, filePath);
342
+ if (exists(fullPath)) {
343
+ const content = readFileSafe(fullPath) ?? "";
344
+ if (content.includes(definition.marker)) {
345
+ actions.push({ type: "text-unpatch", path: filePath, definition });
346
+ }
347
+ }
348
+ }
349
+ const preserved = planExistingDirectoriesRemoval(schema.preservedDirs.toReversed(), ctx.cwd);
350
+ actions.push(...preserved.actions);
351
+ wouldRemove.push(...preserved.removed);
352
+ const owned = planExistingDirectoriesRemoval(schema.ownedDirs.toReversed(), ctx.cwd);
353
+ actions.push(...owned.actions);
354
+ wouldRemove.push(...owned.removed);
355
+ if (full) {
356
+ const managed = planExistingFilesRemoval(Object.keys(schema.managedFiles), ctx.cwd);
357
+ actions.push(...managed.actions);
358
+ wouldRemove.push(...managed.removed);
359
+ }
360
+ const packagesToRemove = full ? computePackagesToRemove(schema, ctx.projectType, ctx.developmentDeps) : [];
361
+ return {
362
+ actions,
363
+ wouldCreate: [],
364
+ wouldUpdate: [],
365
+ wouldRemove,
366
+ packagesToInstall: [],
367
+ packagesToRemove
368
+ };
369
+ }
370
+ function executePlan(plan, ctx) {
371
+ const created = [];
372
+ const updated = [];
373
+ const removed = [];
374
+ const result = { created, updated, removed };
375
+ for (const action of plan.actions) {
376
+ executeAction(action, ctx, result);
377
+ }
378
+ return result;
379
+ }
380
+ function executeChmod(cwd, paths) {
381
+ for (const path of paths) {
382
+ const fullPath = nodePath2.join(cwd, path);
383
+ if (exists(fullPath)) makeScriptsExecutable(fullPath);
384
+ }
385
+ }
386
+ function executeRmdir(cwd, path, result) {
387
+ if (removeIfEmpty(nodePath2.join(cwd, path))) result.removed.push(path);
388
+ }
389
+ function executeAction(action, ctx, result) {
390
+ switch (action.type) {
391
+ case "mkdir":
392
+ ensureDirectory(nodePath2.join(ctx.cwd, action.path));
393
+ result.created.push(action.path);
394
+ break;
395
+ case "rmdir":
396
+ executeRmdir(ctx.cwd, action.path, result);
397
+ break;
398
+ case "write":
399
+ executeWrite(ctx.cwd, action.path, action.content, result);
400
+ break;
401
+ case "rm":
402
+ remove(nodePath2.join(ctx.cwd, action.path));
403
+ result.removed.push(action.path);
404
+ break;
405
+ case "chmod":
406
+ executeChmod(ctx.cwd, action.paths);
407
+ break;
408
+ case "json-merge":
409
+ executeJsonMerge(ctx.cwd, action.path, action.definition, ctx);
410
+ break;
411
+ case "json-unmerge":
412
+ executeJsonUnmerge(ctx.cwd, action.path, action.definition);
413
+ break;
414
+ case "text-patch":
415
+ executeTextPatch(ctx.cwd, action.path, action.definition);
416
+ break;
417
+ case "text-unpatch":
418
+ executeTextUnpatch(ctx.cwd, action.path, action.definition);
419
+ break;
420
+ }
421
+ }
422
+ function executeWrite(cwd, path, content, result) {
423
+ const fullPath = nodePath2.join(cwd, path);
424
+ const existed = exists(fullPath);
425
+ writeFile(fullPath, content);
426
+ (existed ? result.updated : result.created).push(path);
427
+ }
428
+ function resolveFileContent(definition, ctx) {
429
+ if (definition.template) {
430
+ const templatesDirectory = getTemplatesDirectory();
431
+ return readFile(nodePath2.join(templatesDirectory, definition.template));
432
+ }
433
+ if (definition.content) {
434
+ return typeof definition.content === "function" ? definition.content() : definition.content;
435
+ }
436
+ if (definition.generator) {
437
+ return definition.generator(ctx);
438
+ }
439
+ throw new Error("FileDefinition must have template, content, or generator");
440
+ }
441
+ function fileNeedsUpdate(installedPath, newContent) {
442
+ if (!exists(installedPath)) return true;
443
+ const currentContent = readFileSafe(installedPath);
444
+ return currentContent?.trim() !== newContent.trim();
445
+ }
446
+ var GIT_ONLY_PACKAGES = /* @__PURE__ */ new Set(["husky", "lint-staged"]);
447
+ function computePackagesToInstall(schema, projectType, installedDevelopmentDeps, isGitRepo2 = true) {
448
+ let needed = [...schema.packages.base];
449
+ if (!isGitRepo2) {
450
+ needed = needed.filter((pkg) => !GIT_ONLY_PACKAGES.has(pkg));
451
+ }
452
+ for (const [key, deps] of Object.entries(schema.packages.conditional)) {
453
+ if (projectType[key]) {
454
+ needed.push(...deps);
455
+ }
456
+ }
457
+ return needed.filter((pkg) => !(pkg in installedDevelopmentDeps));
458
+ }
459
+ function computePackagesToRemove(schema, projectType, installedDevelopmentDeps) {
460
+ const safewordPackages = [...schema.packages.base];
461
+ for (const [key, deps] of Object.entries(schema.packages.conditional)) {
462
+ if (projectType[key]) {
463
+ safewordPackages.push(...deps);
464
+ }
465
+ }
466
+ return safewordPackages.filter((pkg) => pkg in installedDevelopmentDeps);
467
+ }
468
+ function executeJsonMerge(cwd, path, definition, ctx) {
469
+ const fullPath = nodePath2.join(cwd, path);
470
+ const existing = readJson(fullPath) ?? {};
471
+ const merged = definition.merge(existing, ctx);
472
+ if (JSON.stringify(existing) === JSON.stringify(merged)) return;
473
+ writeJson(fullPath, merged);
474
+ }
475
+ function executeJsonUnmerge(cwd, path, definition) {
476
+ const fullPath = nodePath2.join(cwd, path);
477
+ if (!exists(fullPath)) return;
478
+ const existing = readJson(fullPath);
479
+ if (!existing) return;
480
+ const unmerged = definition.unmerge(existing);
481
+ if (definition.removeFileIfEmpty) {
482
+ const remainingKeys = Object.keys(unmerged).filter((k) => unmerged[k] !== void 0);
483
+ if (remainingKeys.length === 0) {
484
+ remove(fullPath);
485
+ return;
486
+ }
487
+ }
488
+ writeJson(fullPath, unmerged);
489
+ }
490
+ function executeTextPatch(cwd, path, definition) {
491
+ const fullPath = nodePath2.join(cwd, path);
492
+ let content = readFileSafe(fullPath) ?? "";
493
+ if (content.includes(definition.marker)) return;
494
+ content = definition.operation === "prepend" ? definition.content + content : content + definition.content;
495
+ writeFile(fullPath, content);
496
+ }
497
+ function executeTextUnpatch(cwd, path, definition) {
498
+ const fullPath = nodePath2.join(cwd, path);
499
+ const content = readFileSafe(fullPath);
500
+ if (!content) return;
501
+ let unpatched = content.replace(definition.content, "");
502
+ if (unpatched === content && content.includes(definition.marker)) {
503
+ const lines = content.split("\n");
504
+ const filtered = lines.filter((line) => !line.includes(definition.marker));
505
+ unpatched = filtered.join("\n").replace(/^\n+/, "");
506
+ }
507
+ writeFile(fullPath, unpatched);
508
+ }
509
+
510
+ // src/templates/config.ts
511
+ function getEslintConfig() {
512
+ return `import { readFileSync } from "fs";
513
+ import { dirname, join } from "path";
514
+ import { fileURLToPath } from "url";
515
+ import safeword from "eslint-plugin-safeword";
516
+ import eslintConfigPrettier from "eslint-config-prettier";
517
+
518
+ // Read package.json relative to this config file (not CWD)
519
+ const __dirname = dirname(fileURLToPath(import.meta.url));
520
+ const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
521
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
522
+
523
+ // Build dynamic ignores based on detected frameworks
524
+ const ignores = ["**/node_modules/", "**/dist/", "**/build/", "**/coverage/"];
525
+ if (deps["next"]) ignores.push(".next/");
526
+ if (deps["astro"]) ignores.push(".astro/");
527
+
528
+ // Select appropriate safeword config based on detected framework
529
+ // Order matters: most specific first
530
+ let baseConfig;
531
+ if (deps["next"]) {
532
+ baseConfig = safeword.configs.recommendedTypeScriptNext;
533
+ } else if (deps["react"]) {
534
+ baseConfig = safeword.configs.recommendedTypeScriptReact;
535
+ } else if (deps["astro"]) {
536
+ baseConfig = safeword.configs.astro;
537
+ } else if (deps["typescript"] || deps["typescript-eslint"]) {
538
+ baseConfig = safeword.configs.recommendedTypeScript;
539
+ } else {
540
+ baseConfig = safeword.configs.recommended;
541
+ }
542
+
543
+ // Start with ignores + safeword config
544
+ const configs = [
545
+ { ignores },
546
+ ...baseConfig,
547
+ ];
548
+
549
+ // Add test configs if testing frameworks detected
550
+ if (deps["vitest"]) {
551
+ configs.push(...safeword.configs.vitest);
552
+ }
553
+ if (deps["playwright"] || deps["@playwright/test"]) {
554
+ configs.push(...safeword.configs.playwright);
555
+ }
556
+
557
+ // eslint-config-prettier must be last to disable conflicting rules
558
+ configs.push(eslintConfigPrettier);
559
+
560
+ export default configs;
561
+ `;
562
+ }
563
+ var CURSOR_HOOKS = {
564
+ afterFileEdit: [{ command: "./.safeword/hooks/cursor/after-file-edit.sh" }],
565
+ stop: [{ command: "./.safeword/hooks/cursor/stop.sh" }]
566
+ };
567
+ var SETTINGS_HOOKS = {
568
+ SessionStart: [
569
+ {
570
+ hooks: [
571
+ {
572
+ type: "command",
573
+ command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/session-verify-agents.sh'
574
+ }
575
+ ]
576
+ },
577
+ {
578
+ hooks: [
579
+ {
580
+ type: "command",
581
+ command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/session-version.sh'
582
+ }
583
+ ]
584
+ },
585
+ {
586
+ hooks: [
587
+ {
588
+ type: "command",
589
+ command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/session-lint-check.sh'
590
+ }
591
+ ]
592
+ }
593
+ ],
594
+ UserPromptSubmit: [
595
+ {
596
+ hooks: [
597
+ {
598
+ type: "command",
599
+ command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/prompt-timestamp.sh'
600
+ }
601
+ ]
602
+ },
603
+ {
604
+ hooks: [
605
+ {
606
+ type: "command",
607
+ command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/prompt-questions.sh'
608
+ }
609
+ ]
610
+ }
611
+ ],
612
+ Stop: [
613
+ {
614
+ hooks: [
615
+ {
616
+ type: "command",
617
+ command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/stop-quality.sh'
618
+ }
619
+ ]
620
+ }
621
+ ],
622
+ PostToolUse: [
623
+ {
624
+ matcher: "Write|Edit|MultiEdit|NotebookEdit",
625
+ hooks: [
626
+ {
627
+ type: "command",
628
+ command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/post-tool-lint.sh'
629
+ }
630
+ ]
631
+ }
632
+ ]
633
+ };
634
+
635
+ // src/templates/content.ts
636
+ var AGENTS_MD_LINK = `**\u26A0\uFE0F ALWAYS READ FIRST:** \`.safeword/SAFEWORD.md\`
637
+
638
+ The SAFEWORD.md file contains core development patterns, workflows, and conventions.
639
+ Read it BEFORE working on any task in this project.
640
+
641
+ ---`;
642
+ function getPrettierConfig(projectType) {
643
+ const config = {
644
+ semi: true,
645
+ singleQuote: true,
646
+ tabWidth: 2,
647
+ trailingComma: "all",
648
+ printWidth: 100,
649
+ endOfLine: "lf",
650
+ useTabs: false,
651
+ bracketSpacing: true,
652
+ arrowParens: "avoid"
653
+ };
654
+ const plugins = [];
655
+ if (projectType.astro) plugins.push("prettier-plugin-astro");
656
+ if (projectType.shell) plugins.push("prettier-plugin-sh");
657
+ if (projectType.tailwind) plugins.push("prettier-plugin-tailwindcss");
658
+ if (plugins.length > 0) {
659
+ config.plugins = plugins;
660
+ }
661
+ return `${JSON.stringify(config, void 0, 2)}
662
+ `;
663
+ }
664
+
665
+ // src/utils/hooks.ts
666
+ function isHookEntry(h) {
667
+ return typeof h === "object" && h !== void 0 && "hooks" in h && Array.isArray(h.hooks);
668
+ }
669
+ function isSafewordHook(h) {
670
+ if (!isHookEntry(h)) return false;
671
+ return h.hooks.some((cmd) => typeof cmd.command === "string" && cmd.command.includes(".safeword"));
672
+ }
673
+ function filterOutSafewordHooks(hooks) {
674
+ return hooks.filter((h) => !isSafewordHook(h));
675
+ }
676
+
677
+ // src/utils/install.ts
678
+ var MCP_SERVERS = {
679
+ context7: {
680
+ command: "npx",
681
+ args: ["-y", "@upstash/context7-mcp@latest"]
682
+ },
683
+ playwright: {
684
+ command: "npx",
685
+ args: ["@playwright/mcp@latest"]
686
+ }
687
+ };
688
+
689
+ // src/schema.ts
690
+ var SAFEWORD_SCHEMA = {
691
+ version: VERSION,
692
+ // Directories fully owned by safeword (created on setup, deleted on reset)
693
+ ownedDirs: [
694
+ ".safeword",
695
+ ".safeword/hooks",
696
+ ".safeword/hooks/cursor",
697
+ ".safeword/lib",
698
+ ".safeword/guides",
699
+ ".safeword/templates",
700
+ ".safeword/prompts",
701
+ ".safeword/planning",
702
+ ".safeword/planning/specs",
703
+ ".safeword/planning/test-definitions",
704
+ ".safeword/planning/design",
705
+ ".safeword/planning/issues",
706
+ ".safeword/planning/plans",
707
+ ".safeword/scripts",
708
+ ".cursor",
709
+ ".cursor/rules",
710
+ ".cursor/commands"
711
+ ],
712
+ // Directories we add to but don't own (not deleted on reset)
713
+ sharedDirs: [".claude", ".claude/skills", ".claude/commands"],
714
+ // Created on setup but NOT deleted on reset (preserves user data)
715
+ preservedDirs: [
716
+ ".safeword/learnings",
717
+ ".safeword/tickets",
718
+ ".safeword/tickets/completed",
719
+ ".safeword/logs"
720
+ ],
721
+ // Files to delete on upgrade (renamed or removed in newer versions)
722
+ deprecatedFiles: [
723
+ ".safeword/templates/user-stories-template.md",
724
+ // Consolidated into planning-guide.md and testing-guide.md (v0.8.0)
725
+ ".safeword/guides/development-workflow.md",
726
+ ".safeword/guides/tdd-best-practices.md",
727
+ ".safeword/guides/user-story-guide.md",
728
+ ".safeword/guides/test-definitions-guide.md",
729
+ // Boundaries config now project-specific (v0.9.0)
730
+ ".safeword/eslint-boundaries.config.mjs"
731
+ ],
732
+ // Packages to uninstall on upgrade (consolidated into eslint-plugin-safeword v0.9.0)
733
+ deprecatedPackages: [
734
+ // Individual ESLint plugins now bundled in eslint-plugin-safeword
735
+ "@eslint/js",
736
+ "eslint-plugin-import-x",
737
+ "eslint-import-resolver-typescript",
738
+ "eslint-plugin-sonarjs",
739
+ "eslint-plugin-unicorn",
740
+ "eslint-plugin-boundaries",
741
+ "eslint-plugin-playwright",
742
+ "eslint-plugin-promise",
743
+ "eslint-plugin-regexp",
744
+ "eslint-plugin-jsdoc",
745
+ "eslint-plugin-simple-import-sort",
746
+ "eslint-plugin-security",
747
+ // Conditional ESLint plugins now in safeword
748
+ "typescript-eslint",
749
+ "eslint-plugin-react",
750
+ "eslint-plugin-react-hooks",
751
+ "eslint-plugin-jsx-a11y",
752
+ "@next/eslint-plugin-next",
753
+ "eslint-plugin-astro",
754
+ "@vitest/eslint-plugin",
755
+ // Pre-commit hooks no longer managed by safeword
756
+ "husky",
757
+ "lint-staged"
758
+ ],
759
+ // Directories to delete on upgrade (no longer managed by safeword)
760
+ deprecatedDirs: [
761
+ ".husky"
762
+ // Pre-commit hooks no longer managed by safeword
763
+ ],
764
+ // Files owned by safeword (overwritten on upgrade if content changed)
765
+ ownedFiles: {
766
+ // Core files
767
+ ".safeword/SAFEWORD.md": { template: "SAFEWORD.md" },
768
+ ".safeword/version": { content: () => VERSION },
769
+ // Hooks (7 files)
770
+ ".safeword/hooks/session-verify-agents.sh": { template: "hooks/session-verify-agents.sh" },
771
+ ".safeword/hooks/session-version.sh": { template: "hooks/session-version.sh" },
772
+ ".safeword/hooks/session-lint-check.sh": { template: "hooks/session-lint-check.sh" },
773
+ ".safeword/hooks/prompt-timestamp.sh": { template: "hooks/prompt-timestamp.sh" },
774
+ ".safeword/hooks/prompt-questions.sh": { template: "hooks/prompt-questions.sh" },
775
+ ".safeword/hooks/post-tool-lint.sh": { template: "hooks/post-tool-lint.sh" },
776
+ ".safeword/hooks/stop-quality.sh": { template: "hooks/stop-quality.sh" },
777
+ // Lib (2 files)
778
+ ".safeword/lib/common.sh": { template: "lib/common.sh" },
779
+ ".safeword/lib/jq-fallback.sh": { template: "lib/jq-fallback.sh" },
780
+ // Guides (11 files)
781
+ ".safeword/guides/architecture-guide.md": { template: "guides/architecture-guide.md" },
782
+ ".safeword/guides/cli-reference.md": { template: "guides/cli-reference.md" },
783
+ ".safeword/guides/code-philosophy.md": { template: "guides/code-philosophy.md" },
784
+ ".safeword/guides/context-files-guide.md": { template: "guides/context-files-guide.md" },
785
+ ".safeword/guides/data-architecture-guide.md": {
786
+ template: "guides/data-architecture-guide.md"
787
+ },
788
+ ".safeword/guides/design-doc-guide.md": { template: "guides/design-doc-guide.md" },
789
+ ".safeword/guides/learning-extraction.md": { template: "guides/learning-extraction.md" },
790
+ ".safeword/guides/llm-guide.md": { template: "guides/llm-guide.md" },
791
+ ".safeword/guides/planning-guide.md": { template: "guides/planning-guide.md" },
792
+ ".safeword/guides/testing-guide.md": { template: "guides/testing-guide.md" },
793
+ ".safeword/guides/zombie-process-cleanup.md": { template: "guides/zombie-process-cleanup.md" },
794
+ // Templates (7 files)
795
+ ".safeword/templates/architecture-template.md": {
796
+ template: "doc-templates/architecture-template.md"
797
+ },
798
+ ".safeword/templates/design-doc-template.md": {
799
+ template: "doc-templates/design-doc-template.md"
800
+ },
801
+ ".safeword/templates/task-spec-template.md": {
802
+ template: "doc-templates/task-spec-template.md"
803
+ },
804
+ ".safeword/templates/test-definitions-feature.md": {
805
+ template: "doc-templates/test-definitions-feature.md"
806
+ },
807
+ ".safeword/templates/ticket-template.md": { template: "doc-templates/ticket-template.md" },
808
+ ".safeword/templates/feature-spec-template.md": {
809
+ template: "doc-templates/feature-spec-template.md"
810
+ },
811
+ ".safeword/templates/work-log-template.md": { template: "doc-templates/work-log-template.md" },
812
+ // Prompts (2 files)
813
+ ".safeword/prompts/architecture.md": { template: "prompts/architecture.md" },
814
+ ".safeword/prompts/quality-review.md": { template: "prompts/quality-review.md" },
815
+ // Scripts (4 files)
816
+ ".safeword/scripts/bisect-test-pollution.sh": { template: "scripts/bisect-test-pollution.sh" },
817
+ ".safeword/scripts/bisect-zombie-processes.sh": {
818
+ template: "scripts/bisect-zombie-processes.sh"
819
+ },
820
+ ".safeword/scripts/cleanup-zombies.sh": { template: "scripts/cleanup-zombies.sh" },
821
+ ".safeword/scripts/lint-md.sh": { template: "scripts/lint-md.sh" },
822
+ // Claude skills and commands (9 files)
823
+ ".claude/skills/safeword-brainstorming/SKILL.md": {
824
+ template: "skills/safeword-brainstorming/SKILL.md"
825
+ },
826
+ ".claude/skills/safeword-debugging/SKILL.md": {
827
+ template: "skills/safeword-debugging/SKILL.md"
828
+ },
829
+ ".claude/skills/safeword-enforcing-tdd/SKILL.md": {
830
+ template: "skills/safeword-enforcing-tdd/SKILL.md"
831
+ },
832
+ ".claude/skills/safeword-quality-reviewer/SKILL.md": {
833
+ template: "skills/safeword-quality-reviewer/SKILL.md"
834
+ },
835
+ ".claude/skills/safeword-refactoring/SKILL.md": {
836
+ template: "skills/safeword-refactoring/SKILL.md"
837
+ },
838
+ ".claude/skills/safeword-writing-plans/SKILL.md": {
839
+ template: "skills/safeword-writing-plans/SKILL.md"
840
+ },
841
+ ".claude/commands/architecture.md": { template: "commands/architecture.md" },
842
+ ".claude/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
843
+ ".claude/commands/lint.md": { template: "commands/lint.md" },
844
+ ".claude/commands/quality-review.md": { template: "commands/quality-review.md" },
845
+ // Cursor rules (7 files)
846
+ ".cursor/rules/safeword-core.mdc": { template: "cursor/rules/safeword-core.mdc" },
847
+ ".cursor/rules/safeword-brainstorming.mdc": {
848
+ template: "cursor/rules/safeword-brainstorming.mdc"
849
+ },
850
+ ".cursor/rules/safeword-debugging.mdc": {
851
+ template: "cursor/rules/safeword-debugging.mdc"
852
+ },
853
+ ".cursor/rules/safeword-enforcing-tdd.mdc": {
854
+ template: "cursor/rules/safeword-enforcing-tdd.mdc"
855
+ },
856
+ ".cursor/rules/safeword-quality-reviewer.mdc": {
857
+ template: "cursor/rules/safeword-quality-reviewer.mdc"
858
+ },
859
+ ".cursor/rules/safeword-refactoring.mdc": {
860
+ template: "cursor/rules/safeword-refactoring.mdc"
861
+ },
862
+ ".cursor/rules/safeword-writing-plans.mdc": {
863
+ template: "cursor/rules/safeword-writing-plans.mdc"
864
+ },
865
+ // Cursor commands (4 files - same as Claude)
866
+ ".cursor/commands/architecture.md": { template: "commands/architecture.md" },
867
+ ".cursor/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
868
+ ".cursor/commands/lint.md": { template: "commands/lint.md" },
869
+ ".cursor/commands/quality-review.md": { template: "commands/quality-review.md" },
870
+ // Cursor hooks adapters (2 files)
871
+ ".safeword/hooks/cursor/after-file-edit.sh": { template: "hooks/cursor/after-file-edit.sh" },
872
+ ".safeword/hooks/cursor/stop.sh": { template: "hooks/cursor/stop.sh" }
873
+ },
874
+ // Files created if missing, updated only if content matches current template
875
+ managedFiles: {
876
+ "eslint.config.mjs": {
877
+ generator: () => getEslintConfig()
878
+ },
879
+ ".prettierrc": { generator: (ctx) => getPrettierConfig(ctx.projectType) },
880
+ ".markdownlint-cli2.jsonc": { template: "markdownlint-cli2.jsonc" },
881
+ // Minimal tsconfig for ESLint type-checked linting (only if missing)
882
+ "tsconfig.json": {
883
+ generator: (ctx) => {
884
+ if (!ctx.developmentDeps.typescript && !ctx.developmentDeps["typescript-eslint"]) {
885
+ return "";
886
+ }
887
+ return JSON.stringify(
888
+ {
889
+ compilerOptions: {
890
+ target: "ES2022",
891
+ module: "NodeNext",
892
+ moduleResolution: "NodeNext",
893
+ strict: true,
894
+ esModuleInterop: true,
895
+ skipLibCheck: true,
896
+ noEmit: true
897
+ },
898
+ include: ["**/*.ts", "**/*.tsx"],
899
+ exclude: ["node_modules", "dist", "build"]
900
+ },
901
+ void 0,
902
+ 2
903
+ );
904
+ }
905
+ }
906
+ },
907
+ // JSON files where we merge specific keys
908
+ jsonMerges: {
909
+ "package.json": {
910
+ keys: [
911
+ "scripts.lint",
912
+ "scripts.lint:md",
913
+ "scripts.format",
914
+ "scripts.format:check",
915
+ "scripts.knip"
916
+ ],
917
+ conditionalKeys: {
918
+ publishableLibrary: ["scripts.publint"],
919
+ shell: ["scripts.lint:sh"]
920
+ },
921
+ merge: (existing, ctx) => {
922
+ const scripts = { ...existing.scripts };
923
+ const result = { ...existing };
924
+ if (!scripts.lint) scripts.lint = "eslint .";
925
+ if (!scripts["lint:md"]) scripts["lint:md"] = 'markdownlint-cli2 "**/*.md" "#node_modules"';
926
+ if (!scripts.format) scripts.format = "prettier --write .";
927
+ if (!scripts["format:check"]) scripts["format:check"] = "prettier --check .";
928
+ if (!scripts.knip) scripts.knip = "knip";
929
+ if (ctx.projectType.publishableLibrary && !scripts.publint) {
930
+ scripts.publint = "publint";
931
+ }
932
+ if (ctx.projectType.shell && !scripts["lint:sh"]) {
933
+ scripts["lint:sh"] = "shellcheck **/*.sh";
934
+ }
935
+ result.scripts = scripts;
936
+ return result;
937
+ },
938
+ unmerge: (existing) => {
939
+ const result = { ...existing };
940
+ const scripts = { ...existing.scripts };
941
+ delete scripts["lint:md"];
942
+ delete scripts["lint:sh"];
943
+ delete scripts["format:check"];
944
+ delete scripts.knip;
945
+ delete scripts.publint;
946
+ if (Object.keys(scripts).length > 0) {
947
+ result.scripts = scripts;
948
+ } else {
949
+ delete result.scripts;
950
+ }
951
+ return result;
952
+ }
953
+ },
954
+ ".claude/settings.json": {
955
+ keys: ["hooks"],
956
+ merge: (existing) => {
957
+ const existingHooks = existing.hooks ?? {};
958
+ const mergedHooks = { ...existingHooks };
959
+ for (const [event, newHooks] of Object.entries(SETTINGS_HOOKS)) {
960
+ const eventHooks = mergedHooks[event] ?? [];
961
+ const nonSafewordHooks = filterOutSafewordHooks(eventHooks);
962
+ mergedHooks[event] = [...nonSafewordHooks, ...newHooks];
963
+ }
964
+ return { ...existing, hooks: mergedHooks };
965
+ },
966
+ unmerge: (existing) => {
967
+ const existingHooks = existing.hooks ?? {};
968
+ const cleanedHooks = {};
969
+ for (const [event, eventHooks] of Object.entries(existingHooks)) {
970
+ const nonSafewordHooks = filterOutSafewordHooks(eventHooks);
971
+ if (nonSafewordHooks.length > 0) {
972
+ cleanedHooks[event] = nonSafewordHooks;
973
+ }
974
+ }
975
+ const result = { ...existing };
976
+ if (Object.keys(cleanedHooks).length > 0) {
977
+ result.hooks = cleanedHooks;
978
+ } else {
979
+ delete result.hooks;
980
+ }
981
+ return result;
982
+ }
983
+ },
984
+ ".mcp.json": {
985
+ keys: ["mcpServers.context7", "mcpServers.playwright"],
986
+ removeFileIfEmpty: true,
987
+ merge: (existing) => {
988
+ const mcpServers = existing.mcpServers ?? {};
989
+ return {
990
+ ...existing,
991
+ mcpServers: {
992
+ ...mcpServers,
993
+ context7: MCP_SERVERS.context7,
994
+ playwright: MCP_SERVERS.playwright
995
+ }
996
+ };
997
+ },
998
+ unmerge: (existing) => {
999
+ const result = { ...existing };
1000
+ const mcpServers = { ...existing.mcpServers };
1001
+ delete mcpServers.context7;
1002
+ delete mcpServers.playwright;
1003
+ if (Object.keys(mcpServers).length > 0) {
1004
+ result.mcpServers = mcpServers;
1005
+ } else {
1006
+ delete result.mcpServers;
1007
+ }
1008
+ return result;
1009
+ }
1010
+ },
1011
+ ".cursor/mcp.json": {
1012
+ keys: ["mcpServers.context7", "mcpServers.playwright"],
1013
+ removeFileIfEmpty: true,
1014
+ merge: (existing) => {
1015
+ const mcpServers = existing.mcpServers ?? {};
1016
+ return {
1017
+ ...existing,
1018
+ mcpServers: {
1019
+ ...mcpServers,
1020
+ context7: MCP_SERVERS.context7,
1021
+ playwright: MCP_SERVERS.playwright
1022
+ }
1023
+ };
1024
+ },
1025
+ unmerge: (existing) => {
1026
+ const result = { ...existing };
1027
+ const mcpServers = { ...existing.mcpServers };
1028
+ delete mcpServers.context7;
1029
+ delete mcpServers.playwright;
1030
+ if (Object.keys(mcpServers).length > 0) {
1031
+ result.mcpServers = mcpServers;
1032
+ } else {
1033
+ delete result.mcpServers;
1034
+ }
1035
+ return result;
1036
+ }
1037
+ },
1038
+ ".cursor/hooks.json": {
1039
+ keys: ["version", "hooks.afterFileEdit", "hooks.stop"],
1040
+ removeFileIfEmpty: true,
1041
+ merge: (existing) => {
1042
+ const hooks = existing.hooks ?? {};
1043
+ return {
1044
+ ...existing,
1045
+ version: 1,
1046
+ // Required by Cursor
1047
+ hooks: {
1048
+ ...hooks,
1049
+ ...CURSOR_HOOKS
1050
+ }
1051
+ };
1052
+ },
1053
+ unmerge: (existing) => {
1054
+ const result = { ...existing };
1055
+ const hooks = { ...existing.hooks };
1056
+ delete hooks.afterFileEdit;
1057
+ delete hooks.stop;
1058
+ if (Object.keys(hooks).length > 0) {
1059
+ result.hooks = hooks;
1060
+ } else {
1061
+ delete result.hooks;
1062
+ delete result.version;
1063
+ }
1064
+ return result;
1065
+ }
1066
+ }
1067
+ },
1068
+ // Text files where we patch specific content
1069
+ textPatches: {
1070
+ "AGENTS.md": {
1071
+ operation: "prepend",
1072
+ content: AGENTS_MD_LINK,
1073
+ marker: ".safeword/SAFEWORD.md",
1074
+ createIfMissing: true
1075
+ },
1076
+ "CLAUDE.md": {
1077
+ operation: "prepend",
1078
+ content: AGENTS_MD_LINK,
1079
+ marker: ".safeword/SAFEWORD.md",
1080
+ createIfMissing: false
1081
+ // Only patch if exists, don't create (AGENTS.md is primary)
1082
+ }
1083
+ },
1084
+ // NPM packages to install
1085
+ packages: {
1086
+ base: [
1087
+ // Core tools
1088
+ "eslint",
1089
+ "prettier",
1090
+ // Safeword plugin (bundles eslint-config-prettier + all ESLint plugins)
1091
+ "eslint-plugin-safeword",
1092
+ // Non-ESLint tools
1093
+ "markdownlint-cli2",
1094
+ "knip"
1095
+ ],
1096
+ conditional: {
1097
+ // Prettier plugins
1098
+ astro: ["prettier-plugin-astro"],
1099
+ tailwind: ["prettier-plugin-tailwindcss"],
1100
+ // Non-ESLint tools
1101
+ publishableLibrary: ["publint"],
1102
+ shell: ["shellcheck", "prettier-plugin-sh"]
1103
+ }
1104
+ }
1105
+ };
1106
+
1107
+ // src/utils/git.ts
1108
+ import nodePath3 from "path";
1109
+ function isGitRepo(cwd) {
1110
+ return exists(nodePath3.join(cwd, ".git"));
1111
+ }
1112
+
1113
+ // src/utils/context.ts
1114
+ import nodePath5 from "path";
1115
+
1116
+ // src/utils/project-detector.ts
1117
+ import { readdirSync as readdirSync2 } from "fs";
1118
+ import nodePath4 from "path";
1119
+ function hasShellScripts(cwd, maxDepth = 4) {
1120
+ const excludeDirectories = /* @__PURE__ */ new Set(["node_modules", ".git", ".safeword"]);
1121
+ function scan(dir, depth) {
1122
+ if (depth > maxDepth) return false;
1123
+ try {
1124
+ const entries = readdirSync2(dir, { withFileTypes: true });
1125
+ for (const entry of entries) {
1126
+ if (entry.isFile() && entry.name.endsWith(".sh")) {
1127
+ return true;
1128
+ }
1129
+ if (entry.isDirectory() && !excludeDirectories.has(entry.name) && scan(nodePath4.join(dir, entry.name), depth + 1)) {
1130
+ return true;
1131
+ }
1132
+ }
1133
+ } catch {
1134
+ }
1135
+ return false;
1136
+ }
1137
+ return scan(cwd, 0);
1138
+ }
1139
+ function detectProjectType(packageJson, cwd) {
1140
+ const deps = packageJson.dependencies || {};
1141
+ const developmentDeps = packageJson.devDependencies || {};
1142
+ const allDeps = { ...deps, ...developmentDeps };
1143
+ const hasTypescript = "typescript" in allDeps;
1144
+ const hasReact = "react" in deps || "react" in developmentDeps;
1145
+ const hasNextJs = "next" in deps;
1146
+ const hasAstro = "astro" in deps || "astro" in developmentDeps;
1147
+ const hasVitest = "vitest" in developmentDeps;
1148
+ const hasPlaywright = "@playwright/test" in developmentDeps;
1149
+ const hasTailwind = "tailwindcss" in allDeps;
1150
+ const hasEntryPoints = !!(packageJson.main || packageJson.module || packageJson.exports);
1151
+ const isPublishable = hasEntryPoints && packageJson.private !== true;
1152
+ const hasShell = cwd ? hasShellScripts(cwd) : false;
1153
+ return {
1154
+ typescript: hasTypescript,
1155
+ react: hasReact || hasNextJs,
1156
+ // Next.js implies React
1157
+ nextjs: hasNextJs,
1158
+ astro: hasAstro,
1159
+ vitest: hasVitest,
1160
+ playwright: hasPlaywright,
1161
+ tailwind: hasTailwind,
1162
+ publishableLibrary: isPublishable,
1163
+ shell: hasShell
1164
+ };
1165
+ }
1166
+
1167
+ // src/utils/context.ts
1168
+ function createProjectContext(cwd) {
1169
+ const packageJson = readJson(nodePath5.join(cwd, "package.json"));
1170
+ return {
1171
+ cwd,
1172
+ projectType: detectProjectType(packageJson ?? {}, cwd),
1173
+ developmentDeps: packageJson?.devDependencies ?? {},
1174
+ isGitRepo: isGitRepo(cwd)
1175
+ };
1176
+ }
1177
+
1178
+ // src/utils/output.ts
1179
+ function info(message) {
1180
+ console.log(message);
1181
+ }
1182
+ function success(message) {
1183
+ console.log(`\u2713 ${message}`);
1184
+ }
1185
+ function warn(message) {
1186
+ console.warn(`\u26A0 ${message}`);
1187
+ }
1188
+ function error(message) {
1189
+ console.error(`\u2717 ${message}`);
1190
+ }
1191
+ function header(title) {
1192
+ console.log(`
1193
+ ${title}`);
1194
+ console.log("\u2500".repeat(title.length));
1195
+ }
1196
+ function listItem(item, indent = 2) {
1197
+ console.log(`${" ".repeat(indent)}\u2022 ${item}`);
1198
+ }
1199
+ function keyValue(key, value) {
1200
+ console.log(` ${key}: ${value}`);
1201
+ }
1202
+
1203
+ export {
1204
+ exists,
1205
+ readFileSafe,
1206
+ writeJson,
1207
+ reconcile,
1208
+ SAFEWORD_SCHEMA,
1209
+ isGitRepo,
1210
+ createProjectContext,
1211
+ info,
1212
+ success,
1213
+ warn,
1214
+ error,
1215
+ header,
1216
+ listItem,
1217
+ keyValue
1218
+ };
1219
+ //# sourceMappingURL=chunk-POPS3ZRQ.js.map