safeword 0.5.2 → 0.6.1

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 (43) hide show
  1. package/dist/{check-3NGQ4NR5.js → check-INXMFCL5.js} +7 -5
  2. package/dist/{check-3NGQ4NR5.js.map → check-INXMFCL5.js.map} +1 -1
  3. package/dist/chunk-6CVTH67L.js +43 -0
  4. package/dist/chunk-6CVTH67L.js.map +1 -0
  5. package/dist/chunk-75FKNZUM.js +15 -0
  6. package/dist/chunk-75FKNZUM.js.map +1 -0
  7. package/dist/{chunk-GZRQL3SX.js → chunk-ARIAOK2F.js} +2 -38
  8. package/dist/chunk-ARIAOK2F.js.map +1 -0
  9. package/dist/chunk-FRPJITGG.js +35 -0
  10. package/dist/chunk-FRPJITGG.js.map +1 -0
  11. package/dist/chunk-IWWBZVHT.js +274 -0
  12. package/dist/chunk-IWWBZVHT.js.map +1 -0
  13. package/dist/cli.js +9 -5
  14. package/dist/cli.js.map +1 -1
  15. package/dist/{diff-Y6QTAW4O.js → diff-L7G22MG7.js} +7 -5
  16. package/dist/{diff-Y6QTAW4O.js.map → diff-L7G22MG7.js.map} +1 -1
  17. package/dist/index.d.ts +4 -0
  18. package/dist/{reset-YPSS7BAB.js → reset-5SRM3P6J.js} +19 -17
  19. package/dist/reset-5SRM3P6J.js.map +1 -0
  20. package/dist/setup-65EVU5OT.js +437 -0
  21. package/dist/setup-65EVU5OT.js.map +1 -0
  22. package/dist/sync-4XBMKLXS.js +116 -0
  23. package/dist/sync-4XBMKLXS.js.map +1 -0
  24. package/dist/{upgrade-GQKC2ZKF.js → upgrade-P3WX3ODU.js} +34 -15
  25. package/dist/upgrade-P3WX3ODU.js.map +1 -0
  26. package/package.json +2 -1
  27. package/templates/SAFEWORD.md +4 -4
  28. package/templates/commands/architecture.md +27 -0
  29. package/templates/commands/lint.md +15 -54
  30. package/templates/commands/quality-review.md +16 -13
  31. package/templates/hooks/post-tool-lint.sh +14 -53
  32. package/dist/chunk-GZRQL3SX.js.map +0 -1
  33. package/dist/chunk-Z7MWRHVP.js +0 -266
  34. package/dist/chunk-Z7MWRHVP.js.map +0 -1
  35. package/dist/reset-YPSS7BAB.js.map +0 -1
  36. package/dist/setup-U35LUMIF.js +0 -298
  37. package/dist/setup-U35LUMIF.js.map +0 -1
  38. package/dist/upgrade-GQKC2ZKF.js.map +0 -1
  39. package/templates/commands/arch-review.md +0 -28
  40. package/templates/hooks/git-pre-commit.sh +0 -18
  41. /package/templates/{markdownlint.jsonc → markdownlint-cli2.jsonc} +0 -0
  42. /package/templates/prompts/{arch-review.md → architecture.md} +0 -0
  43. /package/templates/prompts/{quality-review.md → review.md} +0 -0
