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