safeword 0.8.7 → 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 (38) hide show
  1. package/dist/{check-PPVIEF3Q.js → check-QMAGWUOA.js} +24 -22
  2. package/dist/check-QMAGWUOA.js.map +1 -0
  3. package/dist/{chunk-BFBUEJDH.js → chunk-4URRFBUS.js} +10 -10
  4. package/dist/chunk-4URRFBUS.js.map +1 -0
  5. package/dist/{chunk-34PU3QZI.js → chunk-CLSGXTOL.js} +232 -461
  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-3OK3NQEW.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-S3ICSYQY.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-ZST2SGZ2.js → reset-QRXG7KZZ.js} +8 -8
  17. package/dist/reset-QRXG7KZZ.js.map +1 -0
  18. package/dist/{setup-ANAIEP3D.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-QFIGWZ5I.js → upgrade-FALAUUKE.js} +22 -10
  22. package/dist/upgrade-FALAUUKE.js.map +1 -0
  23. package/package.json +2 -2
  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/dist/check-PPVIEF3Q.js.map +0 -1
  29. package/dist/chunk-34PU3QZI.js.map +0 -1
  30. package/dist/chunk-3OK3NQEW.js.map +0 -1
  31. package/dist/chunk-BFBUEJDH.js.map +0 -1
  32. package/dist/chunk-W66Z3C5H.js.map +0 -1
  33. package/dist/diff-S3ICSYQY.js.map +0 -1
  34. package/dist/reset-ZST2SGZ2.js.map +0 -1
  35. package/dist/setup-ANAIEP3D.js.map +0 -1
  36. package/dist/sync-V6D7QTMO.js +0 -9
  37. package/dist/upgrade-QFIGWZ5I.js.map +0 -1
  38. /package/dist/{sync-V6D7QTMO.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,148 +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
- // Uses type-aware rules if tsconfig.json exists, otherwise falls back to basic rules
274
- if (deps["typescript"] || deps["typescript-eslint"]) {
275
- const tseslint = await tryImport("typescript-eslint", "TypeScript");
276
- const { existsSync } = await import("fs");
277
- const hasTsconfig = existsSync(join(__dirname, "tsconfig.json"));
278
-
279
- configs.push(importX.flatConfigs.typescript);
280
-
281
- if (hasTsconfig) {
282
- // Type-aware linting (recommended + stylistic)
283
- configs.push(...tseslint.default.configs.recommendedTypeChecked);
284
- configs.push(...tseslint.default.configs.stylisticTypeChecked);
285
- configs.push({
286
- languageOptions: {
287
- parserOptions: {
288
- projectService: true,
289
- tsconfigRootDir: __dirname,
290
- },
291
- },
292
- });
293
- // Disable type-checked rules for JS files (no type info available)
294
- configs.push({
295
- files: ["**/*.js", "**/*.mjs", "**/*.cjs"],
296
- extends: [tseslint.default.configs.disableTypeChecked],
297
- });
298
- } else {
299
- // Fall back to non-type-aware rules when no tsconfig exists
300
- configs.push(...tseslint.default.configs.recommended);
301
- configs.push(...tseslint.default.configs.stylistic);
302
- }
303
- }
304
-
305
- // React/Next.js support
306
- if (deps["react"] || deps["next"]) {
307
- const react = await tryImport("eslint-plugin-react", "React");
308
- const reactHooks = await tryImport("eslint-plugin-react-hooks", "React");
309
- const jsxA11y = await tryImport("eslint-plugin-jsx-a11y", "React");
310
- configs.push(react.default.configs.flat.recommended);
311
- configs.push(react.default.configs.flat["jsx-runtime"]);
312
- configs.push({
313
- name: "react-hooks",
314
- plugins: { "react-hooks": reactHooks.default },
315
- rules: reactHooks.default.configs.recommended.rules,
316
- });
317
- configs.push(jsxA11y.default.flatConfigs.recommended);
318
- }
319
-
320
- // Next.js plugin
321
- if (deps["next"]) {
322
- const nextPlugin = await tryImport("@next/eslint-plugin-next", "Next.js");
323
- configs.push({
324
- name: "nextjs",
325
- plugins: { "@next/next": nextPlugin.default },
326
- rules: nextPlugin.default.configs.recommended.rules,
327
- });
148
+ // Add test configs if testing frameworks detected
149
+ if (deps["vitest"]) {
150
+ configs.push(...safeword.configs.vitest);
328
151
  }
329
-
330
- // Astro support
331
- if (deps["astro"]) {
332
- const astro = await tryImport("eslint-plugin-astro", "Astro");
333
- configs.push(...astro.default.configs.recommended);
152
+ if (deps["playwright"] || deps["@playwright/test"]) {
153
+ configs.push(...safeword.configs.playwright);
334
154
  }
335
155
 
336
- // Vue support
156
+ // Frameworks NOT in eslint-plugin-safeword (dynamic imports)
337
157
  if (deps["vue"] || deps["nuxt"]) {
338
158
  const vue = await tryImport("eslint-plugin-vue", "Vue");
339
159
  configs.push(...vue.default.configs["flat/recommended"]);
340
160
  }
341
161
 
342
- // Svelte support
343
162
  if (deps["svelte"] || deps["@sveltejs/kit"]) {
344
163
  const svelte = await tryImport("eslint-plugin-svelte", "Svelte");
345
164
  configs.push(...svelte.default.configs.recommended);
346
165
  }
347
166
 
348
- // Electron support
349
167
  if (deps["electron"]) {
350
168
  const electron = await tryImport("@electron-toolkit/eslint-config", "Electron");
351
169
  configs.push(electron.default);
352
170
  }
353
171
 
354
- // Vitest support (scoped to test files)
355
- if (deps["vitest"]) {
356
- const vitest = await tryImport("@vitest/eslint-plugin", "Vitest");
357
- configs.push({
358
- name: "vitest",
359
- files: ["**/*.test.{js,ts,jsx,tsx}", "**/*.spec.{js,ts,jsx,tsx}", "**/tests/**"],
360
- plugins: { vitest: vitest.default },
361
- languageOptions: {
362
- globals: { ...vitest.default.environments.env.globals },
363
- },
364
- rules: { ...vitest.default.configs.recommended.rules },
365
- });
366
- }
367
-
368
- // Playwright for e2e tests (always included - safeword sets up Playwright)
369
- configs.push({
370
- name: "playwright",
371
- files: ["**/e2e/**", "**/*.e2e.{js,ts,jsx,tsx}", "**/playwright/**"],
372
- ...playwright.configs["flat/recommended"],
373
- });
374
-
375
- // Architecture boundaries${options.boundaries ? "\nconfigs.push(boundariesConfig);" : ""}
376
-
377
172
  // eslint-config-prettier must be last to disable conflicting rules
378
173
  configs.push(eslintConfigPrettier);
379
174
 
380
- export default defineConfig(configs);
175
+ export default configs;
381
176
  `;
382
177
  }
383
178
  var CURSOR_HOOKS = {
@@ -452,166 +247,50 @@ var SETTINGS_HOOKS = {
452
247
  ]
453
248
  };
454
249
 
455
- // src/utils/boundaries.ts
456
- import { join as join3 } from "path";
457
- import { readdirSync as readdirSync3 } from "fs";
458
- var ARCHITECTURE_LAYERS = [
459
- // Layer 0: Pure types (no imports)
460
- { layer: "types", dirs: ["types", "interfaces", "schemas"] },
461
- // Layer 1: Utilities (only types)
462
- { layer: "utils", dirs: ["utils", "helpers", "shared", "common", "core"] },
463
- // Layer 2: Libraries (types, utils)
464
- { layer: "lib", dirs: ["lib", "libraries"] },
465
- // Layer 3: State & logic (types, utils, lib)
466
- { layer: "hooks", dirs: ["hooks", "composables"] },
467
- { layer: "services", dirs: ["services", "api", "stores", "state"] },
468
- // Layer 4: UI components (all above)
469
- { layer: "components", dirs: ["components", "ui"] },
470
- // Layer 5: Features (all above)
471
- { layer: "features", dirs: ["features", "modules", "domains"] },
472
- // Layer 6: Entry points (can import everything)
473
- { layer: "app", dirs: ["app", "pages", "views", "routes", "commands"] }
474
- ];
475
- var HIERARCHY = {
476
- types: [],
477
- utils: ["types"],
478
- lib: ["utils", "types"],
479
- hooks: ["lib", "utils", "types"],
480
- services: ["lib", "utils", "types"],
481
- components: ["hooks", "services", "lib", "utils", "types"],
482
- features: ["components", "hooks", "services", "lib", "utils", "types"],
483
- app: ["features", "components", "hooks", "services", "lib", "utils", "types"]
484
- };
485
- function findMonorepoPackages(projectDir) {
486
- const packages = [];
487
- const monorepoRoots = ["packages", "apps", "libs", "modules"];
488
- for (const root of monorepoRoots) {
489
- const rootPath = join3(projectDir, root);
490
- if (exists(rootPath)) {
491
- try {
492
- const entries = readdirSync3(rootPath, { withFileTypes: true });
493
- for (const entry of entries) {
494
- if (entry.isDirectory() && !entry.name.startsWith(".")) {
495
- packages.push(join3(root, entry.name));
496
- }
497
- }
498
- } catch {
499
- }
500
- }
501
- }
502
- return packages;
503
- }
504
- function hasLayerForPrefix(elements, layer, pathPrefix) {
505
- return elements.some((e) => e.layer === layer && e.pattern.startsWith(pathPrefix));
506
- }
507
- function scanSearchPath(projectDir, searchPath, pathPrefix, elements) {
508
- for (const layerDef of ARCHITECTURE_LAYERS) {
509
- for (const dirName of layerDef.dirs) {
510
- const fullPath = join3(projectDir, searchPath, dirName);
511
- if (exists(fullPath) && !hasLayerForPrefix(elements, layerDef.layer, pathPrefix)) {
512
- elements.push({
513
- layer: layerDef.layer,
514
- pattern: `${pathPrefix}${dirName}/**`,
515
- location: `${pathPrefix}${dirName}`
516
- });
517
- }
518
- }
519
- }
520
- }
521
- function scanForLayers(projectDir, basePath) {
522
- const elements = [];
523
- const prefix = basePath ? `${basePath}/` : "";
524
- scanSearchPath(projectDir, join3(basePath, "src"), `${prefix}src/`, elements);
525
- scanSearchPath(projectDir, basePath, prefix, elements);
526
- return elements;
527
- }
528
- function detectArchitecture(projectDir) {
529
- const elements = [];
530
- const packages = findMonorepoPackages(projectDir);
531
- const isMonorepo = packages.length > 0;
532
- if (isMonorepo) {
533
- for (const pkg of packages) {
534
- elements.push(...scanForLayers(projectDir, pkg));
535
- }
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;
536
276
  }
537
- elements.push(...scanForLayers(projectDir, ""));
538
- const seen = /* @__PURE__ */ new Set();
539
- const uniqueElements = elements.filter((e) => {
540
- if (seen.has(e.pattern)) return false;
541
- seen.add(e.pattern);
542
- return true;
543
- });
544
- return { elements: uniqueElements, isMonorepo };
545
- }
546
- function formatElement(el) {
547
- return ` { type: '${el.layer}', pattern: '${el.pattern}', mode: 'full' }`;
548
- }
549
- function formatAllowedImports(allowed) {
550
- return allowed.map((d) => `'${d}'`).join(", ");
277
+ return `${JSON.stringify(config, void 0, 2)}
278
+ `;
551
279
  }
552
- function generateRule(layer, detectedLayers) {
553
- const allowedLayers = HIERARCHY[layer];
554
- if (allowedLayers.length === 0) return null;
555
- const allowed = allowedLayers.filter((dep) => detectedLayers.has(dep));
556
- if (allowed.length === 0) return null;
557
- 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);
558
284
  }
559
- function buildDetectedInfo(arch) {
560
- if (arch.elements.length === 0) {
561
- return "No architecture directories detected yet - add types/, utils/, components/, etc.";
562
- }
563
- const locations = arch.elements.map((e) => e.location).join(", ");
564
- const monorepoNote = arch.isMonorepo ? " (monorepo)" : "";
565
- 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"));
566
288
  }
567
- function generateBoundariesConfig(arch) {
568
- const hasElements = arch.elements.length > 0;
569
- const elementsContent = arch.elements.map((el) => formatElement(el)).join(",\n");
570
- const detectedLayers = new Set(arch.elements.map((e) => e.layer));
571
- const rules = [...detectedLayers].map((layer) => generateRule(layer, detectedLayers)).filter((rule) => rule !== null);
572
- const rulesContent = rules.join(",\n");
573
- const detectedInfo = buildDetectedInfo(arch);
574
- return `/**
575
- * Architecture Boundaries Configuration (AUTO-GENERATED)
576
- *
577
- * ${detectedInfo}
578
- *
579
- * This enforces import boundaries between architectural layers:
580
- * - Lower layers (types, utils) cannot import from higher layers (components, features)
581
- * - Uses 'warn' severity - informative, not blocking
582
- *
583
- * Recognized directories (in hierarchy order):
584
- * types \u2192 utils \u2192 lib \u2192 hooks/services \u2192 components \u2192 features/modules \u2192 app
585
- *
586
- * To customize, override in your eslint.config.mjs:
587
- * rules: { 'boundaries/element-types': ['error', { ... }] }
588
- */
589
-
590
- import boundaries from 'eslint-plugin-boundaries';
591
-
592
- export default {
593
- plugins: { boundaries },
594
- settings: {
595
- 'boundaries/elements': [
596
- ${elementsContent}
597
- ],
598
- },
599
- rules: {${hasElements ? `
600
- 'boundaries/element-types': ['warn', {
601
- default: 'disallow',
602
- rules: [
603
- ${rulesContent}
604
- ],
605
- }],` : ""}
606
- 'boundaries/no-unknown': 'off', // Allow files outside defined elements
607
- 'boundaries/no-unknown-files': 'off', // Allow non-matching files
608
- },
609
- };
610
- `;
289
+ function filterOutSafewordHooks(hooks) {
290
+ return hooks.filter((h) => !isSafewordHook(h));
611
291
  }
612
292
 
613
293
  // src/utils/install.ts
614
- var HUSKY_PRE_COMMIT_CONTENT = "npx safeword sync --quiet --stage\nnpx lint-staged\n";
615
294
  var MCP_SERVERS = {
616
295
  context7: {
617
296
  command: "npx",
@@ -623,18 +302,6 @@ var MCP_SERVERS = {
623
302
  }
624
303
  };
625
304
 
626
- // src/utils/hooks.ts
627
- function isHookEntry(h) {
628
- return typeof h === "object" && h !== null && "hooks" in h && Array.isArray(h.hooks);
629
- }
630
- function isSafewordHook(h) {
631
- if (!isHookEntry(h)) return false;
632
- return h.hooks.some((cmd) => typeof cmd.command === "string" && cmd.command.includes(".safeword"));
633
- }
634
- function filterOutSafewordHooks(hooks) {
635
- return hooks.filter((h) => !isSafewordHook(h));
636
- }
637
-
638
305
  // src/schema.ts
639
306
  function isEslintPackage(pkg) {
640
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";
@@ -664,7 +331,6 @@ var SAFEWORD_SCHEMA = {
664
331
  ".safeword/planning/issues",
665
332
  ".safeword/planning/plans",
666
333
  ".safeword/scripts",
667
- ".husky",
668
334
  ".cursor",
669
335
  ".cursor/rules",
670
336
  ".cursor/commands"
@@ -685,16 +351,47 @@ var SAFEWORD_SCHEMA = {
685
351
  ".safeword/guides/development-workflow.md",
686
352
  ".safeword/guides/tdd-best-practices.md",
687
353
  ".safeword/guides/user-story-guide.md",
688
- ".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
689
389
  ],
690
390
  // Files owned by safeword (overwritten on upgrade if content changed)
691
391
  ownedFiles: {
692
392
  // Core files
693
393
  ".safeword/SAFEWORD.md": { template: "SAFEWORD.md" },
694
394
  ".safeword/version": { content: () => VERSION },
695
- ".safeword/eslint-boundaries.config.mjs": {
696
- generator: (ctx) => generateBoundariesConfig(detectArchitecture(ctx.cwd))
697
- },
698
395
  // Hooks (7 files)
699
396
  ".safeword/hooks/session-verify-agents.sh": { template: "hooks/session-verify-agents.sh" },
700
397
  ".safeword/hooks/session-version.sh": { template: "hooks/session-version.sh" },
@@ -741,11 +438,12 @@ var SAFEWORD_SCHEMA = {
741
438
  // Prompts (2 files)
742
439
  ".safeword/prompts/architecture.md": { template: "prompts/architecture.md" },
743
440
  ".safeword/prompts/quality-review.md": { template: "prompts/quality-review.md" },
744
- // Scripts (3 files)
441
+ // Scripts (4 files)
745
442
  ".safeword/scripts/bisect-test-pollution.sh": { template: "scripts/bisect-test-pollution.sh" },
746
443
  ".safeword/scripts/bisect-zombie-processes.sh": {
747
444
  template: "scripts/bisect-zombie-processes.sh"
748
445
  },
446
+ ".safeword/scripts/cleanup-zombies.sh": { template: "scripts/cleanup-zombies.sh" },
749
447
  ".safeword/scripts/lint-md.sh": { template: "scripts/lint-md.sh" },
750
448
  // Claude skills and commands (9 files)
751
449
  ".claude/skills/safeword-brainstorming/SKILL.md": {
@@ -767,10 +465,9 @@ var SAFEWORD_SCHEMA = {
767
465
  template: "skills/safeword-writing-plans/SKILL.md"
768
466
  },
769
467
  ".claude/commands/architecture.md": { template: "commands/architecture.md" },
468
+ ".claude/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
770
469
  ".claude/commands/lint.md": { template: "commands/lint.md" },
771
470
  ".claude/commands/quality-review.md": { template: "commands/quality-review.md" },
772
- // Husky (1 file)
773
- ".husky/pre-commit": { content: HUSKY_PRE_COMMIT_CONTENT },
774
471
  // Cursor rules (7 files)
775
472
  ".cursor/rules/safeword-core.mdc": { template: "cursor/rules/safeword-core.mdc" },
776
473
  ".cursor/rules/safeword-brainstorming.mdc": {
@@ -791,10 +488,11 @@ var SAFEWORD_SCHEMA = {
791
488
  ".cursor/rules/safeword-writing-plans.mdc": {
792
489
  template: "cursor/rules/safeword-writing-plans.mdc"
793
490
  },
794
- // 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" },
795
494
  ".cursor/commands/lint.md": { template: "commands/lint.md" },
796
495
  ".cursor/commands/quality-review.md": { template: "commands/quality-review.md" },
797
- ".cursor/commands/architecture.md": { template: "commands/architecture.md" },
798
496
  // Cursor hooks adapters (2 files)
799
497
  ".safeword/hooks/cursor/after-file-edit.sh": { template: "hooks/cursor/after-file-edit.sh" },
800
498
  ".safeword/hooks/cursor/stop.sh": { template: "hooks/cursor/stop.sh" }
@@ -802,10 +500,35 @@ var SAFEWORD_SCHEMA = {
802
500
  // Files created if missing, updated only if content matches current template
803
501
  managedFiles: {
804
502
  "eslint.config.mjs": {
805
- generator: () => getEslintConfig({ boundaries: true })
503
+ generator: () => getEslintConfig()
806
504
  },
807
505
  ".prettierrc": { generator: (ctx) => getPrettierConfig(ctx.projectType) },
808
- ".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
+ }
809
532
  },
810
533
  // JSON files where we merge specific keys
811
534
  jsonMerges: {
@@ -815,23 +538,20 @@ var SAFEWORD_SCHEMA = {
815
538
  "scripts.lint:md",
816
539
  "scripts.format",
817
540
  "scripts.format:check",
818
- "scripts.knip",
819
- "scripts.prepare",
820
- "lint-staged"
541
+ "scripts.knip"
821
542
  ],
822
543
  conditionalKeys: {
823
544
  publishableLibrary: ["scripts.publint"],
824
545
  shell: ["scripts.lint:sh"]
825
546
  },
826
547
  merge: (existing, ctx) => {
827
- const scripts = existing.scripts ?? {};
548
+ const scripts = { ...existing.scripts };
828
549
  const result = { ...existing };
829
550
  if (!scripts.lint) scripts.lint = "eslint .";
830
551
  if (!scripts["lint:md"]) scripts["lint:md"] = 'markdownlint-cli2 "**/*.md" "#node_modules"';
831
552
  if (!scripts.format) scripts.format = "prettier --write .";
832
553
  if (!scripts["format:check"]) scripts["format:check"] = "prettier --check .";
833
554
  if (!scripts.knip) scripts.knip = "knip";
834
- if (!scripts.prepare) scripts.prepare = "husky || true";
835
555
  if (ctx.projectType.publishableLibrary && !scripts.publint) {
836
556
  scripts.publint = "publint";
837
557
  }
@@ -839,9 +559,6 @@ var SAFEWORD_SCHEMA = {
839
559
  scripts["lint:sh"] = "shellcheck **/*.sh";
840
560
  }
841
561
  result.scripts = scripts;
842
- if (!existing["lint-staged"]) {
843
- result["lint-staged"] = getLintStagedConfig(ctx.projectType);
844
- }
845
562
  return result;
846
563
  },
847
564
  unmerge: (existing) => {
@@ -851,14 +568,12 @@ var SAFEWORD_SCHEMA = {
851
568
  delete scripts["lint:sh"];
852
569
  delete scripts["format:check"];
853
570
  delete scripts.knip;
854
- delete scripts.prepare;
855
571
  delete scripts.publint;
856
572
  if (Object.keys(scripts).length > 0) {
857
573
  result.scripts = scripts;
858
574
  } else {
859
575
  delete result.scripts;
860
576
  }
861
- delete result["lint-staged"];
862
577
  return result;
863
578
  }
864
579
  },
@@ -995,42 +710,98 @@ var SAFEWORD_SCHEMA = {
995
710
  // NPM packages to install
996
711
  packages: {
997
712
  base: [
713
+ // Core tools
998
714
  "eslint",
999
715
  "prettier",
1000
- "@eslint/js",
1001
- "eslint-plugin-import-x",
1002
- "eslint-import-resolver-typescript",
1003
- "eslint-plugin-sonarjs",
1004
- "eslint-plugin-unicorn",
1005
- "eslint-plugin-boundaries",
1006
- "eslint-plugin-playwright",
1007
- "@microsoft/eslint-plugin-sdl",
1008
716
  "eslint-config-prettier",
717
+ // Safeword plugin (bundles all ESLint plugins)
718
+ "eslint-plugin-safeword",
719
+ // Non-ESLint tools
1009
720
  "markdownlint-cli2",
1010
- "knip",
1011
- "husky",
1012
- "lint-staged"
721
+ "knip"
1013
722
  ],
1014
723
  conditional: {
1015
- typescript: ["typescript-eslint"],
1016
- react: ["eslint-plugin-react", "eslint-plugin-react-hooks", "eslint-plugin-jsx-a11y"],
1017
- nextjs: ["@next/eslint-plugin-next"],
1018
- astro: ["eslint-plugin-astro", "prettier-plugin-astro"],
724
+ // Frameworks NOT in eslint-plugin-safeword
1019
725
  vue: ["eslint-plugin-vue"],
1020
726
  svelte: ["eslint-plugin-svelte", "prettier-plugin-svelte"],
1021
727
  electron: ["@electron-toolkit/eslint-config"],
1022
- vitest: ["@vitest/eslint-plugin"],
728
+ // Prettier plugins
729
+ astro: ["prettier-plugin-astro"],
1023
730
  tailwind: ["prettier-plugin-tailwindcss"],
731
+ // Non-ESLint tools
1024
732
  publishableLibrary: ["publint"],
1025
733
  shell: ["shellcheck", "prettier-plugin-sh"]
1026
734
  }
1027
735
  }
1028
736
  };
1029
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
+
1030
801
  export {
1031
- getTemplatesDir,
802
+ getTemplatesDirectory,
1032
803
  exists,
1033
- ensureDir,
804
+ ensureDirectory,
1034
805
  readFile,
1035
806
  readFileSafe,
1036
807
  writeFile,
@@ -1039,9 +810,9 @@ export {
1039
810
  makeScriptsExecutable,
1040
811
  readJson,
1041
812
  writeJson,
1042
- detectProjectType,
1043
813
  getBaseEslintPackages,
1044
814
  getConditionalEslintPackages,
1045
- SAFEWORD_SCHEMA
815
+ SAFEWORD_SCHEMA,
816
+ detectProjectType
1046
817
  };
1047
- //# sourceMappingURL=chunk-34PU3QZI.js.map
818
+ //# sourceMappingURL=chunk-CLSGXTOL.js.map