@@ -0,0 +1,116 @@
1
+ import {
2
+ detectProjectType
3
+ } from "./chunk-6CVTH67L.js";
4
+ import {
5
+ exists,
6
+ readJson
7
+ } from "./chunk-ARIAOK2F.js";
8
+
9
+ // src/commands/sync.ts
10
+ import { join } from "path";
11
+ import { execSync } from "child_process";
12
+ function getRequiredPlugins(projectType) {
13
+ const plugins = [
14
+ // Always required (base plugins)
15
+ "eslint",
16
+ "@eslint/js",
17
+ "eslint-plugin-import-x",
18
+ "eslint-plugin-sonarjs",
19
+ "@microsoft/eslint-plugin-sdl",
20
+ "eslint-config-prettier",
21
+ "eslint-plugin-boundaries",
22
+ "eslint-plugin-playwright"
23
+ ];
24
+ if (projectType.typescript) {
25
+ plugins.push("typescript-eslint");
26
+ }
27
+ if (projectType.react || projectType.nextjs) {
28
+ plugins.push("eslint-plugin-react", "eslint-plugin-react-hooks", "eslint-plugin-jsx-a11y");
29
+ }
30
+ if (projectType.nextjs) {
31
+ plugins.push("@next/eslint-plugin-next");
32
+ }
33
+ if (projectType.astro) {
34
+ plugins.push("eslint-plugin-astro");
35
+ }
36
+ if (projectType.vue) {
37
+ plugins.push("eslint-plugin-vue");
38
+ }
39
+ if (projectType.svelte) {
40
+ plugins.push("eslint-plugin-svelte");
41
+ }
42
+ if (projectType.electron) {
43
+ plugins.push("@electron-toolkit/eslint-config");
44
+ }
45
+ if (projectType.vitest) {
46
+ plugins.push("@vitest/eslint-plugin");
47
+ }
48
+ return plugins;
49
+ }
50
+ function getMissingPackages(required, installed) {
51
+ return required.filter((pkg) => !(pkg in installed));
52
+ }
53
+ async function sync(options = {}) {
54
+ const cwd = process.cwd();
55
+ const safewordDir = join(cwd, ".safeword");
56
+ const packageJsonPath = join(cwd, "package.json");
57
+ if (!exists(safewordDir)) {
58
+ if (!options.quiet) {
59
+ console.error("Not a safeword project. Run `safeword setup` first.");
60
+ }
61
+ process.exit(1);
62
+ }
63
+ if (!exists(packageJsonPath)) {
64
+ if (!options.quiet) {
65
+ console.error("No package.json found.");
66
+ }
67
+ process.exit(1);
68
+ }
69
+ const packageJson = readJson(packageJsonPath);
70
+ if (!packageJson) {
71
+ process.exit(1);
72
+ }
73
+ const projectType = detectProjectType(packageJson);
74
+ const devDeps = packageJson.devDependencies || {};
75
+ const requiredPlugins = getRequiredPlugins(projectType);
76
+ const missingPlugins = getMissingPackages(requiredPlugins, devDeps);
77
+ if (missingPlugins.length === 0) {
78
+ return;
79
+ }
80
+ if (!options.quiet) {
81
+ console.log(`Installing missing ESLint plugins: ${missingPlugins.join(", ")}`);
82
+ }
83
+ try {
84
+ execSync(`npm install -D ${missingPlugins.join(" ")}`, {
85
+ cwd,
86
+ stdio: options.quiet ? "pipe" : "inherit"
87
+ });
88
+ } catch (error) {
89
+ const pluginList = missingPlugins.join(" ");
90
+ console.error(`
91
+ \u2717 Failed to install ESLint plugins
92
+ `);
93
+ console.error(`Your project needs: ${pluginList}`);
94
+ console.error(`
95
+ Run manually when online:`);
96
+ console.error(` npm install -D ${pluginList}
97
+ `);
98
+ process.exit(1);
99
+ }
100
+ if (options.stage) {
101
+ try {
102
+ execSync("git add package.json package-lock.json", {
103
+ cwd,
104
+ stdio: "pipe"
105
+ });
106
+ } catch {
107
+ }
108
+ }
109
+ if (!options.quiet) {
110
+ console.log(`\u2713 Installed ${missingPlugins.length} ESLint plugin(s)`);
111
+ }
112
+ }
113
+ export {
114
+ sync
115
+ };
116
+ //# sourceMappingURL=sync-4XBMKLXS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/sync.ts"],"sourcesContent":["/**\n * Sync command - Keep linting plugins in sync with project dependencies\n *\n * Detects frameworks in package.json and ensures the corresponding ESLint plugins\n * are installed. Designed to be called from Husky pre-commit hook.\n *\n * Behavior:\n * - Fast exit when nothing needs to change\n * - Installs missing plugins\n * - Optionally stages modified files (--stage flag for pre-commit)\n * - Clear error message if installation fails\n */\n\nimport { join } from 'node:path';\nimport { execSync } from 'node:child_process';\nimport { exists, readJson } from '../utils/fs.js';\nimport { detectProjectType, type PackageJson, type ProjectType } from '../utils/project-detector.js';\n\nexport interface SyncOptions {\n quiet?: boolean;\n stage?: boolean;\n}\n\n/**\n * Maps project type flags to their required ESLint plugin packages\n */\nfunction getRequiredPlugins(projectType: ProjectType): string[] {\n const plugins: string[] = [\n // Always required (base plugins)\n 'eslint',\n '@eslint/js',\n 'eslint-plugin-import-x',\n 'eslint-plugin-sonarjs',\n '@microsoft/eslint-plugin-sdl',\n 'eslint-config-prettier',\n 'eslint-plugin-boundaries',\n 'eslint-plugin-playwright',\n ];\n\n // Framework plugins (detected at runtime by dynamic ESLint config)\n if (projectType.typescript) {\n plugins.push('typescript-eslint');\n }\n if (projectType.react || projectType.nextjs) {\n plugins.push('eslint-plugin-react', 'eslint-plugin-react-hooks', 'eslint-plugin-jsx-a11y');\n }\n if (projectType.nextjs) {\n plugins.push('@next/eslint-plugin-next');\n }\n if (projectType.astro) {\n plugins.push('eslint-plugin-astro');\n }\n if (projectType.vue) {\n plugins.push('eslint-plugin-vue');\n }\n if (projectType.svelte) {\n plugins.push('eslint-plugin-svelte');\n }\n if (projectType.electron) {\n plugins.push('@electron-toolkit/eslint-config');\n }\n if (projectType.vitest) {\n plugins.push('@vitest/eslint-plugin');\n }\n\n return plugins;\n}\n\n/**\n * Check which packages are missing from devDependencies\n */\nfunction getMissingPackages(required: string[], installed: Record<string, string>): string[] {\n return required.filter(pkg => !(pkg in installed));\n}\n\n/**\n * Sync linting configuration with current project dependencies\n */\nexport async function sync(options: SyncOptions = {}): Promise<void> {\n const cwd = process.cwd();\n const safewordDir = join(cwd, '.safeword');\n const packageJsonPath = join(cwd, 'package.json');\n\n // Must be in a safeword project\n if (!exists(safewordDir)) {\n if (!options.quiet) {\n console.error('Not a safeword project. Run `safeword setup` first.');\n }\n process.exit(1);\n }\n\n if (!exists(packageJsonPath)) {\n if (!options.quiet) {\n console.error('No package.json found.');\n }\n process.exit(1);\n }\n\n const packageJson = readJson<PackageJson>(packageJsonPath);\n if (!packageJson) {\n process.exit(1);\n }\n\n // Detect current project type\n const projectType = detectProjectType(packageJson);\n const devDeps = packageJson.devDependencies || {};\n\n // Check for missing plugins\n const requiredPlugins = getRequiredPlugins(projectType);\n const missingPlugins = getMissingPackages(requiredPlugins, devDeps);\n\n // Fast exit if nothing to install\n if (missingPlugins.length === 0) {\n return;\n }\n\n // Install missing plugins\n if (!options.quiet) {\n console.log(`Installing missing ESLint plugins: ${missingPlugins.join(', ')}`);\n }\n\n try {\n execSync(`npm install -D ${missingPlugins.join(' ')}`, {\n cwd,\n stdio: options.quiet ? 'pipe' : 'inherit',\n });\n } catch (error) {\n // Clear error message for network/install failures\n const pluginList = missingPlugins.join(' ');\n console.error(`\\n✗ Failed to install ESLint plugins\\n`);\n console.error(`Your project needs: ${pluginList}`);\n console.error(`\\nRun manually when online:`);\n console.error(` npm install -D ${pluginList}\\n`);\n process.exit(1);\n }\n\n // Stage modified files if --stage flag is set (for pre-commit hook)\n if (options.stage) {\n try {\n execSync('git add package.json package-lock.json', {\n cwd,\n stdio: 'pipe',\n });\n } catch {\n // Not in a git repo or git add failed - ignore\n }\n }\n\n if (!options.quiet) {\n console.log(`✓ Installed ${missingPlugins.length} ESLint plugin(s)`);\n }\n}\n"],"mappings":";;;;;;;;;AAaA,SAAS,YAAY;AACrB,SAAS,gBAAgB;AAYzB,SAAS,mBAAmB,aAAoC;AAC9D,QAAM,UAAoB;AAAA;AAAA,IAExB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAGA,MAAI,YAAY,YAAY;AAC1B,YAAQ,KAAK,mBAAmB;AAAA,EAClC;AACA,MAAI,YAAY,SAAS,YAAY,QAAQ;AAC3C,YAAQ,KAAK,uBAAuB,6BAA6B,wBAAwB;AAAA,EAC3F;AACA,MAAI,YAAY,QAAQ;AACtB,YAAQ,KAAK,0BAA0B;AAAA,EACzC;AACA,MAAI,YAAY,OAAO;AACrB,YAAQ,KAAK,qBAAqB;AAAA,EACpC;AACA,MAAI,YAAY,KAAK;AACnB,YAAQ,KAAK,mBAAmB;AAAA,EAClC;AACA,MAAI,YAAY,QAAQ;AACtB,YAAQ,KAAK,sBAAsB;AAAA,EACrC;AACA,MAAI,YAAY,UAAU;AACxB,YAAQ,KAAK,iCAAiC;AAAA,EAChD;AACA,MAAI,YAAY,QAAQ;AACtB,YAAQ,KAAK,uBAAuB;AAAA,EACtC;AAEA,SAAO;AACT;AAKA,SAAS,mBAAmB,UAAoB,WAA6C;AAC3F,SAAO,SAAS,OAAO,SAAO,EAAE,OAAO,UAAU;AACnD;AAKA,eAAsB,KAAK,UAAuB,CAAC,GAAkB;AACnE,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,cAAc,KAAK,KAAK,WAAW;AACzC,QAAM,kBAAkB,KAAK,KAAK,cAAc;AAGhD,MAAI,CAAC,OAAO,WAAW,GAAG;AACxB,QAAI,CAAC,QAAQ,OAAO;AAClB,cAAQ,MAAM,qDAAqD;AAAA,IACrE;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,CAAC,OAAO,eAAe,GAAG;AAC5B,QAAI,CAAC,QAAQ,OAAO;AAClB,cAAQ,MAAM,wBAAwB;AAAA,IACxC;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,cAAc,SAAsB,eAAe;AACzD,MAAI,CAAC,aAAa;AAChB,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,cAAc,kBAAkB,WAAW;AACjD,QAAM,UAAU,YAAY,mBAAmB,CAAC;AAGhD,QAAM,kBAAkB,mBAAmB,WAAW;AACtD,QAAM,iBAAiB,mBAAmB,iBAAiB,OAAO;AAGlE,MAAI,eAAe,WAAW,GAAG;AAC/B;AAAA,EACF;AAGA,MAAI,CAAC,QAAQ,OAAO;AAClB,YAAQ,IAAI,sCAAsC,eAAe,KAAK,IAAI,CAAC,EAAE;AAAA,EAC/E;AAEA,MAAI;AACF,aAAS,kBAAkB,eAAe,KAAK,GAAG,CAAC,IAAI;AAAA,MACrD;AAAA,MACA,OAAO,QAAQ,QAAQ,SAAS;AAAA,IAClC,CAAC;AAAA,EACH,SAAS,OAAO;AAEd,UAAM,aAAa,eAAe,KAAK,GAAG;AAC1C,YAAQ,MAAM;AAAA;AAAA,CAAwC;AACtD,YAAQ,MAAM,uBAAuB,UAAU,EAAE;AACjD,YAAQ,MAAM;AAAA,0BAA6B;AAC3C,YAAQ,MAAM,oBAAoB,UAAU;AAAA,CAAI;AAChD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,MAAI,QAAQ,OAAO;AACjB,QAAI;AACF,eAAS,0CAA0C;AAAA,QACjD;AAAA,QACA,OAAO;AAAA,MACT,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,CAAC,QAAQ,OAAO;AAClB,YAAQ,IAAI,oBAAe,eAAe,MAAM,mBAAmB;AAAA,EACrE;AACF;","names":[]}
@@ -1,3 +1,6 @@
1
+ import {
2
+ isGitRepo
3
+ } from "./chunk-75FKNZUM.js";
1
4
  import {
2
5
  compareVersions
3
6
  } from "./chunk-W66Z3C5H.js";
