safeword 0.10.1 → 0.11.1

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.
@@ -1,96 +1,48 @@
1
1
  import {
2
2
  VERSION
3
3
  } from "./chunk-ORQHKDT2.js";
4
-
5
- // src/utils/fs.ts
6
4
  import {
7
- chmodSync,
8
- existsSync,
9
- mkdirSync,
10
- readdirSync,
11
- readFileSync,
12
- rmdirSync,
13
- rmSync,
14
- writeFileSync
15
- } from "fs";
5
+ ensureDirectory,
6
+ exists,
7
+ getTemplatesDirectory,
8
+ makeScriptsExecutable,
9
+ readFile,
10
+ readFileSafe,
11
+ readJson,
12
+ remove,
13
+ removeIfEmpty,
14
+ writeFile,
15
+ writeJson
16
+ } from "./chunk-DYLHQBW3.js";
17
+
18
+ // src/reconcile.ts
16
19
  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;
20
+ var HUSKY_DIR = ".husky";
21
+ var PRETTIER_PACKAGES = /* @__PURE__ */ new Set([
22
+ "prettier",
23
+ "prettier-plugin-astro",
24
+ "prettier-plugin-tailwindcss",
25
+ "prettier-plugin-sh"
26
+ ]);
27
+ function getConditionalPackages(conditionalPackages, projectType) {
28
+ const packages = [];
29
+ for (const [key, deps] of Object.entries(conditionalPackages)) {
30
+ if (key === "standard") {
31
+ if (!projectType.existingFormatter) {
32
+ packages.push(...deps);
33
+ }
34
+ continue;
32
35
  }
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);
36
+ if (projectType[key]) {
37
+ if (projectType.existingFormatter) {
38
+ packages.push(...deps.filter((pkg) => !PRETTIER_PACKAGES.has(pkg)));
39
+ } else {
40
+ packages.push(...deps);
41
+ }
74
42
  }
75
43
  }
44
+ return packages;
76
45
  }
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
46
  function shouldSkipForNonGit(path, isGitRepo2) {
95
47
  return path.startsWith(HUSKY_DIR) && !isGitRepo2;
96
48
  }
