safeword 0.8.6 → 0.8.9

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 (39) hide show
  1. package/dist/{check-I2J6THGQ.js → check-QMAGWUOA.js} +24 -22
  2. package/dist/check-QMAGWUOA.js.map +1 -0
  3. package/dist/{chunk-DES5CSPH.js → chunk-4URRFBUS.js} +10 -10
  4. package/dist/chunk-4URRFBUS.js.map +1 -0
  5. package/dist/{chunk-DXT6TWW4.js → chunk-CLSGXTOL.js} +232 -435
  6. package/dist/chunk-CLSGXTOL.js.map +1 -0
  7. package/dist/{chunk-W66Z3C5H.js → chunk-FJYRWU2V.js} +5 -5
  8. package/dist/chunk-FJYRWU2V.js.map +1 -0
  9. package/dist/{chunk-VXKJ5ZIV.js → chunk-KQ6BLN6W.js} +172 -155
  10. package/dist/chunk-KQ6BLN6W.js.map +1 -0
  11. package/dist/cli.js +6 -6
  12. package/dist/cli.js.map +1 -1
  13. package/dist/{diff-4YFDNEZB.js → diff-2T7UDES7.js} +12 -12
  14. package/dist/diff-2T7UDES7.js.map +1 -0
  15. package/dist/index.d.ts +2 -2
  16. package/dist/{reset-QVERBAQJ.js → reset-QRXG7KZZ.js} +8 -8
  17. package/dist/reset-QRXG7KZZ.js.map +1 -0
  18. package/dist/{setup-ZSMZ7HZG.js → setup-QUUJ7SH3.js} +8 -8
  19. package/dist/setup-QUUJ7SH3.js.map +1 -0
  20. package/dist/sync-ISBJ7X2T.js +9 -0
  21. package/dist/{upgrade-WILVVHUY.js → upgrade-FALAUUKE.js} +22 -10
  22. package/dist/upgrade-FALAUUKE.js.map +1 -0
  23. package/package.json +15 -14
  24. package/templates/SAFEWORD.md +4 -2
  25. package/templates/commands/cleanup-zombies.md +48 -0
  26. package/templates/guides/zombie-process-cleanup.md +40 -24
  27. package/templates/scripts/cleanup-zombies.sh +222 -0
  28. package/templates/scripts/lint-md.sh +0 -0
  29. package/dist/check-I2J6THGQ.js.map +0 -1
  30. package/dist/chunk-DES5CSPH.js.map +0 -1
  31. package/dist/chunk-DXT6TWW4.js.map +0 -1
  32. package/dist/chunk-VXKJ5ZIV.js.map +0 -1
  33. package/dist/chunk-W66Z3C5H.js.map +0 -1
  34. package/dist/diff-4YFDNEZB.js.map +0 -1
  35. package/dist/reset-QVERBAQJ.js.map +0 -1
  36. package/dist/setup-ZSMZ7HZG.js.map +0 -1
  37. package/dist/sync-VQW5DSTV.js +0 -9
  38. package/dist/upgrade-WILVVHUY.js.map +0 -1
  39. /package/dist/{sync-VQW5DSTV.js.map → sync-ISBJ7X2T.js.map} +0 -0