@@ -7,29 +10,30 @@ import {
7
10
  import {
8
11
  SETTINGS_HOOKS,
9
12
  ensureAgentsMdLink,
10
- filterOutSafewordHooks,
11
- installGitHook,
12
- isGitRepo
13
- } from "./chunk-Z7MWRHVP.js";
13
+ filterOutSafewordHooks
14
+ } from "./chunk-IWWBZVHT.js";
15
+ import {
16
+ error,
17
+ header,
18
+ info,
19
+ listItem,
20
+ success
21
+ } from "./chunk-FRPJITGG.js";
14
22
  import {
15
23
  copyDir,
16
24
  copyFile,
17
25
  ensureDir,
18
- error,
19
26
  exists,
20
27
  getTemplatesDir,
21
- header,
22
- info,
23
- listItem,
24
28
  makeScriptsExecutable,
25
29
  readFileSafe,
26
- success,
27
30
  updateJson,
28
31
  writeFile
29
- } from "./chunk-GZRQL3SX.js";
32
+ } from "./chunk-ARIAOK2F.js";
30
33
 
31
34
  // src/commands/upgrade.ts
32
35
  import { join } from "path";
36
+ import { execSync } from "child_process";
33
37
  async function upgrade() {
34
38
  const cwd = process.cwd();
35
39
  const safewordDir = join(cwd, ".safeword");
@@ -101,10 +105,25 @@ async function upgrade() {
101
105
  updated.push(".claude/commands/");
102
106
  success("Updated skills and commands");
103
107
  if (isGitRepo(cwd)) {
104
- info("\nUpdating git hooks...");
105
- installGitHook(cwd);
106
- updated.push(".git/hooks/pre-commit");
107
- success("Updated git pre-commit hook");
108
+ const huskyDir = join(cwd, ".husky");
109
+ if (exists(huskyDir)) {
110
+ info("\nUpdating Husky pre-commit hook...");
111
+ const huskyPreCommit = join(huskyDir, "pre-commit");
112
+ writeFile(huskyPreCommit, "npx lint-staged\n");
113
+ updated.push(".husky/pre-commit");
114
+ success("Updated Husky pre-commit hook");
115
+ } else {
116
+ info("\nInitializing Husky...");
117
+ try {
118
+ execSync("npx husky init", { cwd, stdio: "pipe" });
119
+ const huskyPreCommit = join(cwd, ".husky", "pre-commit");
120
+ writeFile(huskyPreCommit, "npx lint-staged\n");
121
+ updated.push(".husky/pre-commit");
122
+ success("Initialized Husky with lint-staged");
123
+ } catch {
124
+ info("Husky not initialized (run: npx husky init)");
125
+ }
126
+ }
108
127
  }
109
128
  header("Upgrade Complete");
110
129
  info(`
@@ -131,4 +150,4 @@ Safeword upgraded to v${VERSION}`);
131
150
  export {
132
151
  upgrade
133
152
  };
134
- //# sourceMappingURL=upgrade-GQKC2ZKF.js.map
153
+ //# sourceMappingURL=upgrade-P3WX3ODU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/upgrade.ts"],"sourcesContent":["/**\n * Upgrade command - Update safeword configuration to latest version\n */\n\nimport { join } from 'node:path';\nimport { VERSION } from '../version.js';\nimport {\n exists,\n ensureDir,\n writeFile,\n readFileSafe,\n updateJson,\n copyDir,\n copyFile,\n getTemplatesDir,\n makeScriptsExecutable,\n} from '../utils/fs.js';\nimport { info, success, error, header, listItem } from '../utils/output.js';\nimport { isGitRepo } from '../utils/git.js';\nimport { execSync } from 'node:child_process';\nimport { compareVersions } from '../utils/version.js';\nimport { filterOutSafewordHooks } from '../utils/hooks.js';\nimport { ensureAgentsMdLink } from '../utils/agents-md.js';\nimport { SETTINGS_HOOKS } from '../templates/index.js';\n\nexport async function upgrade(): Promise<void> {\n const cwd = process.cwd();\n const safewordDir = join(cwd, '.safeword');\n\n // Check if configured\n if (!exists(safewordDir)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n // Read project version\n const versionPath = join(safewordDir, 'version');\n const projectVersion = readFileSafe(versionPath)?.trim() ?? '0.0.0';\n\n // Check for downgrade\n if (compareVersions(VERSION, projectVersion) < 0) {\n error(`CLI v${VERSION} is older than project v${projectVersion}.`);\n error('Update the CLI first: npm install -g safeword');\n process.exit(1);\n }\n\n header('Safeword Upgrade');\n info(`Upgrading from v${projectVersion} to v${VERSION}`);\n\n const updated: string[] = [];\n const unchanged: string[] = [];\n\n try {\n const templatesDir = getTemplatesDir();\n\n // 1. Update .safeword directory\n info('\\nUpdating .safeword directory...');\n\n // Update core files from templates\n copyFile(join(templatesDir, 'SAFEWORD.md'), join(safewordDir, 'SAFEWORD.md'));\n writeFile(join(safewordDir, 'version'), VERSION);\n updated.push('.safeword/SAFEWORD.md');\n updated.push('.safeword/version');\n\n // Update guides, templates, prompts from templates\n copyDir(join(templatesDir, 'guides'), join(safewordDir, 'guides'));\n copyDir(join(templatesDir, 'doc-templates'), join(safewordDir, 'templates'));\n copyDir(join(templatesDir, 'prompts'), join(safewordDir, 'prompts'));\n\n // Update lib scripts and make executable\n copyDir(join(templatesDir, 'lib'), join(safewordDir, 'lib'));\n makeScriptsExecutable(join(safewordDir, 'lib'));\n\n // Update hook scripts and make executable\n copyDir(join(templatesDir, 'hooks'), join(safewordDir, 'hooks'));\n makeScriptsExecutable(join(safewordDir, 'hooks'));\n\n updated.push('.safeword/guides/');\n updated.push('.safeword/templates/');\n updated.push('.safeword/prompts/');\n updated.push('.safeword/hooks/');\n success('Updated .safeword directory');\n\n // 2. Verify AGENTS.md link\n info('\\nVerifying AGENTS.md...');\n const agentsMdResult = ensureAgentsMdLink(cwd);\n if (agentsMdResult === 'created') {\n updated.push('AGENTS.md');\n success('Created AGENTS.md');\n } else if (agentsMdResult === 'modified') {\n updated.push('AGENTS.md');\n success('Restored link to AGENTS.md');\n } else {\n unchanged.push('AGENTS.md');\n info('AGENTS.md link is present');\n }\n\n // 3. Update Claude Code hooks\n info('\\nUpdating Claude Code hooks...');\n\n const claudeDir = join(cwd, '.claude');\n const settingsPath = join(claudeDir, 'settings.json');\n\n ensureDir(claudeDir);\n\n updateJson<{ hooks?: Record<string, unknown[]> }>(settingsPath, existing => {\n const hooks = existing?.hooks ?? {};\n\n // Merge hooks, preserving existing non-safeword hooks\n for (const [event, newHooks] of Object.entries(SETTINGS_HOOKS)) {\n const existingHooks = (hooks[event] as unknown[]) ?? [];\n const nonSafewordHooks = filterOutSafewordHooks(existingHooks);\n hooks[event] = [...nonSafewordHooks, ...newHooks];\n }\n\n return { ...existing, hooks };\n });\n\n updated.push('.claude/settings.json');\n success('Updated hooks in .claude/settings.json');\n\n // 4. Update skills and commands\n info('\\nUpdating skills and commands...');\n\n copyDir(join(templatesDir, 'skills'), join(claudeDir, 'skills'));\n copyDir(join(templatesDir, 'commands'), join(claudeDir, 'commands'));\n\n updated.push('.claude/skills/');\n updated.push('.claude/commands/');\n success('Updated skills and commands');\n\n // 5. Update Husky hooks if repo exists\n if (isGitRepo(cwd)) {\n const huskyDir = join(cwd, '.husky');\n if (exists(huskyDir)) {\n info('\\nUpdating Husky pre-commit hook...');\n const huskyPreCommit = join(huskyDir, 'pre-commit');\n writeFile(huskyPreCommit, 'npx lint-staged\\n');\n updated.push('.husky/pre-commit');\n success('Updated Husky pre-commit hook');\n } else {\n // Initialize Husky if not present\n info('\\nInitializing Husky...');\n try {\n execSync('npx husky init', { cwd, stdio: 'pipe' });\n const huskyPreCommit = join(cwd, '.husky', 'pre-commit');\n writeFile(huskyPreCommit, 'npx lint-staged\\n');\n updated.push('.husky/pre-commit');\n success('Initialized Husky with lint-staged');\n } catch {\n info('Husky not initialized (run: npx husky init)');\n }\n }\n }\n\n // Print summary\n header('Upgrade Complete');\n\n info(`\\nVersion: v${projectVersion} → v${VERSION}`);\n\n if (updated.length > 0) {\n info('\\nUpdated:');\n for (const file of updated) {\n listItem(file);\n }\n }\n\n if (unchanged.length > 0) {\n info('\\nUnchanged:');\n for (const file of unchanged) {\n listItem(file);\n }\n }\n\n success(`\\nSafeword upgraded to v${VERSION}`);\n } catch (err) {\n error(`Upgrade failed: ${err instanceof Error ? err.message : 'Unknown error'}`);\n process.exit(1);\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safeword",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "CLI for setting up and managing safeword development environments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,7 @@
23
23
  "build": "tsup",
24
24
  "dev": "tsup --watch",
25
25
  "test": "vitest run",
26
+ "test:e2e": "vitest run tests/e2e/",
26
27
  "test:watch": "vitest",
27
28
  "test:coverage": "vitest run --coverage",
28
29
  "typecheck": "tsc --noEmit",
@@ -77,7 +77,7 @@ bash ./framework/scripts/setup-quality.sh # Quality review only
77
77
  - `.safeword/SAFEWORD.md` - Global patterns and workflows (copied from this file)
78
78
  - `.safeword/guides/` - Reference documentation
79
79
  - `.claude/hooks/` - Hook scripts (with version comments)
80
- - `.claude/commands/` - Slash commands (`/lint`, `/quality-review`)
80
+ - `.claude/commands/` - Slash commands (`/lint`, `/review`, `/architecture`)
81
81
  - `.claude/settings.json` - Hook configuration (appends to existing)
82
82
  - `SAFEWORD.md` or `CLAUDE.md` - Project context file with ALWAYS trigger for @./.safeword/SAFEWORD.md
83
83
  - If CLAUDE.md exists → prepends trigger
@@ -375,11 +375,11 @@ last_modified: 2025-11-24T19:09:00Z
375
375
  - Running semantic architecture review on code changes
376
376
  - Setting up pre-commit hook for architecture enforcement
377
377
  - Setting up CI workflow for PR architecture checks
378
- - Understanding arch-review verdicts (clean/minor/refactor_needed)
378
+ - Understanding architecture review verdicts (clean/minor/refactor_needed)
379
379
  - Checking for: god modules, leaky abstractions, misplaced logic, tight coupling
380
380
 
381
- **Script:** `.safeword/scripts/arch-review.sh`
382
- **Prompt:** `.safeword/prompts/arch-review.md`
381
+ **Command:** `/architecture`
382
+ **Prompt:** `.safeword/prompts/architecture.md`
383
383
  **CI Template:** `.safeword/templates/ci/architecture-check.yml`
384
384
 
385
385
  ---
@@ -0,0 +1,27 @@
1
+ ---
2
+ description: Review code against architecture guidelines and layer boundaries
3
+ ---
4
+
5
+ # Architecture Review
6
+
7
+ Review code changes against project architecture guidelines and dependency boundaries.
8
+
9
+ ## Instructions
10
+
11
+ When the user invokes this command:
12
+
13
+ 1. **Read architecture docs** - Check `.safeword/guides/architecture-guide.md` if it exists
14
+ 2. **Identify layers** - Map the project's architectural layers (types, utils, services, components, etc.)
15
+ 3. **Check boundaries** - Verify imports follow the hierarchy:
16
+ - Lower layers cannot import from higher layers
17
+ - `types` → `utils` → `lib` → `hooks/services` → `components` → `features` → `app`
18
+ 4. **Validate dependencies** - Ensure dependency directions are correct
19
+ 5. **Report violations** - List any boundary violations with specific file:line references
20
+
21
+ ## Example Usage
22
+
23
+ ```
24
+ /architecture
25
+ ```
26
+
27
+ Then: "Review my recent changes" or "Check if the new auth module follows our architecture"
@@ -1,73 +1,34 @@
1
1
  ---
2
- description: Run all linters and formatters on the project
2
+ description: Run linters and formatters to fix code style issues
3
3
  ---
4
4
 
5
5
  # Lint
6
6
 
7
- Run the full linting and formatting suite on the project.
7
+ Run the full linting and formatting suite using the npm scripts configured by safeword.
8
8
 
9
9
  ## Instructions
10
10
 
11
- Run the following commands based on what's available in the project. Check for each tool before running.
12
-
13
- ### JavaScript/TypeScript (if package.json exists)
14
-
15
- ```bash
16
- # Prettier (format all supported files)
17
- npx prettier --write "**/*.{js,jsx,ts,tsx,mjs,cjs,vue,svelte,astro,json,css,scss,html,yaml,yml,graphql,md}" --ignore-path .gitignore 2>/dev/null || true
18
-
19
- # ESLint (lint and fix JS/TS files)
20
- npx eslint --fix "**/*.{js,jsx,ts,tsx,mjs,cjs,vue,svelte,astro}" --ignore-path .gitignore 2>&1 || true
21
- ```
22
-
23
- ### Markdown (if .md files exist)
24
-
25
- ```bash
26
- # markdownlint
27
- npx markdownlint-cli2 --fix "**/*.md" "#node_modules" 2>&1 || true
28
- ```
29
-
30
- ### Python (if .py files exist and ruff is installed)
11
+ Run these npm scripts in order. Each script is configured in package.json by `safeword setup`.
31
12
 
32
13
  ```bash
33
- # Check if ruff is available
34
- if command -v ruff &> /dev/null; then
35
- ruff format .
36
- ruff check --fix .
37
- fi
38
- ```
39
-
40
- ### Go (if .go files exist)
41
-
42
- ```bash
43
- # Check if go is available
44
- if command -v go &> /dev/null; then
45
- go fmt ./...
46
- # Run go vet for static analysis
47
- go vet ./... 2>&1 || true
48
- fi
49
- ```
14
+ # 1. ESLint - fix code quality issues
15
+ npm run lint 2>&1 || true
50
16
 
51
- ### Rust (if Cargo.toml exists)
17
+ # 2. Prettier - format all files
18
+ npm run format 2>&1 || true
52
19
 
53
- ```bash
54
- # Check if cargo is available
55
- if command -v cargo &> /dev/null; then
56
- cargo fmt
57
- # Run clippy for lints
58
- cargo clippy --fix --allow-dirty --allow-staged 2>&1 || true
59
- fi
60
- ```
61
-
62
- ### TypeScript Type Checking (if tsconfig.json exists)
20
+ # 3. Markdownlint - fix markdown issues
21
+ npm run lint:md 2>&1 || true
63
22
 
64
- ```bash
65
- npx tsc --noEmit 2>&1 || true
23
+ # 4. TypeScript type check (if tsconfig.json exists)
24
+ [ -f tsconfig.json ] && npx tsc --noEmit 2>&1 || true
66
25
  ```
67
26
 
68
27
  ## Summary
69
28
 
70
29
  After running, report:
71
- 1. Number of files formatted/fixed
72
- 2. Any remaining errors that couldn't be auto-fixed
30
+ 1. Any ESLint errors that couldn't be auto-fixed
31
+ 2. Any formatting issues
73
32
  3. Type errors (if TypeScript)
33
+
34
+ Note: File patterns are defined in the npm scripts (single source of truth), not here.
@@ -1,27 +1,30 @@
1
1
  ---
2
- description: Deep code quality review with web research against latest docs and versions
2
+ description: Deep code review with web research against latest docs and versions
3
3
  ---
4
4
 
5
5
  # Quality Review
6
6
 
7
- Deep code quality review with web research.
7
+ Perform a deep code review with web research to verify against latest documentation.
8
8
 
9
- ## When to Use
9
+ ## Instructions
10
10
 
11
- - User explicitly requests verification against latest docs
12
- - Need deeper analysis beyond automatic hook
13
- - Working on projects without SAFEWORD.md/CLAUDE.md
11
+ When the user invokes this command:
14
12
 
15
- ## What It Does
13
+ 1. **Identify the scope** - Ask what code to review if not specified
14
+ 2. **Fetch current docs** - Use WebFetch/WebSearch for libraries being used
15
+ 3. **Check versions** - Verify dependencies are current and secure
16
+ 4. **Analyze deeply** - Look for:
17
+ - Performance issues
18
+ - Security vulnerabilities
19
+ - Deprecated APIs
20
+ - Better alternatives
21
+ - Missing error handling
22
+ 5. **Report findings** - Provide actionable recommendations
16
23
 
17
- 1. Fetches current documentation (WebFetch)
18
- 2. Checks latest versions (WebSearch)
19
- 3. Provides deep analysis (performance, security, alternatives)
20
-
21
- ## Usage
24
+ ## Example Usage
22
25
 
23
26
  ```
24
27
  /quality-review
25
28
  ```
26
29
 
27
- Then describe what you want reviewed.
30
+ Then: "Review the authentication implementation" or "Check if my React hooks are following best practices"
@@ -1,6 +1,12 @@
1
1
  #!/bin/bash
2
2
  # Safeword: Auto-lint changed files (PostToolUse)
3
3
  # Silently auto-fixes, only outputs unfixable errors
4
+ #
5
+ # SYNC: Keep file patterns in sync with LINT_STAGED_CONFIG in:
6
+ # packages/cli/src/templates/content.ts
7
+ #
8
+ # This hook is intentionally simple - ESLint's config handles
9
+ # framework-specific rules (React, Vue, Svelte, Astro, etc.)
4
10
 
5
11
  # Require jq for JSON parsing
6
12
  command -v jq &> /dev/null || exit 0
@@ -14,71 +20,26 @@ file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.notebook_path
14
20
  # Change to project directory
15
21
  [ -n "$CLAUDE_PROJECT_DIR" ] && cd "$CLAUDE_PROJECT_DIR"
16
22
 
17
- # Determine linters based on file extension
23
+ # Determine linter based on file extension
18
24
  case "$file" in
19
- *.js|*.jsx|*.ts|*.tsx|*.mjs|*.cjs|*.astro)
20
- # Prettier (silent, ignore errors)
21
- npx prettier --write "$file" 2>/dev/null
22
-
23
- # ESLint --fix (capture unfixable errors)
25
+ # JS/TS and framework files - ESLint first (fix code), then Prettier (format)
26
+ *.js|*.jsx|*.ts|*.tsx|*.mjs|*.mts|*.cjs|*.cts|*.vue|*.svelte|*.astro)
24
27
  errors=$(npx eslint --fix "$file" 2>&1)
