safeword 0.8.7 → 0.8.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/{check-PPVIEF3Q.js → check-4HX4SNVV.js} +27 -27
  2. package/dist/check-4HX4SNVV.js.map +1 -0
  3. package/dist/{chunk-W66Z3C5H.js → chunk-FJYRWU2V.js} +5 -5
  4. package/dist/chunk-FJYRWU2V.js.map +1 -0
  5. package/dist/chunk-POPS3ZRQ.js +1219 -0
  6. package/dist/chunk-POPS3ZRQ.js.map +1 -0
  7. package/dist/cli.js +5 -9
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{diff-S3ICSYQY.js → diff-7QIV6Z5B.js} +14 -16
  10. package/dist/diff-7QIV6Z5B.js.map +1 -0
  11. package/dist/index.d.ts +2 -7
  12. package/dist/reset-RP7AGR2Y.js +73 -0
  13. package/dist/reset-RP7AGR2Y.js.map +1 -0
  14. package/dist/setup-ZYRPDTQI.js +91 -0
  15. package/dist/setup-ZYRPDTQI.js.map +1 -0
  16. package/dist/upgrade-K2FFESUH.js +76 -0
  17. package/dist/upgrade-K2FFESUH.js.map +1 -0
  18. package/package.json +2 -2
  19. package/templates/SAFEWORD.md +5 -2
  20. package/templates/commands/cleanup-zombies.md +48 -0
  21. package/templates/guides/cli-reference.md +9 -11
  22. package/templates/guides/zombie-process-cleanup.md +40 -24
  23. package/templates/scripts/cleanup-zombies.sh +222 -0
  24. package/dist/check-PPVIEF3Q.js.map +0 -1
  25. package/dist/chunk-34PU3QZI.js +0 -1047
  26. package/dist/chunk-34PU3QZI.js.map +0 -1
  27. package/dist/chunk-3OK3NQEW.js +0 -476
  28. package/dist/chunk-3OK3NQEW.js.map +0 -1
  29. package/dist/chunk-BFBUEJDH.js +0 -88
  30. package/dist/chunk-BFBUEJDH.js.map +0 -1
  31. package/dist/chunk-W66Z3C5H.js.map +0 -1
  32. package/dist/diff-S3ICSYQY.js.map +0 -1
  33. package/dist/reset-ZST2SGZ2.js +0 -74
  34. package/dist/reset-ZST2SGZ2.js.map +0 -1
  35. package/dist/setup-ANAIEP3D.js +0 -100
  36. package/dist/setup-ANAIEP3D.js.map +0 -1
  37. package/dist/sync-V6D7QTMO.js +0 -9
  38. package/dist/sync-V6D7QTMO.js.map +0 -1
  39. package/dist/upgrade-QFIGWZ5I.js +0 -76
  40. package/dist/upgrade-QFIGWZ5I.js.map +0 -1
