safeword 0.6.8 → 0.7.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.
Files changed (57) hide show
  1. package/dist/{check-OYYSYHFP.js → check-QGZJ62PY.js} +73 -57
  2. package/dist/check-QGZJ62PY.js.map +1 -0
  3. package/dist/{chunk-ZS3Z3Q37.js → chunk-QPO3C3FP.js} +285 -65
  4. package/dist/chunk-QPO3C3FP.js.map +1 -0
  5. package/dist/{chunk-LNSEDZIW.js → chunk-YMLVQC4V.js} +159 -152
  6. package/dist/chunk-YMLVQC4V.js.map +1 -0
  7. package/dist/cli.js +6 -6
  8. package/dist/{diff-325TIZ63.js → diff-654SSCFQ.js} +51 -53
  9. package/dist/diff-654SSCFQ.js.map +1 -0
  10. package/dist/index.d.ts +1 -0
  11. package/dist/{reset-ZGJIKMUW.js → reset-IU6AIT7C.js} +3 -3
  12. package/dist/{setup-GAMXTFM2.js → setup-5IQ4KV2M.js} +17 -20
  13. package/dist/setup-5IQ4KV2M.js.map +1 -0
  14. package/dist/{sync-BFMXZEHM.js → sync-YBQEISFI.js} +8 -26
  15. package/dist/sync-YBQEISFI.js.map +1 -0
  16. package/dist/{upgrade-X4GREJXN.js → upgrade-RRTWEQIP.js} +3 -3
  17. package/package.json +1 -1
  18. package/templates/SAFEWORD.md +141 -73
  19. package/templates/commands/architecture.md +1 -1
  20. package/templates/commands/lint.md +1 -0
  21. package/templates/commands/quality-review.md +1 -1
  22. package/templates/cursor/rules/safeword-core.mdc +5 -0
  23. package/templates/doc-templates/architecture-template.md +1 -1
  24. package/templates/doc-templates/task-spec-template.md +151 -0
  25. package/templates/doc-templates/ticket-template.md +2 -4
  26. package/templates/guides/architecture-guide.md +2 -2
  27. package/templates/guides/code-philosophy.md +1 -1
  28. package/templates/guides/context-files-guide.md +3 -3
  29. package/templates/guides/design-doc-guide.md +2 -2
  30. package/templates/guides/development-workflow.md +2 -2
  31. package/templates/guides/learning-extraction.md +9 -9
  32. package/templates/guides/tdd-best-practices.md +39 -38
  33. package/templates/guides/test-definitions-guide.md +15 -14
  34. package/templates/hooks/cursor/after-file-edit.sh +66 -0
  35. package/templates/hooks/cursor/stop.sh +50 -0
  36. package/templates/hooks/post-tool-lint.sh +19 -5
  37. package/templates/hooks/prompt-questions.sh +1 -1
  38. package/templates/hooks/prompt-timestamp.sh +8 -1
  39. package/templates/hooks/session-lint-check.sh +1 -1
  40. package/templates/hooks/session-verify-agents.sh +1 -1
  41. package/templates/hooks/session-version.sh +1 -1
  42. package/templates/hooks/stop-quality.sh +2 -2
  43. package/templates/markdownlint-cli2.jsonc +18 -19
  44. package/templates/scripts/bisect-test-pollution.sh +87 -0
  45. package/templates/scripts/bisect-zombie-processes.sh +129 -0
  46. package/templates/scripts/lint-md.sh +16 -0
  47. package/templates/skills/safeword-quality-reviewer/SKILL.md +3 -3
  48. package/templates/skills/safeword-systematic-debugger/SKILL.md +246 -0
  49. package/templates/skills/safeword-tdd-enforcer/SKILL.md +221 -0
  50. package/dist/check-OYYSYHFP.js.map +0 -1
  51. package/dist/chunk-LNSEDZIW.js.map +0 -1
  52. package/dist/chunk-ZS3Z3Q37.js.map +0 -1
  53. package/dist/diff-325TIZ63.js.map +0 -1
  54. package/dist/setup-GAMXTFM2.js.map +0 -1
  55. package/dist/sync-BFMXZEHM.js.map +0 -1
  56. /package/dist/{reset-ZGJIKMUW.js.map → reset-IU6AIT7C.js.map} +0 -0
  57. /package/dist/{upgrade-X4GREJXN.js.map → upgrade-RRTWEQIP.js.map} +0 -0