25
- exit_code=$?
26
- if [ $exit_code -ne 0 ] && [ -n "$errors" ]; then
27
- echo "$errors"
28
- fi
29
- ;;
30
-
31
- *.vue|*.svelte)
32
- # Vue/Svelte: Prettier + ESLint (requires eslint-plugin-vue or eslint-plugin-svelte)
28
+ [ $? -ne 0 ] && [ -n "$errors" ] && echo "$errors"
33
29
  npx prettier --write "$file" 2>/dev/null
34
-
35
- # ESLint --fix (capture unfixable errors)
36
- errors=$(npx eslint --fix "$file" 2>&1)
37
- exit_code=$?
38
- if [ $exit_code -ne 0 ] && [ -n "$errors" ]; then
39
- echo "$errors"
40
- fi
41
30
  ;;
42
31
 
32
+ # Markdown - markdownlint first, then Prettier
43
33
  *.md)
44
- # Markdownlint (capture unfixable errors)
45
34
  errors=$(npx markdownlint-cli2 --fix "$file" 2>&1)
46
- exit_code=$?
47
- if [ $exit_code -ne 0 ] && [ -n "$errors" ]; then
48
- echo "$errors"
49
- fi
35
+ [ $? -ne 0 ] && [ -n "$errors" ] && echo "$errors"
36
+ npx prettier --write "$file" 2>/dev/null
50
37
  ;;