@@ -99,7 +51,7 @@ function planMissingDirectories(directories, cwd, isGitRepo2) {
99
51
  const created = [];
100
52
  for (const dir of directories) {
101
53
  if (shouldSkipForNonGit(dir, isGitRepo2)) continue;
102
- if (!exists(nodePath2.join(cwd, dir))) {
54
+ if (!exists(nodePath.join(cwd, dir))) {
103
55
  actions.push({ type: "mkdir", path: dir });
104
56
  created.push(dir);
105
57
  }
@@ -110,7 +62,7 @@ function planTextPatches(patches, cwd, isGitRepo2) {
110
62
  const actions = [];
111
63
  for (const [filePath, definition] of Object.entries(patches)) {
112
64
  if (shouldSkipForNonGit(filePath, isGitRepo2)) continue;
113
- const content = readFileSafe(nodePath2.join(cwd, filePath)) ?? "";
65
+ const content = readFileSafe(nodePath.join(cwd, filePath)) ?? "";
114
66
  if (!content.includes(definition.marker)) {
115
67
  actions.push({ type: "text-patch", path: filePath, definition });
116
68
  }
@@ -132,7 +84,7 @@ function planManagedFileWrites(files, ctx) {
132
84
  const actions = [];
133
85
  const created = [];
134
86
  for (const [filePath, definition] of Object.entries(files)) {
135
- if (exists(nodePath2.join(ctx.cwd, filePath))) continue;
87
+ if (exists(nodePath.join(ctx.cwd, filePath))) continue;
136
88
  const content = resolveFileContent(definition, ctx);
137
89
  actions.push({ type: "write", path: filePath, content });
138
90
  created.push(filePath);
@@ -145,7 +97,7 @@ function planTextPatchesWithCreation(patches, ctx) {
145
97
  for (const [filePath, definition] of Object.entries(patches)) {
146
98
  if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;
147
99
  actions.push({ type: "text-patch", path: filePath, definition });
148
- if (definition.createIfMissing && !exists(nodePath2.join(ctx.cwd, filePath))) {
100
+ if (definition.createIfMissing && !exists(nodePath.join(ctx.cwd, filePath))) {
149
101
  created.push(filePath);
150
102
  }
151
103
  }
@@ -155,7 +107,7 @@ function planExistingDirectoriesRemoval(directories, cwd) {
155
107
  const actions = [];
156
108
  const removed = [];
157
109
  for (const dir of directories) {
158
- if (exists(nodePath2.join(cwd, dir))) {
110
+ if (exists(nodePath.join(cwd, dir))) {
159
111
  actions.push({ type: "rmdir", path: dir });
160
112
  removed.push(dir);
161
113
  }
@@ -166,7 +118,7 @@ function planExistingFilesRemoval(files, cwd) {
166
118
  const actions = [];
167
119
  const removed = [];
168
120
  for (const filePath of files) {
169
- if (exists(nodePath2.join(cwd, filePath))) {
121
+ if (exists(nodePath.join(cwd, filePath))) {
170
122
  actions.push({ type: "rm", path: filePath });
171
123
  removed.push(filePath);
172
124
  }
@@ -210,7 +162,7 @@ function planDeprecatedFilesRemoval(deprecatedFiles, cwd) {
210
162
  const actions = [];
211
163
  const removed = [];
212
164
  for (const filePath of deprecatedFiles) {
213
- if (exists(nodePath2.join(cwd, filePath))) {
165
+ if (exists(nodePath.join(cwd, filePath))) {
214
166
  actions.push({ type: "rm", path: filePath });
215
167
  removed.push(filePath);
216
168
  }
@@ -241,16 +193,21 @@ function computeInstallPlan(schema, ctx) {
241
193
  const actions = [];
242
194
  const wouldCreate = [];
243
195
  const allDirectories = [...schema.ownedDirs, ...schema.sharedDirs, ...schema.preservedDirs];
244
- const dirs = planMissingDirectories(allDirectories, ctx.cwd, ctx.isGitRepo);
245
- actions.push(...dirs.actions);
246
- wouldCreate.push(...dirs.created);
196
+ const directories = planMissingDirectories(allDirectories, ctx.cwd, ctx.isGitRepo);
197
+ actions.push(...directories.actions);
198
+ wouldCreate.push(...directories.created);
247
199
  const owned = planOwnedFileWrites(schema.ownedFiles, ctx);
248
200
  actions.push(...owned.actions);
249
201
  wouldCreate.push(...owned.created);
250
202
  const managed = planManagedFileWrites(schema.managedFiles, ctx);
251
203
  actions.push(...managed.actions);
252
204
  wouldCreate.push(...managed.created);
253
- const chmodPaths = [".safeword/hooks", ".safeword/hooks/cursor", ".safeword/lib", ".safeword/scripts"];
205
+ const chmodPaths = [
206
+ ".safeword/hooks",
207
+ ".safeword/hooks/cursor",
208
+ ".safeword/lib",
209
+ ".safeword/scripts"
210
+ ];
254
211
  if (ctx.isGitRepo) chmodPaths.push(HUSKY_DIR);
255
212
  actions.push({ type: "chmod", paths: chmodPaths });
256
213
  for (const [filePath, definition] of Object.entries(schema.jsonMerges)) {
@@ -259,8 +216,20 @@ function computeInstallPlan(schema, ctx) {
259
216
  const patches = planTextPatchesWithCreation(schema.textPatches, ctx);
260
217
  actions.push(...patches.actions);
261
218
  wouldCreate.push(...patches.created);
262
- const packagesToInstall = computePackagesToInstall(schema, ctx.projectType, ctx.developmentDeps, ctx.isGitRepo);
263
- return { actions, wouldCreate, wouldUpdate: [], wouldRemove: [], packagesToInstall, packagesToRemove: [] };
219
+ const packagesToInstall = computePackagesToInstall(
220
+ schema,
221
+ ctx.projectType,
222
+ ctx.developmentDeps,
223
+ ctx.isGitRepo
224
+ );
225
+ return {
226
+ actions,
227
+ wouldCreate,
228
+ wouldUpdate: [],
229
+ wouldRemove: [],
230
+ packagesToInstall,
231
+ packagesToRemove: []
232
+ };
264
233
  }
265
234
  function computeUpgradePlan(schema, ctx) {
266
235
  const actions = [];
@@ -272,7 +241,7 @@ function computeUpgradePlan(schema, ctx) {
272
241
  wouldCreate.push(...missingDirectories.created);
273
242
  for (const [filePath, definition] of Object.entries(schema.ownedFiles)) {
274
243
  if (shouldSkipForNonGit(filePath, ctx.isGitRepo)) continue;
275
- const fullPath = nodePath2.join(ctx.cwd, filePath);
244
+ const fullPath = nodePath.join(ctx.cwd, filePath);
276
245
  const newContent = resolveFileContent(definition, ctx);
277
246
  if (!fileNeedsUpdate(fullPath, newContent)) continue;
278
247
  actions.push({ type: "write", path: filePath, content: newContent });
@@ -283,7 +252,7 @@ function computeUpgradePlan(schema, ctx) {
283
252
  }
284
253
  }
285
254
  for (const [filePath, definition] of Object.entries(schema.managedFiles)) {
286
- const fullPath = nodePath2.join(ctx.cwd, filePath);
255
+ const fullPath = nodePath.join(ctx.cwd, filePath);
287
256
  const newContent = resolveFileContent(definition, ctx);
288
257
  if (!exists(fullPath)) {
289
258
  actions.push({ type: "write", path: filePath, content: newContent });
@@ -341,7 +310,7 @@ function computeUninstallPlan(schema, ctx, full) {
341
310
  actions.push({ type: "json-unmerge", path: filePath, definition });
342
311
  }
343
312
  for (const [filePath, definition] of Object.entries(schema.textPatches)) {
344
- const fullPath = nodePath2.join(ctx.cwd, filePath);
313
+ const fullPath = nodePath.join(ctx.cwd, filePath);
345
314
  if (exists(fullPath)) {
346
315
  const content = readFileSafe(fullPath) ?? "";
347
316
  if (content.includes(definition.marker)) {
@@ -382,48 +351,57 @@ function executePlan(plan, ctx) {
382
351
  }
383
352
  function executeChmod(cwd, paths) {
384
353
  for (const path of paths) {
385
- const fullPath = nodePath2.join(cwd, path);
354
+ const fullPath = nodePath.join(cwd, path);
386
355
  if (exists(fullPath)) makeScriptsExecutable(fullPath);
387
356
  }
388
357
  }
389
358
  function executeRmdir(cwd, path, result) {
390
- if (removeIfEmpty(nodePath2.join(cwd, path))) result.removed.push(path);
359
+ if (removeIfEmpty(nodePath.join(cwd, path))) result.removed.push(path);
391
360
  }
392
361
  function executeAction(action, ctx, result) {
393
362
  switch (action.type) {
394
- case "mkdir":
395
- ensureDirectory(nodePath2.join(ctx.cwd, action.path));
363
+ case "mkdir": {
364
+ ensureDirectory(nodePath.join(ctx.cwd, action.path));
396
365
  result.created.push(action.path);
397
366
  break;
398
- case "rmdir":
367
+ }
368
+ case "rmdir": {
399
369
  executeRmdir(ctx.cwd, action.path, result);
400
370
  break;
401
- case "write":
371
+ }
372
+ case "write": {
402
373
  executeWrite(ctx.cwd, action.path, action.content, result);
403
374
  break;
404
- case "rm":
405
- remove(nodePath2.join(ctx.cwd, action.path));
375
+ }
376
+ case "rm": {
377
+ remove(nodePath.join(ctx.cwd, action.path));
406
378
  result.removed.push(action.path);
407
379
  break;
408
- case "chmod":
380
+ }
381
+ case "chmod": {
409
382
  executeChmod(ctx.cwd, action.paths);
410
383
  break;
411
- case "json-merge":
384
+ }
385
+ case "json-merge": {
412
386
  executeJsonMerge(ctx.cwd, action.path, action.definition, ctx);
413
387
  break;
414
- case "json-unmerge":
388
+ }
389
+ case "json-unmerge": {
415
390
  executeJsonUnmerge(ctx.cwd, action.path, action.definition);
416
391
  break;
417
- case "text-patch":
392
+ }
393
+ case "text-patch": {
418
394
  executeTextPatch(ctx.cwd, action.path, action.definition);
419
395
  break;
420
- case "text-unpatch":
396
+ }
397
+ case "text-unpatch": {
421
398
  executeTextUnpatch(ctx.cwd, action.path, action.definition);
422
399
  break;
400
+ }
423
401
  }
424
402
  }
425
403
  function executeWrite(cwd, path, content, result) {
426
- const fullPath = nodePath2.join(cwd, path);
404
+ const fullPath = nodePath.join(cwd, path);
427
405
  const existed = exists(fullPath);
428
406
  writeFile(fullPath, content);
429
407
  (existed ? result.updated : result.created).push(path);
@@ -431,7 +409,7 @@ function executeWrite(cwd, path, content, result) {
431
409
  function resolveFileContent(definition, ctx) {
432
410
  if (definition.template) {
433
411
  const templatesDirectory = getTemplatesDirectory();
434
- return readFile(nodePath2.join(templatesDirectory, definition.template));
412
+ return readFile(nodePath.join(templatesDirectory, definition.template));
435
413
  }
436
414
  if (definition.content) {
437
415
  return typeof definition.content === "function" ? definition.content() : definition.content;
@@ -452,31 +430,25 @@ function computePackagesToInstall(schema, projectType, installedDevelopmentDeps,
452
430
  if (!isGitRepo2) {
453
431
  needed = needed.filter((pkg) => !GIT_ONLY_PACKAGES.has(pkg));
454
432
  }
455
- for (const [key, deps] of Object.entries(schema.packages.conditional)) {
456
- if (projectType[key]) {
457
- needed.push(...deps);
458
- }
459
- }
433
+ needed.push(...getConditionalPackages(schema.packages.conditional, projectType));
460
434
  return needed.filter((pkg) => !(pkg in installedDevelopmentDeps));
461
435
  }
462
436
  function computePackagesToRemove(schema, projectType, installedDevelopmentDeps) {
463
- const safewordPackages = [...schema.packages.base];
464
- for (const [key, deps] of Object.entries(schema.packages.conditional)) {
465
- if (projectType[key]) {
466
- safewordPackages.push(...deps);
467
- }
468
- }
437
+ const safewordPackages = [
438
+ ...schema.packages.base,
439
+ ...getConditionalPackages(schema.packages.conditional, projectType)
440
+ ];
469
441
  return safewordPackages.filter((pkg) => pkg in installedDevelopmentDeps);
470
442
  }
471
443
  function executeJsonMerge(cwd, path, definition, ctx) {
472
- const fullPath = nodePath2.join(cwd, path);
444
+ const fullPath = nodePath.join(cwd, path);
473
445
  const existing = readJson(fullPath) ?? {};
474
446
  const merged = definition.merge(existing, ctx);
475
447
  if (JSON.stringify(existing) === JSON.stringify(merged)) return;
476
448
  writeJson(fullPath, merged);
477
449
  }
478
450
  function executeJsonUnmerge(cwd, path, definition) {
479
- const fullPath = nodePath2.join(cwd, path);
451
+ const fullPath = nodePath.join(cwd, path);
480
452
  if (!exists(fullPath)) return;
481
453
  const existing = readJson(fullPath);
482
454
  if (!existing) return;
@@ -491,14 +463,14 @@ function executeJsonUnmerge(cwd, path, definition) {
491
463
  writeJson(fullPath, unmerged);
492
464
  }
493
465
  function executeTextPatch(cwd, path, definition) {
494
- const fullPath = nodePath2.join(cwd, path);
466
+ const fullPath = nodePath.join(cwd, path);
495
467
  let content = readFileSafe(fullPath) ?? "";
496
468
  if (content.includes(definition.marker)) return;
497
469
  content = definition.operation === "prepend" ? definition.content + content : content + definition.content;
498
470
  writeFile(fullPath, content);
499
471
  }
500
472
  function executeTextUnpatch(cwd, path, definition) {
501
- const fullPath = nodePath2.join(cwd, path);
473
+ const fullPath = nodePath.join(cwd, path);
502
474
  const content = readFileSafe(fullPath);
503
475
  if (!content) return;
504
476
  let unpatched = content.replace(definition.content, "");
@@ -511,56 +483,74 @@ function executeTextUnpatch(cwd, path, definition) {
511
483
  }
512
484
 
513
485
  // src/templates/config.ts
514
- function getEslintConfig() {
515
- return `import { readFileSync } from "fs";
516
- import { dirname, join } from "path";
517
- import { fileURLToPath } from "url";
486
+ function getEslintConfig(hasExistingFormatter2 = false) {
487
+ if (hasExistingFormatter2) {
488
+ return getFormatterAgnosticEslintConfig();
489
+ }
490
+ return getStandardEslintConfig();
491
+ }
492
+ function getStandardEslintConfig() {
493
+ return `import { dirname } from "node:path";
494
+ import { fileURLToPath } from "node:url";
518
495
  import safeword from "eslint-plugin-safeword";
519
496
  import eslintConfigPrettier from "eslint-config-prettier";
520
497
 
521
- // Read package.json relative to this config file (not CWD)
498
+ const { detect, configs } = safeword;
522
499
  const __dirname = dirname(fileURLToPath(import.meta.url));
523
- const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
524
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
525
-
526
- // Build dynamic ignores based on detected frameworks
527
- const ignores = ["**/node_modules/", "**/dist/", "**/build/", "**/coverage/"];
528
- if (deps["next"]) ignores.push(".next/");
529
- if (deps["astro"]) ignores.push(".astro/");
500
+ const deps = detect.collectAllDeps(__dirname);
501
+ const framework = detect.detectFramework(deps);
530
502
 
531
- // Select appropriate safeword config based on detected framework
532
- // Order matters: most specific first
533
- let baseConfig;
534
- if (deps["next"]) {
535
- baseConfig = safeword.configs.recommendedTypeScriptNext;
536
- } else if (deps["react"]) {
537
- baseConfig = safeword.configs.recommendedTypeScriptReact;
538
- } else if (deps["astro"]) {
539
- baseConfig = safeword.configs.astro;
540
- } else if (deps["typescript"] || deps["typescript-eslint"]) {
541
- baseConfig = safeword.configs.recommendedTypeScript;
542
- } else {
543
- baseConfig = safeword.configs.recommended;
544
- }
503
+ // Map framework to base config
504
+ // Note: Astro config only lints .astro files, so we combine it with TypeScript config
505
+ // to also lint .ts files in Astro projects
506
+ const baseConfigs = {
507
+ next: configs.recommendedTypeScriptNext,
508
+ react: configs.recommendedTypeScriptReact,
509
+ astro: [...configs.recommendedTypeScript, ...configs.astro],
510
+ typescript: configs.recommendedTypeScript,
511
+ javascript: configs.recommended,
512
+ };
545
513
 
546
- // Start with ignores + safeword config
547
- const configs = [
548
- { ignores },
549
- ...baseConfig,
514
+ export default [
515
+ { ignores: detect.getIgnores(deps) },
516
+ ...baseConfigs[framework],
517
+ ...(detect.hasVitest(deps) ? configs.vitest : []),
518
+ ...(detect.hasPlaywright(deps) ? configs.playwright : []),
519
+ ...(detect.hasTailwind(deps) ? configs.tailwind : []),
520
+ ...(detect.hasTanstackQuery(deps) ? configs.tanstackQuery : []),
521
+ eslintConfigPrettier,
550
522
  ];
551
-
552
- // Add test configs if testing frameworks detected
553
- if (deps["vitest"]) {
554
- configs.push(...safeword.configs.vitest);
555
- }
556
- if (deps["playwright"] || deps["@playwright/test"]) {
557
- configs.push(...safeword.configs.playwright);
523
+ `;
558
524
  }
525
+ function getFormatterAgnosticEslintConfig() {
526
+ return `import { dirname } from "node:path";
527
+ import { fileURLToPath } from "node:url";
528
+ import safeword from "eslint-plugin-safeword";
559
529
 
560
- // eslint-config-prettier must be last to disable conflicting rules
561
- configs.push(eslintConfigPrettier);
530
+ const { detect, configs } = safeword;
531
+ const __dirname = dirname(fileURLToPath(import.meta.url));
532
+ const deps = detect.collectAllDeps(__dirname);
533
+ const framework = detect.detectFramework(deps);
562
534
 
563
- export default configs;
535
+ // Map framework to base config
536
+ // Note: Astro config only lints .astro files, so we combine it with TypeScript config
537
+ // to also lint .ts files in Astro projects
538
+ const baseConfigs = {
539
+ next: configs.recommendedTypeScriptNext,
540
+ react: configs.recommendedTypeScriptReact,
541
+ astro: [...configs.recommendedTypeScript, ...configs.astro],
542
+ typescript: configs.recommendedTypeScript,
543
+ javascript: configs.recommended,
544
+ };
545
+
546
+ export default [
547
+ { ignores: detect.getIgnores(deps) },
548
+ ...baseConfigs[framework],
549
+ ...(detect.hasVitest(deps) ? configs.vitest : []),
550
+ ...(detect.hasPlaywright(deps) ? configs.playwright : []),
551
+ ...(detect.hasTailwind(deps) ? configs.tailwind : []),
552
+ ...(detect.hasTanstackQuery(deps) ? configs.tanstackQuery : []),
553
+ ];
564
554
  `;
565
555
  }
566
556
  var CURSOR_HOOKS = {
@@ -645,7 +635,7 @@ Read it BEFORE working on any task in this project.
645
635
 
646
636
  // src/utils/hooks.ts
647
637
  function isHookEntry(h) {
648
- return typeof h === "object" && h !== void 0 && "hooks" in h && Array.isArray(h.hooks);
638
+ return typeof h === "object" && h !== null && "hooks" in h && Array.isArray(h.hooks);
649
639
  }
650
640
  function isSafewordHook(h) {
651
641
  if (!isHookEntry(h)) return false;
@@ -822,6 +812,7 @@ var SAFEWORD_SCHEMA = {
822
812
  template: "skills/safeword-writing-plans/SKILL.md"
823
813
  },
824
814
  ".claude/commands/architecture.md": { template: "commands/architecture.md" },
815
+ ".claude/commands/audit.md": { template: "commands/audit.md" },
825
816
  ".claude/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
826
817
  ".claude/commands/lint.md": { template: "commands/lint.md" },
827
818
  ".claude/commands/quality-review.md": { template: "commands/quality-review.md" },
@@ -845,8 +836,9 @@ var SAFEWORD_SCHEMA = {
845
836
  ".cursor/rules/safeword-writing-plans.mdc": {
846
837
  template: "cursor/rules/safeword-writing-plans.mdc"
847
838
  },
848
- // Cursor commands (4 files - same as Claude)
839
+ // Cursor commands (5 files - same as Claude)
849
840
  ".cursor/commands/architecture.md": { template: "commands/architecture.md" },
841
+ ".cursor/commands/audit.md": { template: "commands/audit.md" },
850
842
  ".cursor/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
851
843
  ".cursor/commands/lint.md": { template: "commands/lint.md" },
852
844
  ".cursor/commands/quality-review.md": { template: "commands/quality-review.md" },
@@ -857,7 +849,7 @@ var SAFEWORD_SCHEMA = {
857
849
  // Files created if missing, updated only if content matches current template
858
850
  managedFiles: {
859
851
  "eslint.config.mjs": {
860
- generator: () => getEslintConfig()
852
+ generator: (ctx) => getEslintConfig(ctx.projectType.existingFormatter)
861
853
  },
862
854
  // Minimal tsconfig for ESLint type-checked linting (only if missing)
863
855
  "tsconfig.json": {
@@ -883,6 +875,17 @@ var SAFEWORD_SCHEMA = {
883
875
  2
884
876
  );
885
877
  }
878
+ },
879
+ // Knip config for dead code detection (used by /audit)
880
+ "knip.json": {
881
+ generator: () => JSON.stringify(
882
+ {
883
+ ignore: [".safeword/**"],
884
+ ignoreDependencies: ["eslint-plugin-safeword"]
885
+ },
886
+ void 0,
887
+ 2
888
+ )
886
889
  }
887
890
  },
888
891
  // JSON files where we merge specific keys
@@ -890,15 +893,23 @@ var SAFEWORD_SCHEMA = {
890
893
  "package.json": {
891
894
  keys: ["scripts.lint", "scripts.format", "scripts.format:check", "scripts.knip"],
892
895
  conditionalKeys: {
896
+ existingLinter: ["scripts.lint:eslint"],
897
+ // Projects with existing linter get separate ESLint script
893
898
  publishableLibrary: ["scripts.publint"],
894
899
  shell: ["scripts.lint:sh"]
895
900
  },
896
901
  merge: (existing, ctx) => {
897
902
  const scripts = { ...existing.scripts };
898
903
  const result = { ...existing };
899
- if (!scripts.lint) scripts.lint = "eslint .";
900
- if (!scripts.format) scripts.format = "prettier --write .";
901
- if (!scripts["format:check"]) scripts["format:check"] = "prettier --check .";
904
+ if (ctx.projectType.existingLinter) {
905
+ if (!scripts["lint:eslint"]) scripts["lint:eslint"] = "eslint .";
906
+ } else {
907
+ if (!scripts.lint) scripts.lint = "eslint .";
908
+ }
909
+ if (!ctx.projectType.existingFormatter) {
910
+ if (!scripts.format) scripts.format = "prettier --write .";
911
+ if (!scripts["format:check"]) scripts["format:check"] = "prettier --check .";
912
+ }
902
913
  if (!scripts.knip) scripts.knip = "knip";
903
914
  if (ctx.projectType.publishableLibrary && !scripts.publint) {
904
915
  scripts.publint = "publint";
@@ -912,6 +923,7 @@ var SAFEWORD_SCHEMA = {
912
923
  unmerge: (existing) => {
913
924
  const result = { ...existing };
914
925
  const scripts = { ...existing.scripts };
926
+ delete scripts["lint:eslint"];
915
927
  delete scripts["lint:sh"];
916
928
  delete scripts["format:check"];
917
929
  delete scripts.knip;
@@ -1087,48 +1099,62 @@ var SAFEWORD_SCHEMA = {
1087
1099
  // NPM packages to install
1088
1100
  packages: {
1089
1101
  base: [
1090
- // Core tools
1102
+ // Core tools (always needed)
1091
1103
  "eslint",
1092
- "prettier",
1093
1104
  // Safeword plugin (bundles eslint-config-prettier + all ESLint plugins)
1094
1105
  "eslint-plugin-safeword",
1095
- // Non-ESLint tools
1106
+ // Architecture and dead code tools (used by /audit)
1107
+ "dependency-cruiser",
1096
1108
  "knip"
1097
1109
  ],
1098
1110
  conditional: {
1099
- // Prettier plugins
1111
+ // Prettier (only for projects without existing formatter)
1112
+ standard: ["prettier"],
1113
+ // "standard" = !existingFormatter
1114
+ // Prettier plugins (only for projects without existing formatter that need them)
1100
1115
  astro: ["prettier-plugin-astro"],
1101
1116
  tailwind: ["prettier-plugin-tailwindcss"],
1117
+ shell: ["prettier-plugin-sh"],
1102
1118
  // Non-ESLint tools
1103
1119
  publishableLibrary: ["publint"],
1104
- shell: ["shellcheck", "prettier-plugin-sh"]
1120
+ shellcheck: ["shellcheck"]
1121
+ // Renamed from shell to avoid conflict with prettier-plugin-sh
1105
1122
  }
1106
1123
  }
1107
1124
  };
1108
1125
 
1109
1126
  // src/utils/git.ts
1110
- import nodePath3 from "path";
1127
+ import nodePath2 from "path";
1111
1128
  function isGitRepo(cwd) {
1112
- return exists(nodePath3.join(cwd, ".git"));
1129
+ return exists(nodePath2.join(cwd, ".git"));
1113
1130
  }
1114
1131
 
1115
1132
  // src/utils/context.ts
1116
- import nodePath5 from "path";
1133
+ import nodePath4 from "path";
1117
1134
 
1118
1135
  // src/utils/project-detector.ts
1119
- import { readdirSync as readdirSync2 } from "fs";
1120
- import nodePath4 from "path";
1136
+ import { readdirSync } from "fs";
1137
+ import nodePath3 from "path";
1138
+ import { detect } from "eslint-plugin-safeword";
1139
+ var {
1140
+ TAILWIND_PACKAGES,
1141
+ TANSTACK_QUERY_PACKAGES,
1142
+ PLAYWRIGHT_PACKAGES,
1143
+ FORMATTER_CONFIG_FILES,
1144
+ hasExistingLinter,
1145
+ hasExistingFormatter
1146
+ } = detect;
1121
1147
  function hasShellScripts(cwd, maxDepth = 4) {
1122
1148
  const excludeDirectories = /* @__PURE__ */ new Set(["node_modules", ".git", ".safeword"]);
1123
1149
  function scan(dir, depth) {
1124
1150
  if (depth > maxDepth) return false;
1125
1151
  try {
1126
- const entries = readdirSync2(dir, { withFileTypes: true });
1152
+ const entries = readdirSync(dir, { withFileTypes: true });
1127
1153
  for (const entry of entries) {
1128
1154
  if (entry.isFile() && entry.name.endsWith(".sh")) {
1129
1155
  return true;
1130
1156
  }
1131
- if (entry.isDirectory() && !excludeDirectories.has(entry.name) && scan(nodePath4.join(dir, entry.name), depth + 1)) {
1157
+ if (entry.isDirectory() && !excludeDirectories.has(entry.name) && scan(nodePath3.join(dir, entry.name), depth + 1)) {
1132
1158
  return true;
1133
1159
  }
1134
1160
  }
@@ -1142,16 +1168,20 @@ function detectProjectType(packageJson, cwd) {
1142
1168
  const deps = packageJson.dependencies || {};
1143
1169
  const developmentDeps = packageJson.devDependencies || {};
1144
1170
  const allDeps = { ...deps, ...developmentDeps };
1171
+ const scripts = packageJson.scripts || {};
1145
1172
  const hasTypescript = "typescript" in allDeps;
1146
1173
  const hasReact = "react" in deps || "react" in developmentDeps;
1147
1174
  const hasNextJs = "next" in deps;
1148
1175
  const hasAstro = "astro" in deps || "astro" in developmentDeps;
1149
1176
  const hasVitest = "vitest" in developmentDeps;
1150
1177
  const hasPlaywright = "@playwright/test" in developmentDeps;
1151
- const hasTailwind = "tailwindcss" in allDeps;
1178
+ const hasTailwind = TAILWIND_PACKAGES.some((pkg) => pkg in allDeps);
1179
+ const hasTanstackQuery = TANSTACK_QUERY_PACKAGES.some((pkg) => pkg in allDeps);
1152
1180
  const hasEntryPoints = !!(packageJson.main || packageJson.module || packageJson.exports);
1153
1181
  const isPublishable = hasEntryPoints && packageJson.private !== true;
1154
1182
  const hasShell = cwd ? hasShellScripts(cwd) : false;
1183
+ const hasLinter = hasExistingLinter(scripts);
1184
+ const hasFormatter = cwd ? hasExistingFormatter(cwd, scripts) : "format" in scripts;
1155
1185
  return {
1156
1186
  typescript: hasTypescript,
1157
1187
  react: hasReact || hasNextJs,
@@ -1161,14 +1191,17 @@ function detectProjectType(packageJson, cwd) {
1161
1191
  vitest: hasVitest,
1162
1192
  playwright: hasPlaywright,
1163
1193
  tailwind: hasTailwind,
1194
+ tanstackQuery: hasTanstackQuery,
1164
1195
  publishableLibrary: isPublishable,
1165
- shell: hasShell
1196
+ shell: hasShell,
1197
+ existingLinter: hasLinter,
1198
+ existingFormatter: hasFormatter
1166
1199
  };
1167
1200
  }
1168
1201
 
1169
1202
  // src/utils/context.ts
1170
1203
  function createProjectContext(cwd) {
1171
- const packageJson = readJson(nodePath5.join(cwd, "package.json"));
1204
+ const packageJson = readJson(nodePath4.join(cwd, "package.json"));
1172
1205
  return {
1173
1206
  cwd,
1174
1207
  projectType: detectProjectType(packageJson ?? {}, cwd),
@@ -1177,45 +1210,10 @@ function createProjectContext(cwd) {
1177
1210
  };
1178
1211
  }
1179
1212
 
1180
- // src/utils/output.ts
1181
- function info(message) {
1182
- console.log(message);
1183
- }
1184
- function success(message) {
1185
- console.log(`\u2713 ${message}`);
1186
- }
1187
- function warn(message) {
1188
- console.warn(`\u26A0 ${message}`);
1189
- }
1190
- function error(message) {
1191
- console.error(`\u2717 ${message}`);
1192
- }
1193
- function header(title) {
1194
- console.log(`
1195
- ${title}`);
1196
- console.log("\u2500".repeat(title.length));
1197
- }
1198
- function listItem(item, indent = 2) {
1199
- console.log(`${" ".repeat(indent)}\u2022 ${item}`);
1200
- }
1201
- function keyValue(key, value) {
1202
- console.log(` ${key}: ${value}`);
1203
- }
1204
-
1205
1213
  export {
1206
- exists,
1207
- readFileSafe,
1208
- writeJson,
1209
1214
  reconcile,
1210
1215
  SAFEWORD_SCHEMA,
1211
1216
  isGitRepo,
1212
- createProjectContext,
1213
- info,
1214
- success,
1215
- warn,
1216
- error,
1217
- header,
1218
- listItem,
1219
- keyValue
1217
+ createProjectContext
1220
1218
  };
1221
- //# sourceMappingURL=chunk-H7PCVPAC.js.map
1219
+ //# sourceMappingURL=chunk-ZE6QJHZD.js.map