@@ -1,1047 +0,0 @@
1
- import {
2
- VERSION
3
- } from "./chunk-ORQHKDT2.js";
4
-
5
- // src/utils/fs.ts
6
- import {
7
- existsSync,
8
- mkdirSync,
9
- readFileSync,
10
- writeFileSync,
11
- rmSync,
12
- rmdirSync,
13
- readdirSync,
14
- chmodSync
15
- } from "fs";
16
- import { join, dirname } from "path";
17
- import { fileURLToPath } from "url";
18
- var __dirname = dirname(fileURLToPath(import.meta.url));
19
- function getTemplatesDir() {
20
- const knownTemplateFile = "SAFEWORD.md";
21
- const candidates = [
22
- join(__dirname, "..", "templates"),
23
- // From dist/ (flat bundled)
24
- join(__dirname, "..", "..", "templates"),
25
- // From src/utils/ or dist/utils/
26
- join(__dirname, "templates")
27
- // Direct sibling (unlikely but safe)
28
- ];
29
- for (const candidate of candidates) {
30
- if (existsSync(join(candidate, knownTemplateFile))) {
31
- return candidate;
32
- }
33
- }
34
- throw new Error("Templates directory not found");
35
- }
36
- function exists(path) {
37
- return existsSync(path);
38
- }
39
- function ensureDir(path) {
40
- if (!existsSync(path)) {
41
- mkdirSync(path, { recursive: true });
42
- }
43
- }
44
- function readFile(path) {
45
- return readFileSync(path, "utf-8");
46
- }
47
- function readFileSafe(path) {
48
- if (!existsSync(path)) return null;
49
- return readFileSync(path, "utf-8");
50
- }
51
- function writeFile(path, content) {
52
- ensureDir(dirname(path));
53
- writeFileSync(path, content);
54
- }
55
- function remove(path) {
56
- if (existsSync(path)) {
57
- rmSync(path, { recursive: true, force: true });
58
- }
59
- }
60
- function removeIfEmpty(path) {
61
- if (!existsSync(path)) return false;
62
- try {
63
- rmdirSync(path);
64
- return true;
65
- } catch {
66
- return false;
67
- }
68
- }
69
- function makeScriptsExecutable(dirPath) {
70
- if (!existsSync(dirPath)) return;
71
- for (const file of readdirSync(dirPath)) {
72
- if (file.endsWith(".sh")) {
73
- chmodSync(join(dirPath, file), 493);
74
- }
75
- }
76
- }
77
- function readJson(path) {
78
- const content = readFileSafe(path);
79
- if (!content) return null;
80
- try {
81
- return JSON.parse(content);
82
- } catch {
83
- return null;
84
- }
85
- }
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;
192
- }
193
-
194
- // 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 */
197
- import { readFileSync } from "fs";
198
- import { dirname, join } from "path";
199
- 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";
208
- import eslintConfigPrettier from "eslint-config-prettier";
209
- ${options.boundaries ? 'import boundariesConfig from "./.safeword/eslint-boundaries.config.mjs";' : ""}
210
-
211
- // 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
- const __dirname = dirname(fileURLToPath(import.meta.url));
214
- const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
215
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
216
-
217
- // Helper for dynamic imports with actionable error messages
218
- async function tryImport(pkgName, frameworkName) {
219
- try {
220
- return await import(pkgName);
221
- } catch (err) {
222
- if (err.code === "ERR_MODULE_NOT_FOUND") {
223
- console.error(\`\\n\u2717 Missing ESLint plugin for \${frameworkName}\\n\`);
224
- console.error(\`Your package.json has \${frameworkName} but the ESLint plugin is not installed.\`);
225
- console.error(\`Run: npm install -D \${pkgName}\\n\`);
226
- console.error(\`Or run: npx safeword sync\\n\`);
227
- }
228
- throw err;
229
- }
230
- }
231
-
232
- // Build dynamic ignores based on detected frameworks
233
- const ignores = ["**/node_modules/", "**/dist/", "**/build/", "**/coverage/"];
234
- if (deps["next"]) ignores.push(".next/");
235
- if (deps["astro"]) ignores.push(".astro/");
236
- if (deps["vue"] || deps["nuxt"]) ignores.push(".nuxt/");
237
- if (deps["svelte"] || deps["@sveltejs/kit"]) ignores.push(".svelte-kit/");
238
-
239
- // Start with base configs (always loaded)
240
- const configs = [
241
- { 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
- },
270
- ];
271
-
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
- });
328
- }
329
-
330
- // Astro support
331
- if (deps["astro"]) {
332
- const astro = await tryImport("eslint-plugin-astro", "Astro");
333
- configs.push(...astro.default.configs.recommended);
334
- }
335
-
336
- // Vue support
337
- if (deps["vue"] || deps["nuxt"]) {
338
- const vue = await tryImport("eslint-plugin-vue", "Vue");
339
- configs.push(...vue.default.configs["flat/recommended"]);
340
- }
341
-
342
- // Svelte support
343
- if (deps["svelte"] || deps["@sveltejs/kit"]) {
344
- const svelte = await tryImport("eslint-plugin-svelte", "Svelte");
345
- configs.push(...svelte.default.configs.recommended);
346
- }
347
-
348
- // Electron support
349
- if (deps["electron"]) {
350
- const electron = await tryImport("@electron-toolkit/eslint-config", "Electron");
351
- configs.push(electron.default);
352
- }
353
-
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
- // eslint-config-prettier must be last to disable conflicting rules
378
- configs.push(eslintConfigPrettier);
379
-
380
- export default defineConfig(configs);
381
- `;
382
- }
383
- var CURSOR_HOOKS = {
384
- afterFileEdit: [{ command: "./.safeword/hooks/cursor/after-file-edit.sh" }],
385
- stop: [{ command: "./.safeword/hooks/cursor/stop.sh" }]
386
- };
387
- var SETTINGS_HOOKS = {
388
- SessionStart: [
389
- {
390
- hooks: [
391
- {
392
- type: "command",
393
- command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/session-verify-agents.sh'
394
- }
395
- ]
396
- },
397
- {
398
- hooks: [
399
- {
400
- type: "command",
401
- command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/session-version.sh'
402
- }
403
- ]
404
- },
405
- {
406
- hooks: [
407
- {
408
- type: "command",
409
- command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/session-lint-check.sh'
410
- }
411
- ]
412
- }
413
- ],
414
- UserPromptSubmit: [
415
- {
416
- hooks: [
417
- {
418
- type: "command",
419
- command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/prompt-timestamp.sh'
420
- }
421
- ]
422
- },
423
- {
424
- hooks: [
425
- {
426
- type: "command",
427
- command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/prompt-questions.sh'
428
- }
429
- ]
430
- }
431
- ],
432
- Stop: [
433
- {
434
- hooks: [
435
- {
436
- type: "command",
437
- command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/stop-quality.sh'
438
- }
439
- ]
440
- }
441
- ],
442
- PostToolUse: [
443
- {
444
- matcher: "Write|Edit|MultiEdit|NotebookEdit",
445
- hooks: [
446
- {
447
- type: "command",
448
- command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/post-tool-lint.sh'
449
- }
450
- ]
451
- }
452
- ]
453
- };
454
-
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
- }
536
- }
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(", ");
551
- }
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)}] }`;
558
- }
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}`;
566
- }
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
- `;
611
- }
612
-
613
- // src/utils/install.ts
614
- var HUSKY_PRE_COMMIT_CONTENT = "npx safeword sync --quiet --stage\nnpx lint-staged\n";
615
- var MCP_SERVERS = {
616
- context7: {
617
- command: "npx",
618
- args: ["-y", "@upstash/context7-mcp@latest"]
619
- },
620
- playwright: {
621
- command: "npx",
622
- args: ["@playwright/mcp@latest"]
623
- }
624
- };
625
-
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
- // src/schema.ts
639
- function isEslintPackage(pkg) {
640
- 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";
641
- }
642
- function getBaseEslintPackages() {
643
- return SAFEWORD_SCHEMA.packages.base.filter((pkg) => isEslintPackage(pkg));
644
- }
645
- function getConditionalEslintPackages(key) {
646
- const deps = SAFEWORD_SCHEMA.packages.conditional[key];
647
- return deps ? deps.filter((pkg) => isEslintPackage(pkg)) : [];
648
- }
649
- var SAFEWORD_SCHEMA = {
650
- version: VERSION,
651
- // Directories fully owned by safeword (created on setup, deleted on reset)
652
- ownedDirs: [
653
- ".safeword",
654
- ".safeword/hooks",
655
- ".safeword/hooks/cursor",
656
- ".safeword/lib",
657
- ".safeword/guides",
658
- ".safeword/templates",
659
- ".safeword/prompts",
660
- ".safeword/planning",
661
- ".safeword/planning/specs",
662
- ".safeword/planning/test-definitions",
663
- ".safeword/planning/design",
664
- ".safeword/planning/issues",
665
- ".safeword/planning/plans",
666
- ".safeword/scripts",
667
- ".husky",
668
- ".cursor",
669
- ".cursor/rules",
670
- ".cursor/commands"
671
- ],
672
- // Directories we add to but don't own (not deleted on reset)
673
- sharedDirs: [".claude", ".claude/skills", ".claude/commands"],
674
- // Created on setup but NOT deleted on reset (preserves user data)
675
- preservedDirs: [
676
- ".safeword/learnings",
677
- ".safeword/tickets",
678
- ".safeword/tickets/completed",
679
- ".safeword/logs"
680
- ],
681
- // Files to delete on upgrade (renamed or removed in newer versions)
682
- deprecatedFiles: [
683
- ".safeword/templates/user-stories-template.md",
684
- // Consolidated into planning-guide.md and testing-guide.md (v0.8.0)
685
- ".safeword/guides/development-workflow.md",
686
- ".safeword/guides/tdd-best-practices.md",
687
- ".safeword/guides/user-story-guide.md",
688
- ".safeword/guides/test-definitions-guide.md"
689
- ],
690
- // Files owned by safeword (overwritten on upgrade if content changed)
691
- ownedFiles: {
692
- // Core files
693
- ".safeword/SAFEWORD.md": { template: "SAFEWORD.md" },
694
- ".safeword/version": { content: () => VERSION },
695
- ".safeword/eslint-boundaries.config.mjs": {
696
- generator: (ctx) => generateBoundariesConfig(detectArchitecture(ctx.cwd))
697
- },
698
- // Hooks (7 files)
699
- ".safeword/hooks/session-verify-agents.sh": { template: "hooks/session-verify-agents.sh" },
700
- ".safeword/hooks/session-version.sh": { template: "hooks/session-version.sh" },
701
- ".safeword/hooks/session-lint-check.sh": { template: "hooks/session-lint-check.sh" },
702
- ".safeword/hooks/prompt-timestamp.sh": { template: "hooks/prompt-timestamp.sh" },
703
- ".safeword/hooks/prompt-questions.sh": { template: "hooks/prompt-questions.sh" },
704
- ".safeword/hooks/post-tool-lint.sh": { template: "hooks/post-tool-lint.sh" },
705
- ".safeword/hooks/stop-quality.sh": { template: "hooks/stop-quality.sh" },
706
- // Lib (2 files)
707
- ".safeword/lib/common.sh": { template: "lib/common.sh" },
708
- ".safeword/lib/jq-fallback.sh": { template: "lib/jq-fallback.sh" },
709
- // Guides (11 files)
710
- ".safeword/guides/architecture-guide.md": { template: "guides/architecture-guide.md" },
711
- ".safeword/guides/cli-reference.md": { template: "guides/cli-reference.md" },
712
- ".safeword/guides/code-philosophy.md": { template: "guides/code-philosophy.md" },
713
- ".safeword/guides/context-files-guide.md": { template: "guides/context-files-guide.md" },
714
- ".safeword/guides/data-architecture-guide.md": {
715
- template: "guides/data-architecture-guide.md"
716
- },
717
- ".safeword/guides/design-doc-guide.md": { template: "guides/design-doc-guide.md" },
718
- ".safeword/guides/learning-extraction.md": { template: "guides/learning-extraction.md" },
719
- ".safeword/guides/llm-guide.md": { template: "guides/llm-guide.md" },
720
- ".safeword/guides/planning-guide.md": { template: "guides/planning-guide.md" },
721
- ".safeword/guides/testing-guide.md": { template: "guides/testing-guide.md" },
722
- ".safeword/guides/zombie-process-cleanup.md": { template: "guides/zombie-process-cleanup.md" },
723
- // Templates (7 files)
724
- ".safeword/templates/architecture-template.md": {
725
- template: "doc-templates/architecture-template.md"
726
- },
727
- ".safeword/templates/design-doc-template.md": {
728
- template: "doc-templates/design-doc-template.md"
729
- },
730
- ".safeword/templates/task-spec-template.md": {
731
- template: "doc-templates/task-spec-template.md"
732
- },
733
- ".safeword/templates/test-definitions-feature.md": {
734
- template: "doc-templates/test-definitions-feature.md"
735
- },
736
- ".safeword/templates/ticket-template.md": { template: "doc-templates/ticket-template.md" },
737
- ".safeword/templates/feature-spec-template.md": {
738
- template: "doc-templates/feature-spec-template.md"
739
- },
740
- ".safeword/templates/work-log-template.md": { template: "doc-templates/work-log-template.md" },
741
- // Prompts (2 files)
742
- ".safeword/prompts/architecture.md": { template: "prompts/architecture.md" },
743
- ".safeword/prompts/quality-review.md": { template: "prompts/quality-review.md" },
744
- // Scripts (3 files)
745
- ".safeword/scripts/bisect-test-pollution.sh": { template: "scripts/bisect-test-pollution.sh" },
746
- ".safeword/scripts/bisect-zombie-processes.sh": {
747
- template: "scripts/bisect-zombie-processes.sh"
748
- },
749
- ".safeword/scripts/lint-md.sh": { template: "scripts/lint-md.sh" },
750
- // Claude skills and commands (9 files)
751
- ".claude/skills/safeword-brainstorming/SKILL.md": {
752
- template: "skills/safeword-brainstorming/SKILL.md"
753
- },
754
- ".claude/skills/safeword-debugging/SKILL.md": {
755
- template: "skills/safeword-debugging/SKILL.md"
756
- },
757
- ".claude/skills/safeword-enforcing-tdd/SKILL.md": {
758
- template: "skills/safeword-enforcing-tdd/SKILL.md"
759
- },
760
- ".claude/skills/safeword-quality-reviewer/SKILL.md": {
761
- template: "skills/safeword-quality-reviewer/SKILL.md"
762
- },
763
- ".claude/skills/safeword-refactoring/SKILL.md": {
764
- template: "skills/safeword-refactoring/SKILL.md"
765
- },
766
- ".claude/skills/safeword-writing-plans/SKILL.md": {
767
- template: "skills/safeword-writing-plans/SKILL.md"
768
- },
769
- ".claude/commands/architecture.md": { template: "commands/architecture.md" },
770
- ".claude/commands/lint.md": { template: "commands/lint.md" },
771
- ".claude/commands/quality-review.md": { template: "commands/quality-review.md" },
772
- // Husky (1 file)
773
- ".husky/pre-commit": { content: HUSKY_PRE_COMMIT_CONTENT },
774
- // Cursor rules (7 files)
775
- ".cursor/rules/safeword-core.mdc": { template: "cursor/rules/safeword-core.mdc" },
776
- ".cursor/rules/safeword-brainstorming.mdc": {
777
- template: "cursor/rules/safeword-brainstorming.mdc"
778
- },
779
- ".cursor/rules/safeword-debugging.mdc": {
780
- template: "cursor/rules/safeword-debugging.mdc"
781
- },
782
- ".cursor/rules/safeword-enforcing-tdd.mdc": {
783
- template: "cursor/rules/safeword-enforcing-tdd.mdc"
784
- },
785
- ".cursor/rules/safeword-quality-reviewer.mdc": {
786
- template: "cursor/rules/safeword-quality-reviewer.mdc"
787
- },
788
- ".cursor/rules/safeword-refactoring.mdc": {
789
- template: "cursor/rules/safeword-refactoring.mdc"
790
- },
791
- ".cursor/rules/safeword-writing-plans.mdc": {
792
- template: "cursor/rules/safeword-writing-plans.mdc"
793
- },
794
- // Cursor commands (3 files - same as Claude)
795
- ".cursor/commands/lint.md": { template: "commands/lint.md" },
796
- ".cursor/commands/quality-review.md": { template: "commands/quality-review.md" },
797
- ".cursor/commands/architecture.md": { template: "commands/architecture.md" },
798
- // Cursor hooks adapters (2 files)
799
- ".safeword/hooks/cursor/after-file-edit.sh": { template: "hooks/cursor/after-file-edit.sh" },
800
- ".safeword/hooks/cursor/stop.sh": { template: "hooks/cursor/stop.sh" }
801
- },
802
- // Files created if missing, updated only if content matches current template
803
- managedFiles: {
804
- "eslint.config.mjs": {
805
- generator: () => getEslintConfig({ boundaries: true })
806
- },
807
- ".prettierrc": { generator: (ctx) => getPrettierConfig(ctx.projectType) },
808
- ".markdownlint-cli2.jsonc": { template: "markdownlint-cli2.jsonc" }
809
- },
810
- // JSON files where we merge specific keys
811
- jsonMerges: {
812
- "package.json": {
813
- keys: [
814
- "scripts.lint",
815
- "scripts.lint:md",
816
- "scripts.format",
817
- "scripts.format:check",
818
- "scripts.knip",
819
- "scripts.prepare",
820
- "lint-staged"
821
- ],
822
- conditionalKeys: {
823
- publishableLibrary: ["scripts.publint"],
824
- shell: ["scripts.lint:sh"]
825
- },
826
- merge: (existing, ctx) => {
827
- const scripts = existing.scripts ?? {};
828
- const result = { ...existing };
829
- if (!scripts.lint) scripts.lint = "eslint .";
830
- if (!scripts["lint:md"]) scripts["lint:md"] = 'markdownlint-cli2 "**/*.md" "#node_modules"';
831
- if (!scripts.format) scripts.format = "prettier --write .";
832
- if (!scripts["format:check"]) scripts["format:check"] = "prettier --check .";
833
- if (!scripts.knip) scripts.knip = "knip";
834
- if (!scripts.prepare) scripts.prepare = "husky || true";
835
- if (ctx.projectType.publishableLibrary && !scripts.publint) {
836
- scripts.publint = "publint";
837
- }
838
- if (ctx.projectType.shell && !scripts["lint:sh"]) {
839
- scripts["lint:sh"] = "shellcheck **/*.sh";
840
- }
841
- result.scripts = scripts;
842
- if (!existing["lint-staged"]) {
843
- result["lint-staged"] = getLintStagedConfig(ctx.projectType);
844
- }
845
- return result;
846
- },
847
- unmerge: (existing) => {
848
- const result = { ...existing };
849
- const scripts = { ...existing.scripts };
850
- delete scripts["lint:md"];
851
- delete scripts["lint:sh"];
852
- delete scripts["format:check"];
853
- delete scripts.knip;
854
- delete scripts.prepare;
855
- delete scripts.publint;
856
- if (Object.keys(scripts).length > 0) {
857
- result.scripts = scripts;
858
- } else {
859
- delete result.scripts;
860
- }
861
- delete result["lint-staged"];
862
- return result;
863
- }
864
- },
865
- ".claude/settings.json": {
866
- keys: ["hooks"],
867
- merge: (existing) => {
868
- const existingHooks = existing.hooks ?? {};
869
- const mergedHooks = { ...existingHooks };
870
- for (const [event, newHooks] of Object.entries(SETTINGS_HOOKS)) {
871
- const eventHooks = mergedHooks[event] ?? [];
872
- const nonSafewordHooks = filterOutSafewordHooks(eventHooks);
873
- mergedHooks[event] = [...nonSafewordHooks, ...newHooks];
874
- }
875
- return { ...existing, hooks: mergedHooks };
876
- },
877
- unmerge: (existing) => {
878
- const existingHooks = existing.hooks ?? {};
879
- const cleanedHooks = {};
880
- for (const [event, eventHooks] of Object.entries(existingHooks)) {
881
- const nonSafewordHooks = filterOutSafewordHooks(eventHooks);
882
- if (nonSafewordHooks.length > 0) {
883
- cleanedHooks[event] = nonSafewordHooks;
884
- }
885
- }
886
- const result = { ...existing };
887
- if (Object.keys(cleanedHooks).length > 0) {
888
- result.hooks = cleanedHooks;
889
- } else {
890
- delete result.hooks;
891
- }
892
- return result;
893
- }
894
- },
895
- ".mcp.json": {
896
- keys: ["mcpServers.context7", "mcpServers.playwright"],
897
- removeFileIfEmpty: true,
898
- merge: (existing) => {
899
- const mcpServers = existing.mcpServers ?? {};
900
- return {
901
- ...existing,
902
- mcpServers: {
903
- ...mcpServers,
904
- context7: MCP_SERVERS.context7,
905
- playwright: MCP_SERVERS.playwright
906
- }
907
- };
908
- },
909
- unmerge: (existing) => {
910
- const result = { ...existing };
911
- const mcpServers = { ...existing.mcpServers };
912
- delete mcpServers.context7;
913
- delete mcpServers.playwright;
914
- if (Object.keys(mcpServers).length > 0) {
915
- result.mcpServers = mcpServers;
916
- } else {
917
- delete result.mcpServers;
918
- }
919
- return result;
920
- }
921
- },
922
- ".cursor/mcp.json": {
923
- keys: ["mcpServers.context7", "mcpServers.playwright"],
924
- removeFileIfEmpty: true,
925
- merge: (existing) => {
926
- const mcpServers = existing.mcpServers ?? {};
927
- return {
928
- ...existing,
929
- mcpServers: {
930
- ...mcpServers,
931
- context7: MCP_SERVERS.context7,
932
- playwright: MCP_SERVERS.playwright
933
- }
934
- };
935
- },
936
- unmerge: (existing) => {
937
- const result = { ...existing };
938
- const mcpServers = { ...existing.mcpServers };
939
- delete mcpServers.context7;
940
- delete mcpServers.playwright;
941
- if (Object.keys(mcpServers).length > 0) {
942
- result.mcpServers = mcpServers;
943
- } else {
944
- delete result.mcpServers;
945
- }
946
- return result;
947
- }
948
- },
949
- ".cursor/hooks.json": {
950
- keys: ["version", "hooks.afterFileEdit", "hooks.stop"],
951
- removeFileIfEmpty: true,
952
- merge: (existing) => {
953
- const hooks = existing.hooks ?? {};
954
- return {
955
- ...existing,
956
- version: 1,
957
- // Required by Cursor
958
- hooks: {
959
- ...hooks,
960
- ...CURSOR_HOOKS
961
- }
962
- };
963
- },
964
- unmerge: (existing) => {
965
- const result = { ...existing };
966
- const hooks = { ...existing.hooks };
967
- delete hooks.afterFileEdit;
968
- delete hooks.stop;
969
- if (Object.keys(hooks).length > 0) {
970
- result.hooks = hooks;
971
- } else {
972
- delete result.hooks;
973
- delete result.version;
974
- }
975
- return result;
976
- }
977
- }
978
- },
979
- // Text files where we patch specific content
980
- textPatches: {
981
- "AGENTS.md": {
982
- operation: "prepend",
983
- content: AGENTS_MD_LINK,
984
- marker: ".safeword/SAFEWORD.md",
985
- createIfMissing: true
986
- },
987
- "CLAUDE.md": {
988
- operation: "prepend",
989
- content: AGENTS_MD_LINK,
990
- marker: ".safeword/SAFEWORD.md",
991
- createIfMissing: false
992
- // Only patch if exists, don't create (AGENTS.md is primary)
993
- }
994
- },
995
- // NPM packages to install
996
- packages: {
997
- base: [
998
- "eslint",
999
- "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
- "eslint-config-prettier",
1009
- "markdownlint-cli2",
1010
- "knip",
1011
- "husky",
1012
- "lint-staged"
1013
- ],
1014
- 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"],
1019
- vue: ["eslint-plugin-vue"],
1020
- svelte: ["eslint-plugin-svelte", "prettier-plugin-svelte"],
1021
- electron: ["@electron-toolkit/eslint-config"],
1022
- vitest: ["@vitest/eslint-plugin"],
1023
- tailwind: ["prettier-plugin-tailwindcss"],
1024
- publishableLibrary: ["publint"],
1025
- shell: ["shellcheck", "prettier-plugin-sh"]
1026
- }
1027
- }
1028
- };
1029
-
1030
- export {
1031
- getTemplatesDir,
1032
- exists,
1033
- ensureDir,
1034
- readFile,
1035
- readFileSafe,
1036
- writeFile,
1037
- remove,
1038
- removeIfEmpty,
1039
- makeScriptsExecutable,
1040
- readJson,
1041
- writeJson,
1042
- detectProjectType,
1043
- getBaseEslintPackages,
1044
- getConditionalEslintPackages,
1045
- SAFEWORD_SCHEMA
1046
- };
1047
- //# sourceMappingURL=chunk-34PU3QZI.js.map