51
38
 
39
+ # Other supported formats - prettier only
52
40
  *.json|*.css|*.scss|*.html|*.yaml|*.yml|*.graphql)
53
- # Prettier only (silent, ignore errors)
54
41
  npx prettier --write "$file" 2>/dev/null
55
42
  ;;
56
-
57
- *.py)
58
- # Python: ruff format + ruff check --fix
59
- if command -v ruff &> /dev/null; then
60
- ruff format "$file" 2>/dev/null
61
- errors=$(ruff check --fix "$file" 2>&1)
62
- exit_code=$?
63
- if [ $exit_code -ne 0 ] && [ -n "$errors" ]; then
64
- echo "$errors"
65
- fi
66
- fi
67
- ;;
68
-
69
- *.go)
70
- # Go: gofmt
71
- if command -v gofmt &> /dev/null; then
72
- gofmt -w "$file" 2>/dev/null
73
- fi
74
- ;;
75
-
76
- *.rs)
77
- # Rust: rustfmt
78
- if command -v rustfmt &> /dev/null; then
79
- rustfmt "$file" 2>/dev/null
80
- fi
81
- ;;
82
43
  esac
83
44
 
84
45
  exit 0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utils/fs.ts","../src/utils/output.ts"],"sourcesContent":["/**\n * File system utilities for CLI operations\n */\n\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n rmSync,\n readdirSync,\n statSync,\n chmodSync,\n copyFileSync,\n} from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n// Get the directory of this module (for locating templates)\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Get path to bundled templates directory.\n * Works in both development (src/) and production (dist/) contexts.\n */\nexport function getTemplatesDir(): string {\n // When running from dist/, __dirname is packages/cli/dist/\n // Templates are at packages/cli/templates/ (one level up)\n const fromDist = join(__dirname, '..', 'templates');\n\n // Fallback path for edge cases\n const fallback = join(__dirname, '..', '..', 'templates');\n\n if (existsSync(fromDist)) return fromDist;\n if (existsSync(fallback)) return fallback;\n\n throw new Error('Templates directory not found');\n}\n\n/**\n * Check if a path exists\n */\nexport function exists(path: string): boolean {\n return existsSync(path);\n}\n\n/**\n * Check if path is a directory\n */\nexport function isDirectory(path: string): boolean {\n return existsSync(path) && statSync(path).isDirectory();\n}\n\n/**\n * Create directory recursively\n */\nexport function ensureDir(path: string): void {\n if (!existsSync(path)) {\n mkdirSync(path, { recursive: true });\n }\n}\n\n/**\n * Read file as string\n */\nexport function readFile(path: string): string {\n return readFileSync(path, 'utf-8');\n}\n\n/**\n * Read file as string, return null if not exists\n */\nexport function readFileSafe(path: string): string | null {\n if (!existsSync(path)) return null;\n return readFileSync(path, 'utf-8');\n}\n\n/**\n * Write file, creating parent directories if needed\n */\nexport function writeFile(path: string, content: string): void {\n ensureDir(dirname(path));\n writeFileSync(path, content);\n}\n\n/**\n * Remove file or directory recursively\n */\nexport function remove(path: string): void {\n if (existsSync(path)) {\n rmSync(path, { recursive: true, force: true });\n }\n}\n\n/**\n * List files in directory\n */\nexport function listDir(path: string): string[] {\n if (!existsSync(path)) return [];\n return readdirSync(path);\n}\n\n/**\n * Copy a single file\n */\nexport function copyFile(src: string, dest: string): void {\n ensureDir(dirname(dest));\n copyFileSync(src, dest);\n}\n\n/**\n * Copy directory recursively\n */\nexport function copyDir(src: string, dest: string): void {\n ensureDir(dest);\n const entries = readdirSync(src, { withFileTypes: true });\n\n for (const entry of entries) {\n const srcPath = join(src, entry.name);\n const destPath = join(dest, entry.name);\n\n if (entry.isDirectory()) {\n copyDir(srcPath, destPath);\n } else {\n copyFileSync(srcPath, destPath);\n }\n }\n}\n\n/**\n * Make file executable\n */\nexport function makeExecutable(path: string): void {\n chmodSync(path, 0o755);\n}\n\n/**\n * Make all shell scripts in a directory executable\n */\nexport function makeScriptsExecutable(dirPath: string): void {\n if (!existsSync(dirPath)) return;\n for (const file of readdirSync(dirPath)) {\n if (file.endsWith('.sh')) {\n chmodSync(join(dirPath, file), 0o755);\n }\n }\n}\n\n/**\n * Read JSON file\n */\nexport function readJson<T = unknown>(path: string): T | null {\n const content = readFileSafe(path);\n if (!content) return null;\n try {\n return JSON.parse(content) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Write JSON file with formatting\n */\nexport function writeJson(path: string, data: unknown): void {\n writeFile(path, JSON.stringify(data, null, 2) + '\\n');\n}\n\n/**\n * Update JSON file, merging with existing content\n */\nexport function updateJson<T extends Record<string, unknown>>(\n path: string,\n updater: (existing: T | null) => T,\n): void {\n const existing = readJson<T>(path);\n const updated = updater(existing);\n writeJson(path, updated);\n}\n","/**\n * Console output utilities for consistent CLI messaging\n */\n\n/**\n * Print info message\n */\nexport function info(message: string): void {\n console.log(message);\n}\n\n/**\n * Print success message\n */\nexport function success(message: string): void {\n console.log(`✓ ${message}`);\n}\n\n/**\n * Print warning message\n */\nexport function warn(message: string): void {\n console.warn(`⚠ ${message}`);\n}\n\n/**\n * Print error message to stderr\n */\nexport function error(message: string): void {\n console.error(`✗ ${message}`);\n}\n\n/**\n * Print a blank line\n */\nexport function blank(): void {\n console.log('');\n}\n\n/**\n * Print a section header\n */\nexport function header(title: string): void {\n console.log(`\\n${title}`);\n console.log('─'.repeat(title.length));\n}\n\n/**\n * Print a list item\n */\nexport function listItem(item: string, indent = 2): void {\n console.log(`${' '.repeat(indent)}• ${item}`);\n}\n\n/**\n * Print key-value pair\n */\nexport function keyValue(key: string, value: string): void {\n console.log(` ${key}: ${value}`);\n}\n"],"mappings":"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","names":[]}