@@ -88,7 +88,31 @@ function writeJson(path, data) {
88
88
  }
89
89
 
90
90
  // src/utils/project-detector.ts
91
- function detectProjectType(packageJson) {
91
+ import { readdirSync as readdirSync2 } from "fs";
92
+ import { join as join2 } from "path";
93
+ function hasShellScripts(cwd, maxDepth = 4) {
94
+ const excludeDirs = /* @__PURE__ */ new Set(["node_modules", ".git", ".safeword"]);
95
+ function scan(dir, depth) {
96
+ if (depth > maxDepth) return false;
97
+ try {
98
+ const entries = readdirSync2(dir, { withFileTypes: true });
99
+ for (const entry of entries) {
100
+ if (entry.isFile() && entry.name.endsWith(".sh")) {
101
+ return true;
102
+ }
103
+ if (entry.isDirectory() && !excludeDirs.has(entry.name)) {
104
+ if (scan(join2(dir, entry.name), depth + 1)) {
105
+ return true;
106
+ }
107
+ }
108
+ }
109
+ } catch {
110
+ }
111
+ return false;
112
+ }
113
+ return scan(cwd, 0);
114
+ }
115
+ function detectProjectType(packageJson, cwd) {
92
116
  const deps = packageJson.dependencies || {};
93
117
  const devDeps = packageJson.devDependencies || {};
94
118
  const allDeps = { ...deps, ...devDeps };
@@ -106,6 +130,7 @@ function detectProjectType(packageJson) {
106
130
  const hasTailwind = "tailwindcss" in allDeps;
107
131
  const hasEntryPoints = !!(packageJson.main || packageJson.module || packageJson.exports);
108
132
  const isPublishable = hasEntryPoints && packageJson.private !== true;
133
+ const hasShell = cwd ? hasShellScripts(cwd) : false;
109
134
  return {
110
135
  typescript: hasTypescript,
111
136
  react: hasReact || hasNextJs,
@@ -122,7 +147,8 @@ function detectProjectType(packageJson) {
122
147
  vitest: hasVitest,
123
148
  playwright: hasPlaywright,
124
149
  tailwind: hasTailwind,
125
- publishableLibrary: isPublishable
150
+ publishableLibrary: isPublishable,
151
+ shell: hasShell
126
152
  };
127
153
  }
128
154
 
@@ -145,28 +171,38 @@ function getPrettierConfig(projectType) {
145
171
  const plugins = [];
146
172
  if (projectType.astro) plugins.push("prettier-plugin-astro");
147
173
  if (projectType.svelte) plugins.push("prettier-plugin-svelte");
174
+ if (projectType.shell) plugins.push("prettier-plugin-sh");
148
175
  if (projectType.tailwind) plugins.push("prettier-plugin-tailwindcss");
149
176
  if (plugins.length > 0) {
150
177
  config.plugins = plugins;
151
178
  }
152
179
  return JSON.stringify(config, null, 2) + "\n";
153
180
  }
154
- var LINT_STAGED_CONFIG = {
155
- "*.{js,jsx,ts,tsx,mjs,mts,cjs,cts}": ["eslint --fix", "prettier --write"],
156
- "*.{vue,svelte,astro}": ["eslint --fix", "prettier --write"],
157
- "*.{json,css,scss,html,yaml,yml,graphql}": ["prettier --write"],
158
- "*.md": ["markdownlint-cli2 --fix", "prettier --write"]
159
- };
181
+ function getLintStagedConfig(projectType) {
182
+ const config = {
183
+ "*.{js,jsx,ts,tsx,mjs,mts,cjs,cts}": ["eslint --fix", "prettier --write"],
184
+ "*.{vue,svelte,astro}": ["eslint --fix", "prettier --write"],
185
+ "*.{json,css,scss,html,yaml,yml,graphql}": ["prettier --write"],
186
+ "*.md": ["markdownlint-cli2 --fix", "prettier --write"]
187
+ };
188
+ if (projectType.shell) {
189
+ config["*.sh"] = ["shellcheck", "prettier --write"];
190
+ }
191
+ return config;
192
+ }
160
193
 
161
194
  // src/templates/config.ts
162
195
  function getEslintConfig(options) {
163
- return `import { readFileSync } from "fs";
196
+ return `/* eslint-disable import-x/no-unresolved -- dynamic imports for optional framework plugins */
197
+ import { readFileSync } from "fs";
164
198
  import { defineConfig } from "eslint/config";
165
199
  import js from "@eslint/js";
166
200
  import { importX } from "eslint-plugin-import-x";
201
+ import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
167
202
  import sonarjs from "eslint-plugin-sonarjs";
168
203
  import sdl from "@microsoft/eslint-plugin-sdl";
169
204
  import playwright from "eslint-plugin-playwright";
205
+ import unicorn from "eslint-plugin-unicorn";
170
206
  import eslintConfigPrettier from "eslint-config-prettier";
171
207
  ${options.boundaries ? 'import boundariesConfig from "./.safeword/eslint-boundaries.config.mjs";' : ""}
172
208
 
@@ -175,7 +211,7 @@ const pkg = JSON.parse(readFileSync("./package.json", "utf8"));
175
211
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
176
212
 
177
213
  // Build dynamic ignores based on detected frameworks
178
- const ignores = ["node_modules/", "dist/", "build/", "coverage/"];
214
+ const ignores = ["**/node_modules/", "**/dist/", "**/build/", "**/coverage/"];
179
215
  if (deps["next"]) ignores.push(".next/");
180
216
  if (deps["astro"]) ignores.push(".astro/");
181
217
  if (deps["vue"] || deps["nuxt"]) ignores.push(".nuxt/");
@@ -186,12 +222,36 @@ const configs = [
186
222
  { ignores },
187
223
  js.configs.recommended,
188
224
  importX.flatConfigs.recommended,
225
+ {
226
+ settings: {
227
+ "import-x/resolver-next": [createTypeScriptImportResolver()],
228
+ },
229
+ },
189
230
  sonarjs.configs.recommended,
190
231
  ...sdl.configs.recommended,
232
+ unicorn.configs["flat/recommended"],
233
+ {
234
+ // Unicorn overrides for LLM-generated code
235
+ // Keep modern JS enforcement, disable overly pedantic rules
236
+ rules: {
237
+ "unicorn/prevent-abbreviations": "off", // ctx, dir, pkg, err are standard
238
+ "unicorn/no-null": "off", // null is valid JS
239
+ "unicorn/no-process-exit": "off", // CLI apps use process.exit
240
+ "unicorn/import-style": "off", // Named imports are fine
241
+ "unicorn/numeric-separators-style": "off", // Style preference
242
+ "unicorn/text-encoding-identifier-case": "off", // utf-8 vs utf8
243
+ "unicorn/switch-case-braces": "warn", // Good practice, not critical
244
+ "unicorn/catch-error-name": "warn", // Reasonable, auto-fixable
245
+ "unicorn/no-negated-condition": "off", // Sometimes clearer
246
+ "unicorn/no-array-reduce": "off", // Reduce is fine when readable
247
+ "unicorn/no-array-for-each": "off", // forEach is fine
248
+ "unicorn/prefer-module": "off", // CJS still valid
249
+ },
250
+ },
191
251
  ];
192
252
 
193
253
  // TypeScript support (detected from package.json)
194
- if (deps["typescript"]) {
254
+ if (deps["typescript"] || deps["typescript-eslint"]) {
195
255
  const tseslint = await import("typescript-eslint");
196
256
  configs.push(importX.flatConfigs.typescript);
197
257
  configs.push(...tseslint.default.configs.recommended);
@@ -275,6 +335,10 @@ configs.push(eslintConfigPrettier);
275
335
  export default defineConfig(configs);
276
336
  `;
277
337
  }
338
+ var CURSOR_HOOKS = {
339
+ afterFileEdit: [{ command: "./.safeword/hooks/cursor/after-file-edit.sh" }],
340
+ stop: [{ command: "./.safeword/hooks/cursor/stop.sh" }]
341
+ };
278
342
  var SETTINGS_HOOKS = {
279
343
  SessionStart: [
280
344
  {
@@ -344,59 +408,124 @@ var SETTINGS_HOOKS = {
344
408
  };
345
409
 
346
410
  // src/utils/boundaries.ts
347
- import { join as join2 } from "path";
348
- var ARCHITECTURE_DIRS = [
349
- "types",
350
- // Bottom: can be imported by everything
351
- "utils",
352
- "lib",
353
- "hooks",
354
- "services",
355
- "components",
356
- "features",
357
- "modules",
358
- "app"
359
- // Top: can import everything
411
+ import { join as join3 } from "path";
412
+ import { readdirSync as readdirSync3 } from "fs";
413
+ var ARCHITECTURE_LAYERS = [
414
+ // Layer 0: Pure types (no imports)
415
+ { layer: "types", dirs: ["types", "interfaces", "schemas"] },
416
+ // Layer 1: Utilities (only types)
417
+ { layer: "utils", dirs: ["utils", "helpers", "shared", "common", "core"] },
418
+ // Layer 2: Libraries (types, utils)
419
+ { layer: "lib", dirs: ["lib", "libraries"] },
420
+ // Layer 3: State & logic (types, utils, lib)
421
+ { layer: "hooks", dirs: ["hooks", "composables"] },
422
+ { layer: "services", dirs: ["services", "api", "stores", "state"] },
423
+ // Layer 4: UI components (all above)
424
+ { layer: "components", dirs: ["components", "ui"] },
425
+ // Layer 5: Features (all above)
426
+ { layer: "features", dirs: ["features", "modules", "domains"] },
427
+ // Layer 6: Entry points (can import everything)
428
+ { layer: "app", dirs: ["app", "pages", "views", "routes", "commands"] }
360
429
  ];
361
430
  var HIERARCHY = {
362
431
  types: [],
363
- // types can't import anything (pure type definitions)
364
432
  utils: ["types"],
365
433
  lib: ["utils", "types"],
366
434
  hooks: ["lib", "utils", "types"],
367
435
  services: ["lib", "utils", "types"],
368
436
  components: ["hooks", "services", "lib", "utils", "types"],
369
437
  features: ["components", "hooks", "services", "lib", "utils", "types"],
370
- modules: ["components", "hooks", "services", "lib", "utils", "types"],
371
- app: ["features", "modules", "components", "hooks", "services", "lib", "utils", "types"]
438
+ app: ["features", "components", "hooks", "services", "lib", "utils", "types"]
372
439
  };
373
- function detectArchitecture(projectDir) {
374
- const foundInSrc = [];
375
- const foundAtRoot = [];
376
- for (const dir of ARCHITECTURE_DIRS) {
377
- if (exists(join2(projectDir, "src", dir))) {
378
- foundInSrc.push(dir);
440
+ function findMonorepoPackages(projectDir) {
441
+ const packages = [];
442
+ const monorepoRoots = ["packages", "apps", "libs", "modules"];
443
+ for (const root of monorepoRoots) {
444
+ const rootPath = join3(projectDir, root);
445
+ if (exists(rootPath)) {
446
+ try {
447
+ const entries = readdirSync3(rootPath, { withFileTypes: true });
448
+ for (const entry of entries) {
449
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
450
+ packages.push(join3(root, entry.name));
451
+ }
452
+ }
453
+ } catch {
454
+ }
379
455
  }
380
- if (exists(join2(projectDir, dir))) {
381
- foundAtRoot.push(dir);
456
+ }
457
+ return packages;
458
+ }
459
+ function hasLayerForPrefix(elements, layer, pathPrefix) {
460
+ return elements.some((e) => e.layer === layer && e.pattern.startsWith(pathPrefix));
461
+ }
462
+ function scanSearchPath(projectDir, searchPath, pathPrefix, elements) {
463
+ for (const layerDef of ARCHITECTURE_LAYERS) {
464
+ for (const dirName of layerDef.dirs) {
465
+ const fullPath = join3(projectDir, searchPath, dirName);
466
+ if (exists(fullPath) && !hasLayerForPrefix(elements, layerDef.layer, pathPrefix)) {
467
+ elements.push({
468
+ layer: layerDef.layer,
469
+ pattern: `${pathPrefix}${dirName}/**`,
470
+ location: `${pathPrefix}${dirName}`
471
+ });
472
+ }
382
473
  }
383
474
  }
384
- const inSrc = foundInSrc.length >= foundAtRoot.length;
385
- const found = inSrc ? foundInSrc : foundAtRoot;
386
- return { directories: found, inSrc };
475
+ }
476
+ function scanForLayers(projectDir, basePath) {
477
+ const elements = [];
478
+ const prefix = basePath ? `${basePath}/` : "";
479
+ scanSearchPath(projectDir, join3(basePath, "src"), `${prefix}src/`, elements);
480
+ scanSearchPath(projectDir, basePath, prefix, elements);
481
+ return elements;
482
+ }
483
+ function detectArchitecture(projectDir) {
484
+ const elements = [];
485
+ const packages = findMonorepoPackages(projectDir);
486
+ const isMonorepo = packages.length > 0;
487
+ if (isMonorepo) {
488
+ for (const pkg of packages) {
489
+ elements.push(...scanForLayers(projectDir, pkg));
490
+ }
491
+ }
492
+ elements.push(...scanForLayers(projectDir, ""));
493
+ const seen = /* @__PURE__ */ new Set();
494
+ const uniqueElements = elements.filter((e) => {
495
+ if (seen.has(e.pattern)) return false;
496
+ seen.add(e.pattern);
497
+ return true;
498
+ });
499
+ return { elements: uniqueElements, isMonorepo };
500
+ }
501
+ function formatElement(el) {
502
+ return ` { type: '${el.layer}', pattern: '${el.pattern}', mode: 'full' }`;
503
+ }
504
+ function formatAllowedImports(allowed) {
505
+ return allowed.map((d) => `'${d}'`).join(", ");
506
+ }
507
+ function generateRule(layer, detectedLayers) {
508
+ const allowedLayers = HIERARCHY[layer];
509
+ if (allowedLayers.length === 0) return null;
510
+ const allowed = allowedLayers.filter((dep) => detectedLayers.has(dep));
511
+ if (allowed.length === 0) return null;
512
+ return ` { from: ['${layer}'], allow: [${formatAllowedImports(allowed)}] }`;
513
+ }
514
+ function buildDetectedInfo(arch) {
515
+ if (arch.elements.length === 0) {
516
+ return "No architecture directories detected yet - add types/, utils/, components/, etc.";
517
+ }
518
+ const locations = arch.elements.map((e) => e.location).join(", ");
519
+ const monorepoNote = arch.isMonorepo ? " (monorepo)" : "";
520
+ return `Detected: ${locations}${monorepoNote}`;
387
521
  }
388
522
  function generateBoundariesConfig(arch) {
389
- const prefix = arch.inSrc ? "src/" : "";
390
- const hasDirectories = arch.directories.length > 0;
391
- const elements = arch.directories.map((dir) => ` { type: '${dir}', pattern: '${prefix}${dir}/**', mode: 'full' }`).join(",\n");
392
- const rules = arch.directories.filter((dir) => HIERARCHY[dir].length > 0).map((dir) => {
393
- const allowed = HIERARCHY[dir].filter((dep) => arch.directories.includes(dep));
394
- if (allowed.length === 0) return null;
395
- return ` { from: ['${dir}'], allow: [${allowed.map((d) => `'${d}'`).join(", ")}] }`;
396
- }).filter(Boolean).join(",\n");
397
- const detectedInfo = hasDirectories ? `Detected directories: ${arch.directories.join(", ")} (${arch.inSrc ? "in src/" : "at root"})` : "No architecture directories detected yet - add types/, utils/, components/, etc.";
398
- const elementsContent = elements || "";
399
- const rulesContent = rules || "";
523
+ const hasElements = arch.elements.length > 0;
524
+ const elementsContent = arch.elements.map((el) => formatElement(el)).join(",\n");
525
+ const detectedLayers = new Set(arch.elements.map((e) => e.layer));
526
+ const rules = [...detectedLayers].map((layer) => generateRule(layer, detectedLayers)).filter((rule) => rule !== null);
527
+ const rulesContent = rules.join(",\n");
528
+ const detectedInfo = buildDetectedInfo(arch);
400
529
  return `/**
401
530
  * Architecture Boundaries Configuration (AUTO-GENERATED)
402
531
  *
@@ -422,13 +551,13 @@ export default {
422
551
  ${elementsContent}
423
552
  ],
424
553
  },
425
- rules: {
554
+ rules: {${hasElements ? `
426
555
  'boundaries/element-types': ['warn', {
427
556
  default: 'disallow',
428
557
  rules: [
429
558
  ${rulesContent}
430
559
  ],
431
- }],
560
+ }],` : ""}
432
561
  'boundaries/no-unknown': 'off', // Allow files outside defined elements
433
562
  'boundaries/no-unknown-files': 'off', // Allow non-matching files
434
563
  },
@@ -455,9 +584,7 @@ function isHookEntry(h) {
455
584
  }
456
585
  function isSafewordHook(h) {
457
586
  if (!isHookEntry(h)) return false;
458
- return h.hooks.some(
459
- (cmd) => typeof cmd.command === "string" && cmd.command.includes(".safeword")
460
- );
587
+ return h.hooks.some((cmd) => typeof cmd.command === "string" && cmd.command.includes(".safeword"));
461
588
  }
462
589
  function filterOutSafewordHooks(hooks) {
463
590
  return hooks.filter((h) => !isSafewordHook(h));
@@ -470,16 +597,21 @@ var SAFEWORD_SCHEMA = {
470
597
  ownedDirs: [
471
598
  ".safeword",
472
599
  ".safeword/hooks",
600
+ ".safeword/hooks/cursor",
473
601
  ".safeword/lib",
474
602
  ".safeword/guides",
475
603
  ".safeword/templates",
476
604
  ".safeword/prompts",
477
605
  ".safeword/planning",
478
- ".safeword/planning/user-stories",
606
+ ".safeword/planning/specs",
479
607
  ".safeword/planning/test-definitions",
480
608
  ".safeword/planning/design",
481
609
  ".safeword/planning/issues",
482
- ".husky"
610
+ ".safeword/scripts",
611
+ ".husky",
612
+ ".cursor",
613
+ ".cursor/rules",
614
+ ".cursor/commands"
483
615
  ],
484
616
  // Directories we add to but don't own (not deleted on reset)
485
617
  sharedDirs: [".claude", ".claude/skills", ".claude/commands"],
@@ -520,13 +652,16 @@ var SAFEWORD_SCHEMA = {
520
652
  ".safeword/guides/test-definitions-guide.md": { template: "guides/test-definitions-guide.md" },
521
653
  ".safeword/guides/user-story-guide.md": { template: "guides/user-story-guide.md" },
522
654
  ".safeword/guides/zombie-process-cleanup.md": { template: "guides/zombie-process-cleanup.md" },
523
- // Templates (5 files)
655
+ // Templates (6 files)
524
656
  ".safeword/templates/architecture-template.md": {
525
657
  template: "doc-templates/architecture-template.md"
526
658
  },
527
659
  ".safeword/templates/design-doc-template.md": {
528
660
  template: "doc-templates/design-doc-template.md"
529
661
  },
662
+ ".safeword/templates/task-spec-template.md": {
663
+ template: "doc-templates/task-spec-template.md"
664
+ },
530
665
  ".safeword/templates/test-definitions-feature.md": {
531
666
  template: "doc-templates/test-definitions-feature.md"
532
667
  },
@@ -537,15 +672,36 @@ var SAFEWORD_SCHEMA = {
537
672
  // Prompts (2 files)
538
673
  ".safeword/prompts/architecture.md": { template: "prompts/architecture.md" },
539
674
  ".safeword/prompts/quality-review.md": { template: "prompts/quality-review.md" },
540
- // Claude skills and commands (4 files)
675
+ // Scripts (3 files)
676
+ ".safeword/scripts/bisect-test-pollution.sh": { template: "scripts/bisect-test-pollution.sh" },
677
+ ".safeword/scripts/bisect-zombie-processes.sh": {
678
+ template: "scripts/bisect-zombie-processes.sh"
679
+ },
680
+ ".safeword/scripts/lint-md.sh": { template: "scripts/lint-md.sh" },
681
+ // Claude skills and commands (6 files)
541
682
  ".claude/skills/safeword-quality-reviewer/SKILL.md": {
542
683
  template: "skills/safeword-quality-reviewer/SKILL.md"
543
684
  },
685
+ ".claude/skills/safeword-systematic-debugger/SKILL.md": {
686
+ template: "skills/safeword-systematic-debugger/SKILL.md"
687
+ },
688
+ ".claude/skills/safeword-tdd-enforcer/SKILL.md": {
689
+ template: "skills/safeword-tdd-enforcer/SKILL.md"
690
+ },
544
691
  ".claude/commands/architecture.md": { template: "commands/architecture.md" },
545
692
  ".claude/commands/lint.md": { template: "commands/lint.md" },
546
693
  ".claude/commands/quality-review.md": { template: "commands/quality-review.md" },
547
694
  // Husky (1 file)
548
- ".husky/pre-commit": { content: HUSKY_PRE_COMMIT_CONTENT }
695
+ ".husky/pre-commit": { content: HUSKY_PRE_COMMIT_CONTENT },
696
+ // Cursor rules (1 file)
697
+ ".cursor/rules/safeword-core.mdc": { template: "cursor/rules/safeword-core.mdc" },
698
+ // Cursor commands (3 files - same as Claude)
699
+ ".cursor/commands/lint.md": { template: "commands/lint.md" },
700
+ ".cursor/commands/quality-review.md": { template: "commands/quality-review.md" },
701
+ ".cursor/commands/architecture.md": { template: "commands/architecture.md" },
702
+ // Cursor hooks adapters (2 files)
703
+ ".safeword/hooks/cursor/after-file-edit.sh": { template: "hooks/cursor/after-file-edit.sh" },
704
+ ".safeword/hooks/cursor/stop.sh": { template: "hooks/cursor/stop.sh" }
549
705
  },
550
706
  // Files created if missing, updated only if content matches current template
551
707
  managedFiles: {
@@ -568,7 +724,8 @@ var SAFEWORD_SCHEMA = {
568
724
  "lint-staged"
569
725
  ],
570
726
  conditionalKeys: {
571
- publishableLibrary: ["scripts.publint"]
727
+ publishableLibrary: ["scripts.publint"],
728
+ shell: ["scripts.lint:sh"]
572
729
  },
573
730
  merge: (existing, ctx) => {
574
731
  const scripts = existing.scripts ?? {};
@@ -582,16 +739,20 @@ var SAFEWORD_SCHEMA = {
582
739
  if (ctx.projectType.publishableLibrary && !scripts.publint) {
583
740
  scripts.publint = "publint";
584
741
  }
742
+ if (ctx.projectType.shell && !scripts["lint:sh"]) {
743
+ scripts["lint:sh"] = "shellcheck **/*.sh";
744
+ }
585
745
  result.scripts = scripts;
586
746
  if (!existing["lint-staged"]) {
587
- result["lint-staged"] = LINT_STAGED_CONFIG;
747
+ result["lint-staged"] = getLintStagedConfig(ctx.projectType);
588
748
  }
589
749
  return result;
590
750
  },
591
751
  unmerge: (existing) => {
592
752
  const result = { ...existing };
593
- const scripts = { ...existing.scripts ?? {} };
753
+ const scripts = { ...existing.scripts };
594
754
  delete scripts["lint:md"];
755
+ delete scripts["lint:sh"];
595
756
  delete scripts["format:check"];
596
757
  delete scripts.knip;
597
758
  delete scripts.prepare;
@@ -651,7 +812,34 @@ var SAFEWORD_SCHEMA = {
651
812
  },
652
813
  unmerge: (existing) => {
653
814
  const result = { ...existing };
654
- const mcpServers = { ...existing.mcpServers ?? {} };
815
+ const mcpServers = { ...existing.mcpServers };
816
+ delete mcpServers.context7;
817
+ delete mcpServers.playwright;
818
+ if (Object.keys(mcpServers).length > 0) {
819
+ result.mcpServers = mcpServers;
820
+ } else {
821
+ delete result.mcpServers;
822
+ }
823
+ return result;
824
+ }
825
+ },
826
+ ".cursor/mcp.json": {
827
+ keys: ["mcpServers.context7", "mcpServers.playwright"],
828
+ removeFileIfEmpty: true,
829
+ merge: (existing) => {
830
+ const mcpServers = existing.mcpServers ?? {};
831
+ return {
832
+ ...existing,
833
+ mcpServers: {
834
+ ...mcpServers,
835
+ context7: MCP_SERVERS.context7,
836
+ playwright: MCP_SERVERS.playwright
837
+ }
838
+ };
839
+ },
840
+ unmerge: (existing) => {
841
+ const result = { ...existing };
842
+ const mcpServers = { ...existing.mcpServers };
655
843
  delete mcpServers.context7;
656
844
  delete mcpServers.playwright;
657
845
  if (Object.keys(mcpServers).length > 0) {
@@ -661,6 +849,35 @@ var SAFEWORD_SCHEMA = {
661
849
  }
662
850
  return result;
663
851
  }
852
+ },
853
+ ".cursor/hooks.json": {
854
+ keys: ["version", "hooks.afterFileEdit", "hooks.stop"],
855
+ removeFileIfEmpty: true,
856
+ merge: (existing) => {
857
+ const hooks = existing.hooks ?? {};
858
+ return {
859
+ ...existing,
860
+ version: 1,
861
+ // Required by Cursor
862
+ hooks: {
863
+ ...hooks,
864
+ ...CURSOR_HOOKS
865
+ }
866
+ };
867
+ },
868
+ unmerge: (existing) => {
869
+ const result = { ...existing };
870
+ const hooks = { ...existing.hooks };
871
+ delete hooks.afterFileEdit;
872
+ delete hooks.stop;
873
+ if (Object.keys(hooks).length > 0) {
874
+ result.hooks = hooks;
875
+ } else {
876
+ delete result.hooks;
877
+ delete result.version;
878
+ }
879
+ return result;
880
+ }
664
881
  }
665
882
  },
666
883
  // Text files where we patch specific content
@@ -686,7 +903,9 @@ var SAFEWORD_SCHEMA = {
686
903
  "prettier",
687
904
  "@eslint/js",
688
905
  "eslint-plugin-import-x",
906
+ "eslint-import-resolver-typescript",
689
907
  "eslint-plugin-sonarjs",
908
+ "eslint-plugin-unicorn",
690
909
  "eslint-plugin-boundaries",
691
910
  "eslint-plugin-playwright",
692
911
  "@microsoft/eslint-plugin-sdl",
@@ -706,7 +925,8 @@ var SAFEWORD_SCHEMA = {
706
925
  electron: ["@electron-toolkit/eslint-config"],
707
926
  vitest: ["@vitest/eslint-plugin"],
708
927
  tailwind: ["prettier-plugin-tailwindcss"],
709
- publishableLibrary: ["publint"]
928
+ publishableLibrary: ["publint"],
929
+ shell: ["shellcheck", "prettier-plugin-sh"]
710
930
  }
711
931
  }
712
932
  };
@@ -726,4 +946,4 @@ export {
726
946
  detectProjectType,
727
947
  SAFEWORD_SCHEMA
728
948
  };
729
- //# sourceMappingURL=chunk-ZS3Z3Q37.js.map
949
+ //# sourceMappingURL=chunk-QPO3C3FP.js.map