@@ -4,30 +4,30 @@ import {
4
4
 
5
5
  // src/utils/fs.ts
6
6
  import {
7
+ chmodSync,
7
8
  existsSync,
8
9
  mkdirSync,
10
+ readdirSync,
9
11
  readFileSync,
10
- writeFileSync,
11
- rmSync,
12
12
  rmdirSync,
13
- readdirSync,
14
- chmodSync
13
+ rmSync,
14
+ writeFileSync
15
15
  } from "fs";
16
- import { join, dirname } from "path";
16
+ import nodePath from "path";
17
17
  import { fileURLToPath } from "url";
18
- var __dirname = dirname(fileURLToPath(import.meta.url));
19
- function getTemplatesDir() {
18
+ var __dirname = nodePath.dirname(fileURLToPath(import.meta.url));
19
+ function getTemplatesDirectory() {
20
20
  const knownTemplateFile = "SAFEWORD.md";
21
21
  const candidates = [
22
- join(__dirname, "..", "templates"),
22
+ nodePath.join(__dirname, "..", "templates"),
23
23
  // From dist/ (flat bundled)
24
- join(__dirname, "..", "..", "templates"),
24
+ nodePath.join(__dirname, "..", "..", "templates"),
25
25
  // From src/utils/ or dist/utils/
26
- join(__dirname, "templates")
26
+ nodePath.join(__dirname, "templates")
27
27
  // Direct sibling (unlikely but safe)
28
28
  ];
29
29
  for (const candidate of candidates) {
30
- if (existsSync(join(candidate, knownTemplateFile))) {
30
+ if (existsSync(nodePath.join(candidate, knownTemplateFile))) {
31
31
  return candidate;
32
32
  }
33
33
  }
@@ -36,20 +36,20 @@ function getTemplatesDir() {
36
36
  function exists(path) {
37
37
  return existsSync(path);
38
38
  }
39
- function ensureDir(path) {
39
+ function ensureDirectory(path) {
40
40
  if (!existsSync(path)) {
41
41
  mkdirSync(path, { recursive: true });
42
42
  }
43
43
  }
44
44
  function readFile(path) {
45
- return readFileSync(path, "utf-8");
45
+ return readFileSync(path, "utf8");
46
46
  }
47
47
  function readFileSafe(path) {
48
- if (!existsSync(path)) return null;
49
- return readFileSync(path, "utf-8");
48
+ if (!existsSync(path)) return void 0;
49
+ return readFileSync(path, "utf8");
50
50
  }
51
51
  function writeFile(path, content) {
52
- ensureDir(dirname(path));
52
+ ensureDirectory(nodePath.dirname(path));
53
53
  writeFileSync(path, content);
54
54
  }
55
55
  function remove(path) {
@@ -70,146 +70,34 @@ function makeScriptsExecutable(dirPath) {
70
70
  if (!existsSync(dirPath)) return;
71
71
  for (const file of readdirSync(dirPath)) {
72
72
  if (file.endsWith(".sh")) {
73
- chmodSync(join(dirPath, file), 493);
73
+ chmodSync(nodePath.join(dirPath, file), 493);
74
74
  }
75
75
  }
76
76
  }
77
77
  function readJson(path) {
78
78
  const content = readFileSafe(path);
79
- if (!content) return null;
79
+ if (!content) return void 0;
80
80
  try {
81
81
  return JSON.parse(content);
82
82
  } catch {
83
- return null;
83
+ return void 0;
84
84
  }
85
85
  }
86
86
  function writeJson(path, data) {
87
- writeFile(path, JSON.stringify(data, null, 2) + "\n");
88
- }
89
-
90
- // src/utils/project-detector.ts
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) {
116
- const deps = packageJson.dependencies || {};
117
- const devDeps = packageJson.devDependencies || {};
118
- const allDeps = { ...deps, ...devDeps };
119
- const hasTypescript = "typescript" in allDeps;
120
- const hasReact = "react" in deps || "react" in devDeps;
121
- const hasNextJs = "next" in deps;
122
- const hasAstro = "astro" in deps || "astro" in devDeps;
123
- const hasVue = "vue" in deps || "vue" in devDeps;
124
- const hasNuxt = "nuxt" in deps;
125
- const hasSvelte = "svelte" in deps || "svelte" in devDeps;
126
- const hasSvelteKit = "@sveltejs/kit" in deps || "@sveltejs/kit" in devDeps;
127
- const hasElectron = "electron" in deps || "electron" in devDeps;
128
- const hasVitest = "vitest" in devDeps;
129
- const hasPlaywright = "@playwright/test" in devDeps;
130
- const hasTailwind = "tailwindcss" in allDeps;
131
- const hasEntryPoints = !!(packageJson.main || packageJson.module || packageJson.exports);
132
- const isPublishable = hasEntryPoints && packageJson.private !== true;
133
- const hasShell = cwd ? hasShellScripts(cwd) : false;
134
- return {
135
- typescript: hasTypescript,
136
- react: hasReact || hasNextJs,
137
- // Next.js implies React
138
- nextjs: hasNextJs,
139
- astro: hasAstro,
140
- vue: hasVue || hasNuxt,
141
- // Nuxt implies Vue
142
- nuxt: hasNuxt,
143
- svelte: hasSvelte || hasSvelteKit,
144
- // SvelteKit implies Svelte
145
- sveltekit: hasSvelteKit,
146
- electron: hasElectron,
147
- vitest: hasVitest,
148
- playwright: hasPlaywright,
149
- tailwind: hasTailwind,
150
- publishableLibrary: isPublishable,
151
- shell: hasShell
152
- };
153
- }
154
-
155
- // src/templates/content.ts
156
- var AGENTS_MD_LINK = `**\u26A0\uFE0F ALWAYS READ FIRST:** \`.safeword/SAFEWORD.md\`
157
-
158
- The SAFEWORD.md file contains core development patterns, workflows, and conventions.
159
- Read it BEFORE working on any task in this project.
160
-
161
- ---`;
162
- function getPrettierConfig(projectType) {
163
- const config = {
164
- semi: true,
165
- singleQuote: true,
166
- tabWidth: 2,
167
- trailingComma: "es5",
168
- printWidth: 100,
169
- endOfLine: "lf"
170
- };
171
- const plugins = [];
172
- if (projectType.astro) plugins.push("prettier-plugin-astro");
173
- if (projectType.svelte) plugins.push("prettier-plugin-svelte");
174
- if (projectType.shell) plugins.push("prettier-plugin-sh");
175
- if (projectType.tailwind) plugins.push("prettier-plugin-tailwindcss");
176
- if (plugins.length > 0) {
177
- config.plugins = plugins;
178
- }
179
- return JSON.stringify(config, null, 2) + "\n";
180
- }
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;
87
+ writeFile(path, `${JSON.stringify(data, void 0, 2)}
88
+ `);
192
89
  }
193
90
 
194
91
  // src/templates/config.ts
195
- function getEslintConfig(options) {
196
- return `/* eslint-disable import-x/no-unresolved, no-undef -- dynamic imports for optional framework plugins, console used in tryImport */
92
+ function getEslintConfig() {
93
+ return `/* eslint-disable import-x/no-unresolved, no-undef -- dynamic imports for optional framework plugins */
197
94
  import { readFileSync } from "fs";
198
95
  import { dirname, join } from "path";
199
96
  import { fileURLToPath } from "url";
200
- import { defineConfig } from "eslint/config";
201
- import js from "@eslint/js";
202
- import { importX } from "eslint-plugin-import-x";
203
- import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
204
- import sonarjs from "eslint-plugin-sonarjs";
205
- import sdl from "@microsoft/eslint-plugin-sdl";
206
- import playwright from "eslint-plugin-playwright";
207
- import unicorn from "eslint-plugin-unicorn";
97
+ import safeword from "eslint-plugin-safeword";
208
98
  import eslintConfigPrettier from "eslint-config-prettier";
209
- ${options.boundaries ? 'import boundariesConfig from "./.safeword/eslint-boundaries.config.mjs";' : ""}
210
99
 
211
100
  // Read package.json relative to this config file (not CWD)
212
- // This ensures correct detection in monorepos where lint-staged may run from subdirectories
213
101
  const __dirname = dirname(fileURLToPath(import.meta.url));
214
102
  const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
215
103
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
@@ -236,122 +124,55 @@ if (deps["astro"]) ignores.push(".astro/");
236
124
  if (deps["vue"] || deps["nuxt"]) ignores.push(".nuxt/");
237
125
  if (deps["svelte"] || deps["@sveltejs/kit"]) ignores.push(".svelte-kit/");
238
126
 
239
- // Start with base configs (always loaded)
127
+ // Select appropriate safeword config based on detected framework
128
+ // Order matters: most specific first
129
+ let baseConfig;
130
+ if (deps["next"]) {
131
+ baseConfig = safeword.configs.recommendedTypeScriptNext;
132
+ } else if (deps["react"]) {
133
+ baseConfig = safeword.configs.recommendedTypeScriptReact;
134
+ } else if (deps["astro"]) {
135
+ baseConfig = safeword.configs.astro;
136
+ } else if (deps["typescript"] || deps["typescript-eslint"]) {
137
+ baseConfig = safeword.configs.recommendedTypeScript;
138
+ } else {
139
+ baseConfig = safeword.configs.recommended;
140
+ }
141
+
142
+ // Start with ignores + safeword config
240
143
  const configs = [
241
144
  { ignores },
242
- js.configs.recommended,
243
- importX.flatConfigs.recommended,
244
- {
245
- settings: {
246
- "import-x/resolver-next": [createTypeScriptImportResolver()],
247
- },
248
- },
249
- sonarjs.configs.recommended,
250
- ...sdl.configs.recommended,
251
- unicorn.configs["flat/recommended"],
252
- {
253
- // Unicorn overrides for LLM-generated code
254
- // Keep modern JS enforcement, disable overly pedantic rules
255
- rules: {
256
- "unicorn/prevent-abbreviations": "off", // ctx, dir, pkg, err are standard
257
- "unicorn/no-null": "off", // null is valid JS
258
- "unicorn/no-process-exit": "off", // CLI apps use process.exit
259
- "unicorn/import-style": "off", // Named imports are fine
260
- "unicorn/numeric-separators-style": "off", // Style preference
261
- "unicorn/text-encoding-identifier-case": "off", // utf-8 vs utf8
262
- "unicorn/switch-case-braces": "warn", // Good practice, not critical
263
- "unicorn/catch-error-name": "warn", // Reasonable, auto-fixable
264
- "unicorn/no-negated-condition": "off", // Sometimes clearer
265
- "unicorn/no-array-reduce": "off", // Reduce is fine when readable
266
- "unicorn/no-array-for-each": "off", // forEach is fine
267
- "unicorn/prefer-module": "off", // CJS still valid
268
- },
269
- },
145
+ ...baseConfig,
270
146
  ];
271
147
 
272
- // TypeScript support (detected from package.json)
273
- if (deps["typescript"] || deps["typescript-eslint"]) {
274
- const tseslint = await tryImport("typescript-eslint", "TypeScript");
275
- configs.push(importX.flatConfigs.typescript);
276
- configs.push(...tseslint.default.configs.recommended);
277
- }
278
-
279
- // React/Next.js support
280
- if (deps["react"] || deps["next"]) {
281
- const react = await tryImport("eslint-plugin-react", "React");
282
- const reactHooks = await tryImport("eslint-plugin-react-hooks", "React");
283
- const jsxA11y = await tryImport("eslint-plugin-jsx-a11y", "React");
284
- configs.push(react.default.configs.flat.recommended);
285
- configs.push(react.default.configs.flat["jsx-runtime"]);
286
- configs.push({
287
- name: "react-hooks",
288
- plugins: { "react-hooks": reactHooks.default },
289
- rules: reactHooks.default.configs.recommended.rules,
290
- });
291
- configs.push(jsxA11y.default.flatConfigs.recommended);
292
- }
293
-
294
- // Next.js plugin
295
- if (deps["next"]) {
296
- const nextPlugin = await tryImport("@next/eslint-plugin-next", "Next.js");
297
- configs.push({
298
- name: "nextjs",
299
- plugins: { "@next/next": nextPlugin.default },
300
- rules: nextPlugin.default.configs.recommended.rules,
301
- });
148
+ // Add test configs if testing frameworks detected
149
+ if (deps["vitest"]) {
150
+ configs.push(...safeword.configs.vitest);
302
151
  }
303
-
304
- // Astro support
305
- if (deps["astro"]) {
306
- const astro = await tryImport("eslint-plugin-astro", "Astro");
307
- configs.push(...astro.default.configs.recommended);
152
+ if (deps["playwright"] || deps["@playwright/test"]) {
153
+ configs.push(...safeword.configs.playwright);
308
154
  }
309
155
 
310
- // Vue support
156
+ // Frameworks NOT in eslint-plugin-safeword (dynamic imports)
311
157
  if (deps["vue"] || deps["nuxt"]) {
312
158
  const vue = await tryImport("eslint-plugin-vue", "Vue");
313
159
  configs.push(...vue.default.configs["flat/recommended"]);
314
160
  }
315
161
 
316
- // Svelte support
317
162
  if (deps["svelte"] || deps["@sveltejs/kit"]) {
318
163
  const svelte = await tryImport("eslint-plugin-svelte", "Svelte");
319
164
  configs.push(...svelte.default.configs.recommended);
320
165
  }
321
166
 
322
- // Electron support
323
167
  if (deps["electron"]) {
324
168
  const electron = await tryImport("@electron-toolkit/eslint-config", "Electron");
325
169
  configs.push(electron.default);
326
170
  }
327
171
 
328
- // Vitest support (scoped to test files)
329
- if (deps["vitest"]) {
330
- const vitest = await tryImport("@vitest/eslint-plugin", "Vitest");
331
- configs.push({
332
- name: "vitest",
333
- files: ["**/*.test.{js,ts,jsx,tsx}", "**/*.spec.{js,ts,jsx,tsx}", "**/tests/**"],
334
- plugins: { vitest: vitest.default },
335
- languageOptions: {
336
- globals: { ...vitest.default.environments.env.globals },
337
- },
338
- rules: { ...vitest.default.configs.recommended.rules },
339
- });
340
- }
341
-
342
- // Playwright for e2e tests (always included - safeword sets up Playwright)
343
- configs.push({
344
- name: "playwright",
345
- files: ["**/e2e/**", "**/*.e2e.{js,ts,jsx,tsx}", "**/playwright/**"],
346
- ...playwright.configs["flat/recommended"],
347
- });
348
-
349
- // Architecture boundaries${options.boundaries ? "\nconfigs.push(boundariesConfig);" : ""}
350
-
351
172
  // eslint-config-prettier must be last to disable conflicting rules
352
173
  configs.push(eslintConfigPrettier);
353
174
 
354
- export default defineConfig(configs);
175
+ export default configs;
355
176
  `;
356
177
  }
357
178
  var CURSOR_HOOKS = {
@@ -426,166 +247,50 @@ var SETTINGS_HOOKS = {
426
247
  ]
427
248
  };
428
249
 
429
- // src/utils/boundaries.ts
430
- import { join as join3 } from "path";
431
- import { readdirSync as readdirSync3 } from "fs";
432
- var ARCHITECTURE_LAYERS = [
433
- // Layer 0: Pure types (no imports)
434
- { layer: "types", dirs: ["types", "interfaces", "schemas"] },
435
- // Layer 1: Utilities (only types)
436
- { layer: "utils", dirs: ["utils", "helpers", "shared", "common", "core"] },
437
- // Layer 2: Libraries (types, utils)
438
- { layer: "lib", dirs: ["lib", "libraries"] },
439
- // Layer 3: State & logic (types, utils, lib)
440
- { layer: "hooks", dirs: ["hooks", "composables"] },
441
- { layer: "services", dirs: ["services", "api", "stores", "state"] },
442
- // Layer 4: UI components (all above)
443
- { layer: "components", dirs: ["components", "ui"] },
444
- // Layer 5: Features (all above)
445
- { layer: "features", dirs: ["features", "modules", "domains"] },
446
- // Layer 6: Entry points (can import everything)
447
- { layer: "app", dirs: ["app", "pages", "views", "routes", "commands"] }
448
- ];
449
- var HIERARCHY = {
450
- types: [],
451
- utils: ["types"],
452
- lib: ["utils", "types"],
453
- hooks: ["lib", "utils", "types"],
454
- services: ["lib", "utils", "types"],
455
- components: ["hooks", "services", "lib", "utils", "types"],
456
- features: ["components", "hooks", "services", "lib", "utils", "types"],
457
- app: ["features", "components", "hooks", "services", "lib", "utils", "types"]
458
- };
459
- function findMonorepoPackages(projectDir) {
460
- const packages = [];
461
- const monorepoRoots = ["packages", "apps", "libs", "modules"];
462
- for (const root of monorepoRoots) {
463
- const rootPath = join3(projectDir, root);
464
- if (exists(rootPath)) {
465
- try {
466
- const entries = readdirSync3(rootPath, { withFileTypes: true });
467
- for (const entry of entries) {
468
- if (entry.isDirectory() && !entry.name.startsWith(".")) {
469
- packages.push(join3(root, entry.name));
470
- }
471
- }
472
- } catch {
473
- }
474
- }
475
- }
476
- return packages;
477
- }
478
- function hasLayerForPrefix(elements, layer, pathPrefix) {
479
- return elements.some((e) => e.layer === layer && e.pattern.startsWith(pathPrefix));
480
- }
481
- function scanSearchPath(projectDir, searchPath, pathPrefix, elements) {
482
- for (const layerDef of ARCHITECTURE_LAYERS) {
483
- for (const dirName of layerDef.dirs) {
484
- const fullPath = join3(projectDir, searchPath, dirName);
485
- if (exists(fullPath) && !hasLayerForPrefix(elements, layerDef.layer, pathPrefix)) {
486
- elements.push({
487
- layer: layerDef.layer,
488
- pattern: `${pathPrefix}${dirName}/**`,
489
- location: `${pathPrefix}${dirName}`
490
- });
491
- }
492
- }
493
- }
494
- }
495
- function scanForLayers(projectDir, basePath) {
496
- const elements = [];
497
- const prefix = basePath ? `${basePath}/` : "";
498
- scanSearchPath(projectDir, join3(basePath, "src"), `${prefix}src/`, elements);
499
- scanSearchPath(projectDir, basePath, prefix, elements);
500
- return elements;
501
- }
502
- function detectArchitecture(projectDir) {
503
- const elements = [];
504
- const packages = findMonorepoPackages(projectDir);
505
- const isMonorepo = packages.length > 0;
506
- if (isMonorepo) {
507
- for (const pkg of packages) {
508
- elements.push(...scanForLayers(projectDir, pkg));
509
- }
250
+ // src/templates/content.ts
251
+ var AGENTS_MD_LINK = `**\u26A0\uFE0F ALWAYS READ FIRST:** \`.safeword/SAFEWORD.md\`
252
+
253
+ The SAFEWORD.md file contains core development patterns, workflows, and conventions.
254
+ Read it BEFORE working on any task in this project.
255
+
256
+ ---`;
257
+ function getPrettierConfig(projectType) {
258
+ const config = {
259
+ semi: true,
260
+ singleQuote: true,
261
+ tabWidth: 2,
262
+ trailingComma: "all",
263
+ printWidth: 100,
264
+ endOfLine: "lf",
265
+ useTabs: false,
266
+ bracketSpacing: true,
267
+ arrowParens: "avoid"
268
+ };
269
+ const plugins = [];
270
+ if (projectType.astro) plugins.push("prettier-plugin-astro");
271
+ if (projectType.svelte) plugins.push("prettier-plugin-svelte");
272
+ if (projectType.shell) plugins.push("prettier-plugin-sh");
273
+ if (projectType.tailwind) plugins.push("prettier-plugin-tailwindcss");
274
+ if (plugins.length > 0) {
275
+ config.plugins = plugins;
510
276
  }
511
- elements.push(...scanForLayers(projectDir, ""));
512
- const seen = /* @__PURE__ */ new Set();
513
- const uniqueElements = elements.filter((e) => {
514
- if (seen.has(e.pattern)) return false;
515
- seen.add(e.pattern);
516
- return true;
517
- });
518
- return { elements: uniqueElements, isMonorepo };
519
- }
520
- function formatElement(el) {
521
- return ` { type: '${el.layer}', pattern: '${el.pattern}', mode: 'full' }`;
522
- }
523
- function formatAllowedImports(allowed) {
524
- return allowed.map((d) => `'${d}'`).join(", ");
277
+ return `${JSON.stringify(config, void 0, 2)}
278
+ `;
525
279
  }
526
- function generateRule(layer, detectedLayers) {
527
- const allowedLayers = HIERARCHY[layer];
528
- if (allowedLayers.length === 0) return null;
529
- const allowed = allowedLayers.filter((dep) => detectedLayers.has(dep));
530
- if (allowed.length === 0) return null;
531
- return ` { from: ['${layer}'], allow: [${formatAllowedImports(allowed)}] }`;
280
+
281
+ // src/utils/hooks.ts
282
+ function isHookEntry(h) {
283
+ return typeof h === "object" && h !== void 0 && "hooks" in h && Array.isArray(h.hooks);
532
284
  }
533
- function buildDetectedInfo(arch) {
534
- if (arch.elements.length === 0) {
535
- return "No architecture directories detected yet - add types/, utils/, components/, etc.";
536
- }
537
- const locations = arch.elements.map((e) => e.location).join(", ");
538
- const monorepoNote = arch.isMonorepo ? " (monorepo)" : "";
539
- return `Detected: ${locations}${monorepoNote}`;
285
+ function isSafewordHook(h) {
286
+ if (!isHookEntry(h)) return false;
287
+ return h.hooks.some((cmd) => typeof cmd.command === "string" && cmd.command.includes(".safeword"));
540
288
  }
541
- function generateBoundariesConfig(arch) {
542
- const hasElements = arch.elements.length > 0;
543
- const elementsContent = arch.elements.map((el) => formatElement(el)).join(",\n");
544
- const detectedLayers = new Set(arch.elements.map((e) => e.layer));
545
- const rules = [...detectedLayers].map((layer) => generateRule(layer, detectedLayers)).filter((rule) => rule !== null);
546
- const rulesContent = rules.join(",\n");
547
- const detectedInfo = buildDetectedInfo(arch);
548
- return `/**
549
- * Architecture Boundaries Configuration (AUTO-GENERATED)
550
- *
551
- * ${detectedInfo}
552
- *
553
- * This enforces import boundaries between architectural layers:
554
- * - Lower layers (types, utils) cannot import from higher layers (components, features)
555
- * - Uses 'warn' severity - informative, not blocking
556
- *
557
- * Recognized directories (in hierarchy order):
558
- * types \u2192 utils \u2192 lib \u2192 hooks/services \u2192 components \u2192 features/modules \u2192 app
559
- *
560
- * To customize, override in your eslint.config.mjs:
561
- * rules: { 'boundaries/element-types': ['error', { ... }] }
562
- */
563
-
564
- import boundaries from 'eslint-plugin-boundaries';
565
-
566
- export default {
567
- plugins: { boundaries },
568
- settings: {
569
- 'boundaries/elements': [
570
- ${elementsContent}
571
- ],
572
- },
573
- rules: {${hasElements ? `
574
- 'boundaries/element-types': ['warn', {
575
- default: 'disallow',
576
- rules: [
577
- ${rulesContent}
578
- ],
579
- }],` : ""}
580
- 'boundaries/no-unknown': 'off', // Allow files outside defined elements
581
- 'boundaries/no-unknown-files': 'off', // Allow non-matching files
582
- },
583
- };
584
- `;
289
+ function filterOutSafewordHooks(hooks) {
290
+ return hooks.filter((h) => !isSafewordHook(h));
585
291
  }
586
292
 
587
293
  // src/utils/install.ts
588
- var HUSKY_PRE_COMMIT_CONTENT = "npx safeword sync --quiet --stage\nnpx lint-staged\n";
589
294
  var MCP_SERVERS = {
590
295
  context7: {
591
296
  command: "npx",
@@ -597,18 +302,6 @@ var MCP_SERVERS = {
597
302
  }
598
303
  };
599
304
 
600
- // src/utils/hooks.ts
601
- function isHookEntry(h) {
602
- return typeof h === "object" && h !== null && "hooks" in h && Array.isArray(h.hooks);
603
- }
604
- function isSafewordHook(h) {
605
- if (!isHookEntry(h)) return false;
606
- return h.hooks.some((cmd) => typeof cmd.command === "string" && cmd.command.includes(".safeword"));
607
- }
608
- function filterOutSafewordHooks(hooks) {
609
- return hooks.filter((h) => !isSafewordHook(h));
610
- }
611
-
612
305
  // src/schema.ts
613
306
  function isEslintPackage(pkg) {
614
307
  return pkg.startsWith("eslint") || pkg.startsWith("@eslint/") || pkg.startsWith("@microsoft/eslint") || pkg.startsWith("@next/eslint") || pkg.startsWith("@vitest/eslint") || pkg.startsWith("@electron-toolkit/eslint") || pkg === "typescript-eslint";
@@ -638,7 +331,6 @@ var SAFEWORD_SCHEMA = {
638
331
  ".safeword/planning/issues",
639
332
  ".safeword/planning/plans",
640
333
  ".safeword/scripts",
641
- ".husky",
642
334
  ".cursor",
643
335
  ".cursor/rules",
644
336
  ".cursor/commands"
@@ -659,16 +351,47 @@ var SAFEWORD_SCHEMA = {
659
351
  ".safeword/guides/development-workflow.md",
660
352
  ".safeword/guides/tdd-best-practices.md",
661
353
  ".safeword/guides/user-story-guide.md",
662
- ".safeword/guides/test-definitions-guide.md"
354
+ ".safeword/guides/test-definitions-guide.md",
355
+ // Boundaries config now project-specific (v0.9.0)
356
+ ".safeword/eslint-boundaries.config.mjs"
357
+ ],
358
+ // Packages to uninstall on upgrade (consolidated into eslint-plugin-safeword v0.9.0)
359
+ deprecatedPackages: [
360
+ // Individual ESLint plugins now bundled in eslint-plugin-safeword
361
+ "@eslint/js",
362
+ "eslint-plugin-import-x",
363
+ "eslint-import-resolver-typescript",
364
+ "eslint-plugin-sonarjs",
365
+ "eslint-plugin-unicorn",
366
+ "eslint-plugin-boundaries",
367
+ "eslint-plugin-playwright",
368
+ "eslint-plugin-promise",
369
+ "eslint-plugin-regexp",
370
+ "eslint-plugin-jsdoc",
371
+ "eslint-plugin-simple-import-sort",
372
+ "eslint-plugin-security",
373
+ // Conditional ESLint plugins now in safeword
374
+ "typescript-eslint",
375
+ "eslint-plugin-react",
376
+ "eslint-plugin-react-hooks",
377
+ "eslint-plugin-jsx-a11y",
378
+ "@next/eslint-plugin-next",
379
+ "eslint-plugin-astro",
380
+ "@vitest/eslint-plugin",
381
+ // Pre-commit hooks no longer managed by safeword
382
+ "husky",
383
+ "lint-staged"
384
+ ],
385
+ // Directories to delete on upgrade (no longer managed by safeword)
386
+ deprecatedDirs: [
387
+ ".husky"
388
+ // Pre-commit hooks no longer managed by safeword
663
389
  ],
664
390
  // Files owned by safeword (overwritten on upgrade if content changed)
665
391
  ownedFiles: {
666
392
  // Core files
667
393
  ".safeword/SAFEWORD.md": { template: "SAFEWORD.md" },
668
394
  ".safeword/version": { content: () => VERSION },
669
- ".safeword/eslint-boundaries.config.mjs": {
670
- generator: (ctx) => generateBoundariesConfig(detectArchitecture(ctx.cwd))
671
- },
672
395
  // Hooks (7 files)
673
396
  ".safeword/hooks/session-verify-agents.sh": { template: "hooks/session-verify-agents.sh" },
674
397
  ".safeword/hooks/session-version.sh": { template: "hooks/session-version.sh" },
@@ -715,11 +438,12 @@ var SAFEWORD_SCHEMA = {
715
438
  // Prompts (2 files)
716
439
  ".safeword/prompts/architecture.md": { template: "prompts/architecture.md" },
717
440
  ".safeword/prompts/quality-review.md": { template: "prompts/quality-review.md" },
718
- // Scripts (3 files)
441
+ // Scripts (4 files)
719
442
  ".safeword/scripts/bisect-test-pollution.sh": { template: "scripts/bisect-test-pollution.sh" },
720
443
  ".safeword/scripts/bisect-zombie-processes.sh": {
721
444
  template: "scripts/bisect-zombie-processes.sh"
722
445
  },
446
+ ".safeword/scripts/cleanup-zombies.sh": { template: "scripts/cleanup-zombies.sh" },
723
447
  ".safeword/scripts/lint-md.sh": { template: "scripts/lint-md.sh" },
724
448
  // Claude skills and commands (9 files)
725
449
  ".claude/skills/safeword-brainstorming/SKILL.md": {
@@ -741,10 +465,9 @@ var SAFEWORD_SCHEMA = {
741
465
  template: "skills/safeword-writing-plans/SKILL.md"
742
466
  },
743
467
  ".claude/commands/architecture.md": { template: "commands/architecture.md" },
468
+ ".claude/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
744
469
  ".claude/commands/lint.md": { template: "commands/lint.md" },
745
470
  ".claude/commands/quality-review.md": { template: "commands/quality-review.md" },
746
- // Husky (1 file)
747
- ".husky/pre-commit": { content: HUSKY_PRE_COMMIT_CONTENT },
748
471
  // Cursor rules (7 files)
749
472
  ".cursor/rules/safeword-core.mdc": { template: "cursor/rules/safeword-core.mdc" },
750
473
  ".cursor/rules/safeword-brainstorming.mdc": {
@@ -765,10 +488,11 @@ var SAFEWORD_SCHEMA = {
765
488
  ".cursor/rules/safeword-writing-plans.mdc": {
766
489
  template: "cursor/rules/safeword-writing-plans.mdc"
767
490
  },
768
- // Cursor commands (3 files - same as Claude)
491
+ // Cursor commands (4 files - same as Claude)
492
+ ".cursor/commands/architecture.md": { template: "commands/architecture.md" },
493
+ ".cursor/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
769
494
  ".cursor/commands/lint.md": { template: "commands/lint.md" },
770
495
  ".cursor/commands/quality-review.md": { template: "commands/quality-review.md" },
771
- ".cursor/commands/architecture.md": { template: "commands/architecture.md" },
772
496
  // Cursor hooks adapters (2 files)
773
497
  ".safeword/hooks/cursor/after-file-edit.sh": { template: "hooks/cursor/after-file-edit.sh" },
774
498
  ".safeword/hooks/cursor/stop.sh": { template: "hooks/cursor/stop.sh" }
@@ -776,10 +500,35 @@ var SAFEWORD_SCHEMA = {
776
500
  // Files created if missing, updated only if content matches current template
777
501
  managedFiles: {
778
502
  "eslint.config.mjs": {
779
- generator: () => getEslintConfig({ boundaries: true })
503
+ generator: () => getEslintConfig()
780
504
  },
781
505
  ".prettierrc": { generator: (ctx) => getPrettierConfig(ctx.projectType) },
782
- ".markdownlint-cli2.jsonc": { template: "markdownlint-cli2.jsonc" }
506
+ ".markdownlint-cli2.jsonc": { template: "markdownlint-cli2.jsonc" },
507
+ // Minimal tsconfig for ESLint type-checked linting (only if missing)
508
+ "tsconfig.json": {
509
+ generator: (ctx) => {
510
+ if (!ctx.developmentDeps.typescript && !ctx.developmentDeps["typescript-eslint"]) {
511
+ return "";
512
+ }
513
+ return JSON.stringify(
514
+ {
515
+ compilerOptions: {
516
+ target: "ES2022",
517
+ module: "NodeNext",
518
+ moduleResolution: "NodeNext",
519
+ strict: true,
520
+ esModuleInterop: true,
521
+ skipLibCheck: true,
522
+ noEmit: true
523
+ },
524
+ include: ["**/*.ts", "**/*.tsx"],
525
+ exclude: ["node_modules", "dist", "build"]
526
+ },
527
+ void 0,
528
+ 2
529
+ );
530
+ }
531
+ }
783
532
  },
784
533
  // JSON files where we merge specific keys
785
534
  jsonMerges: {
@@ -789,23 +538,20 @@ var SAFEWORD_SCHEMA = {
789
538
  "scripts.lint:md",
790
539
  "scripts.format",
791
540
  "scripts.format:check",
792
- "scripts.knip",
793
- "scripts.prepare",
794
- "lint-staged"
541
+ "scripts.knip"
795
542
  ],
796
543
  conditionalKeys: {
797
544
  publishableLibrary: ["scripts.publint"],
798
545
  shell: ["scripts.lint:sh"]
799
546
  },
800
547
  merge: (existing, ctx) => {
801
- const scripts = existing.scripts ?? {};
548
+ const scripts = { ...existing.scripts };
802
549
  const result = { ...existing };
803
550
  if (!scripts.lint) scripts.lint = "eslint .";
804
551
  if (!scripts["lint:md"]) scripts["lint:md"] = 'markdownlint-cli2 "**/*.md" "#node_modules"';
805
552
  if (!scripts.format) scripts.format = "prettier --write .";
806
553
  if (!scripts["format:check"]) scripts["format:check"] = "prettier --check .";
807
554
  if (!scripts.knip) scripts.knip = "knip";
808
- if (!scripts.prepare) scripts.prepare = "husky || true";
809
555
  if (ctx.projectType.publishableLibrary && !scripts.publint) {
810
556
  scripts.publint = "publint";
811
557
  }
@@ -813,9 +559,6 @@ var SAFEWORD_SCHEMA = {
813
559
  scripts["lint:sh"] = "shellcheck **/*.sh";
814
560
  }
815
561
  result.scripts = scripts;
816
- if (!existing["lint-staged"]) {
817
- result["lint-staged"] = getLintStagedConfig(ctx.projectType);
818
- }
819
562
  return result;
820
563
  },
821
564
  unmerge: (existing) => {
@@ -825,14 +568,12 @@ var SAFEWORD_SCHEMA = {
825
568
  delete scripts["lint:sh"];
826
569
  delete scripts["format:check"];
827
570
  delete scripts.knip;
828
- delete scripts.prepare;
829
571
  delete scripts.publint;
830
572
  if (Object.keys(scripts).length > 0) {
831
573
  result.scripts = scripts;
832
574
  } else {
833
575
  delete result.scripts;
834
576
  }
835
- delete result["lint-staged"];
836
577
  return result;
837
578
  }
838
579
  },
@@ -969,42 +710,98 @@ var SAFEWORD_SCHEMA = {
969
710
  // NPM packages to install
970
711
  packages: {
971
712
  base: [
713
+ // Core tools
972
714
  "eslint",
973
715
  "prettier",
974
- "@eslint/js",
975
- "eslint-plugin-import-x",
976
- "eslint-import-resolver-typescript",
977
- "eslint-plugin-sonarjs",
978
- "eslint-plugin-unicorn",
979
- "eslint-plugin-boundaries",
980
- "eslint-plugin-playwright",
981
- "@microsoft/eslint-plugin-sdl",
982
716
  "eslint-config-prettier",
717
+ // Safeword plugin (bundles all ESLint plugins)
718
+ "eslint-plugin-safeword",
719
+ // Non-ESLint tools
983
720
  "markdownlint-cli2",
984
- "knip",
985
- "husky",
986
- "lint-staged"
721
+ "knip"
987
722
  ],
988
723
  conditional: {
989
- typescript: ["typescript-eslint"],
990
- react: ["eslint-plugin-react", "eslint-plugin-react-hooks", "eslint-plugin-jsx-a11y"],
991
- nextjs: ["@next/eslint-plugin-next"],
992
- astro: ["eslint-plugin-astro", "prettier-plugin-astro"],
724
+ // Frameworks NOT in eslint-plugin-safeword
993
725
  vue: ["eslint-plugin-vue"],
994
726
  svelte: ["eslint-plugin-svelte", "prettier-plugin-svelte"],
995
727
  electron: ["@electron-toolkit/eslint-config"],
996
- vitest: ["@vitest/eslint-plugin"],
728
+ // Prettier plugins
729
+ astro: ["prettier-plugin-astro"],
997
730
  tailwind: ["prettier-plugin-tailwindcss"],
731
+ // Non-ESLint tools
998
732
  publishableLibrary: ["publint"],
999
733
  shell: ["shellcheck", "prettier-plugin-sh"]
1000
734
  }
1001
735
  }
1002
736
  };
1003
737
 
738
+ // src/utils/project-detector.ts
739
+ import { readdirSync as readdirSync2 } from "fs";
740
+ import nodePath2 from "path";
741
+ function hasShellScripts(cwd, maxDepth = 4) {
742
+ const excludeDirectories = /* @__PURE__ */ new Set(["node_modules", ".git", ".safeword"]);
743
+ function scan(dir, depth) {
744
+ if (depth > maxDepth) return false;
745
+ try {
746
+ const entries = readdirSync2(dir, { withFileTypes: true });
747
+ for (const entry of entries) {
748
+ if (entry.isFile() && entry.name.endsWith(".sh")) {
749
+ return true;
750
+ }
751
+ if (entry.isDirectory() && !excludeDirectories.has(entry.name) && scan(nodePath2.join(dir, entry.name), depth + 1)) {
752
+ return true;
753
+ }
754
+ }
755
+ } catch {
756
+ }
757
+ return false;
758
+ }
759
+ return scan(cwd, 0);
760
+ }
761
+ function detectProjectType(packageJson, cwd) {
762
+ const deps = packageJson.dependencies || {};
763
+ const developmentDeps = packageJson.devDependencies || {};
764
+ const allDeps = { ...deps, ...developmentDeps };
765
+ const hasTypescript = "typescript" in allDeps;
766
+ const hasReact = "react" in deps || "react" in developmentDeps;
767
+ const hasNextJs = "next" in deps;
768
+ const hasAstro = "astro" in deps || "astro" in developmentDeps;
769
+ const hasVue = "vue" in deps || "vue" in developmentDeps;
770
+ const hasNuxt = "nuxt" in deps;
771
+ const hasSvelte = "svelte" in deps || "svelte" in developmentDeps;
772
+ const hasSvelteKit = "@sveltejs/kit" in deps || "@sveltejs/kit" in developmentDeps;
773
+ const hasElectron = "electron" in deps || "electron" in developmentDeps;
774
+ const hasVitest = "vitest" in developmentDeps;
775
+ const hasPlaywright = "@playwright/test" in developmentDeps;
776
+ const hasTailwind = "tailwindcss" in allDeps;
777
+ const hasEntryPoints = !!(packageJson.main || packageJson.module || packageJson.exports);
778
+ const isPublishable = hasEntryPoints && packageJson.private !== true;
779
+ const hasShell = cwd ? hasShellScripts(cwd) : false;
780
+ return {
781
+ typescript: hasTypescript,
782
+ react: hasReact || hasNextJs,
783
+ // Next.js implies React
784
+ nextjs: hasNextJs,
785
+ astro: hasAstro,
786
+ vue: hasVue || hasNuxt,
787
+ // Nuxt implies Vue
788
+ nuxt: hasNuxt,
789
+ svelte: hasSvelte || hasSvelteKit,
790
+ // SvelteKit implies Svelte
791
+ sveltekit: hasSvelteKit,
792
+ electron: hasElectron,
793
+ vitest: hasVitest,
794
+ playwright: hasPlaywright,
795
+ tailwind: hasTailwind,
796
+ publishableLibrary: isPublishable,
797
+ shell: hasShell
798
+ };
799
+ }
800
+
1004
801
  export {
1005
- getTemplatesDir,
802
+ getTemplatesDirectory,
1006
803
  exists,
1007
- ensureDir,
804
+ ensureDirectory,
1008
805
  readFile,
1009
806
  readFileSafe,
1010
807
  writeFile,
@@ -1013,9 +810,9 @@ export {
1013
810
  makeScriptsExecutable,
1014
811
  readJson,
1015
812
  writeJson,
1016
- detectProjectType,
1017
813
  getBaseEslintPackages,
1018
814
  getConditionalEslintPackages,
1019
- SAFEWORD_SCHEMA
815
+ SAFEWORD_SCHEMA,
816
+ detectProjectType
1020
817
  };
1021
- //# sourceMappingURL=chunk-DXT6TWW4.js.map
818
+ //# sourceMappingURL=chunk-CLSGXTOL.js.map