safeword 0.12.2 → 0.13.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.
Files changed (40) hide show
  1. package/dist/{check-3X75X2JL.js → check-X7NR4WAM.js} +5 -4
  2. package/dist/check-X7NR4WAM.js.map +1 -0
  3. package/dist/{chunk-O4LAXZK3.js → chunk-XLOXGDJG.js} +103 -41
  4. package/dist/chunk-XLOXGDJG.js.map +1 -0
  5. package/dist/cli.js +5 -5
  6. package/dist/{diff-6HFT7BLG.js → diff-3USPMFT2.js} +2 -2
  7. package/dist/{reset-XFXLQXOC.js → reset-CM3BNT5S.js} +2 -2
  8. package/dist/{setup-C6NF3YJ5.js → setup-4PQV6EV2.js} +4 -17
  9. package/dist/setup-4PQV6EV2.js.map +1 -0
  10. package/dist/{upgrade-C2I22FAB.js → upgrade-BIMRJENC.js} +12 -6
  11. package/dist/upgrade-BIMRJENC.js.map +1 -0
  12. package/package.json +2 -2
  13. package/templates/hooks/cursor/after-file-edit.ts +47 -0
  14. package/templates/hooks/cursor/stop.ts +73 -0
  15. package/templates/hooks/lib/lint.ts +49 -0
  16. package/templates/hooks/lib/quality.ts +30 -0
  17. package/templates/hooks/post-tool-lint.ts +33 -0
  18. package/templates/hooks/prompt-questions.ts +32 -0
  19. package/templates/hooks/prompt-timestamp.ts +30 -0
  20. package/templates/hooks/session-lint-check.ts +62 -0
  21. package/templates/hooks/session-verify-agents.ts +32 -0
  22. package/templates/hooks/session-version.ts +18 -0
  23. package/templates/hooks/stop-quality.ts +171 -0
  24. package/dist/check-3X75X2JL.js.map +0 -1
  25. package/dist/chunk-O4LAXZK3.js.map +0 -1
  26. package/dist/setup-C6NF3YJ5.js.map +0 -1
  27. package/dist/upgrade-C2I22FAB.js.map +0 -1
  28. package/templates/hooks/cursor/after-file-edit.sh +0 -58
  29. package/templates/hooks/cursor/stop.sh +0 -50
  30. package/templates/hooks/post-tool-lint.sh +0 -51
  31. package/templates/hooks/prompt-questions.sh +0 -27
  32. package/templates/hooks/prompt-timestamp.sh +0 -13
  33. package/templates/hooks/session-lint-check.sh +0 -42
  34. package/templates/hooks/session-verify-agents.sh +0 -31
  35. package/templates/hooks/session-version.sh +0 -17
  36. package/templates/hooks/stop-quality.sh +0 -91
  37. package/templates/lib/common.sh +0 -26
  38. package/templates/lib/jq-fallback.sh +0 -20
  39. /package/dist/{diff-6HFT7BLG.js.map → diff-3USPMFT2.js.map} +0 -0
  40. /package/dist/{reset-XFXLQXOC.js.map → reset-CM3BNT5S.js.map} +0 -0
@@ -6,9 +6,10 @@ import {
6
6
  import {
7
7
  SAFEWORD_SCHEMA,
8
8
  createProjectContext,
9
+ installDependencies,
9
10
  isGitRepo,
10
11
  reconcile
11
- } from "./chunk-O4LAXZK3.js";
12
+ } from "./chunk-XLOXGDJG.js";
12
13
  import {
13
14
  VERSION
14
15
  } from "./chunk-ORQHKDT2.js";
@@ -24,7 +25,6 @@ import {
24
25
  } from "./chunk-DYLHQBW3.js";
25
26
 
26
27
  // src/commands/setup.ts
27
- import { execSync } from "child_process";
28
28
  import nodePath from "path";
29
29
  function ensurePackageJson(cwd) {
30
30
  const packageJsonPath = nodePath.join(cwd, "package.json");
@@ -34,19 +34,6 @@ function ensurePackageJson(cwd) {
34
34
  writeJson(packageJsonPath, defaultPackageJson);
35
35
  return true;
36
36
  }
37
- function installDependencies(cwd, packages) {
38
- if (packages.length === 0) return;
39
- info("\nInstalling linting dependencies...");
40
- const installCmd = `npm install -D ${packages.join(" ")}`;
41
- info(`Running: ${installCmd}`);
42
- try {
43
- execSync(installCmd, { cwd, stdio: "inherit" });
44
- success("Installed linting dependencies");
45
- } catch {
46
- warn("Failed to install dependencies. Run manually:");
47
- listItem(installCmd);
48
- }
49
- }
50
37
  function printSetupSummary(result, packageJsonCreated, archFiles = []) {
51
38
  header("Setup Complete");
52
39
  const allCreated = [...result.created, ...archFiles];
@@ -98,7 +85,7 @@ async function setup(options) {
98
85
  Architecture detected: ${detected.join("; ")}`);
99
86
  info("Generated dependency-cruiser config for /audit command");
100
87
  }
101
- installDependencies(cwd, result.packagesToInstall);
88
+ installDependencies(cwd, result.packagesToInstall, "linting dependencies");
102
89
  if (!isGitRepo(cwd)) {
103
90
  const isNonInteractive = options.yes || !process.stdin.isTTY;
104
91
  warn(
@@ -116,4 +103,4 @@ Architecture detected: ${detected.join("; ")}`);
116
103
  export {
117
104
  setup
118
105
  };
119
- //# sourceMappingURL=setup-C6NF3YJ5.js.map
106
+ //# sourceMappingURL=setup-4PQV6EV2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/setup.ts"],"sourcesContent":["/**\n * Setup command - Initialize safeword in a project\n *\n * Uses reconcile() with mode='install' to create all managed files.\n */\n\nimport nodePath from 'node:path';\n\nimport { reconcile, type ReconcileResult } from '../reconcile.js';\nimport { SAFEWORD_SCHEMA } from '../schema.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { exists, writeJson } from '../utils/fs.js';\nimport { isGitRepo } from '../utils/git.js';\nimport { installDependencies } from '../utils/install.js';\nimport { error, header, info, listItem, success, warn } from '../utils/output.js';\nimport { VERSION } from '../version.js';\nimport { buildArchitecture, hasArchitectureDetected, syncConfigCore } from './sync-config.js';\n\nexport interface SetupOptions {\n yes?: boolean;\n}\n\ninterface PackageJson {\n name?: string;\n version?: string;\n scripts?: Record<string, string>;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n 'lint-staged'?: Record<string, string[]>;\n}\n\nfunction ensurePackageJson(cwd: string): boolean {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n if (exists(packageJsonPath)) return false;\n\n const dirName = nodePath.basename(cwd) || 'project';\n const defaultPackageJson: PackageJson = { name: dirName, version: '0.1.0', scripts: {} };\n writeJson(packageJsonPath, defaultPackageJson);\n return true;\n}\n\nfunction printSetupSummary(\n result: ReconcileResult,\n packageJsonCreated: boolean,\n archFiles: string[] = [],\n): void {\n header('Setup Complete');\n\n const allCreated = [...result.created, ...archFiles];\n if (allCreated.length > 0 || packageJsonCreated) {\n info('\\nCreated:');\n if (packageJsonCreated) listItem('package.json');\n for (const file of allCreated) listItem(file);\n }\n\n if (result.updated.length > 0) {\n info('\\nModified:');\n for (const file of result.updated) listItem(file);\n }\n\n info('\\nNext steps:');\n listItem('Run `safeword check` to verify setup');\n listItem('Commit the new files to git');\n\n success(`\\nSafeword ${VERSION} installed successfully!`);\n}\n\nexport async function setup(options: SetupOptions): Promise<void> {\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n if (exists(safewordDirectory)) {\n error('Already configured. Run `safeword upgrade` to update.');\n process.exit(1);\n }\n\n const packageJsonCreated = ensurePackageJson(cwd);\n\n header('Safeword Setup');\n info(`Version: ${VERSION}`);\n if (packageJsonCreated) info('Created package.json (none found)');\n\n try {\n info('\\nCreating safeword configuration...');\n const ctx = createProjectContext(cwd);\n const result = await reconcile(SAFEWORD_SCHEMA, 'install', ctx);\n success('Created .safeword directory and configuration');\n\n // Detect architecture and workspaces, generate depcruise configs if found\n const arch = buildArchitecture(cwd);\n const archFiles: string[] = [];\n\n if (hasArchitectureDetected(arch)) {\n const syncResult = syncConfigCore(cwd, arch);\n if (syncResult.generatedConfig) archFiles.push('.safeword/depcruise-config.js');\n if (syncResult.createdMainConfig) archFiles.push('.dependency-cruiser.js');\n\n const detected: string[] = [];\n if (arch.elements.length > 0) {\n detected.push(arch.elements.map(element => element.location).join(', '));\n }\n if (arch.workspaces && arch.workspaces.length > 0) {\n detected.push(`workspaces: ${arch.workspaces.join(', ')}`);\n }\n info(`\\nArchitecture detected: ${detected.join('; ')}`);\n info('Generated dependency-cruiser config for /audit command');\n }\n\n installDependencies(cwd, result.packagesToInstall, 'linting dependencies');\n\n if (!isGitRepo(cwd)) {\n const isNonInteractive = options.yes || !process.stdin.isTTY;\n warn(\n isNonInteractive\n ? 'Skipped Husky setup (no git repository)'\n : 'Skipped Husky setup (no .git directory)',\n );\n if (!isNonInteractive)\n info('Initialize git and run safeword upgrade to enable pre-commit hooks');\n }\n\n printSetupSummary(result, packageJsonCreated, archFiles);\n } catch (error_) {\n error(`Setup failed: ${error_ instanceof Error ? error_.message : 'Unknown error'}`);\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAMA,OAAO,cAAc;AAyBrB,SAAS,kBAAkB,KAAsB;AAC/C,QAAM,kBAAkB,SAAS,KAAK,KAAK,cAAc;AACzD,MAAI,OAAO,eAAe,EAAG,QAAO;AAEpC,QAAM,UAAU,SAAS,SAAS,GAAG,KAAK;AAC1C,QAAM,qBAAkC,EAAE,MAAM,SAAS,SAAS,SAAS,SAAS,CAAC,EAAE;AACvF,YAAU,iBAAiB,kBAAkB;AAC7C,SAAO;AACT;AAEA,SAAS,kBACP,QACA,oBACA,YAAsB,CAAC,GACjB;AACN,SAAO,gBAAgB;AAEvB,QAAM,aAAa,CAAC,GAAG,OAAO,SAAS,GAAG,SAAS;AACnD,MAAI,WAAW,SAAS,KAAK,oBAAoB;AAC/C,SAAK,YAAY;AACjB,QAAI,mBAAoB,UAAS,cAAc;AAC/C,eAAW,QAAQ,WAAY,UAAS,IAAI;AAAA,EAC9C;AAEA,MAAI,OAAO,QAAQ,SAAS,GAAG;AAC7B,SAAK,aAAa;AAClB,eAAW,QAAQ,OAAO,QAAS,UAAS,IAAI;AAAA,EAClD;AAEA,OAAK,eAAe;AACpB,WAAS,sCAAsC;AAC/C,WAAS,6BAA6B;AAEtC,UAAQ;AAAA,WAAc,OAAO,0BAA0B;AACzD;AAEA,eAAsB,MAAM,SAAsC;AAChE,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,oBAAoB,SAAS,KAAK,KAAK,WAAW;AAExD,MAAI,OAAO,iBAAiB,GAAG;AAC7B,UAAM,uDAAuD;AAC7D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,qBAAqB,kBAAkB,GAAG;AAEhD,SAAO,gBAAgB;AACvB,OAAK,YAAY,OAAO,EAAE;AAC1B,MAAI,mBAAoB,MAAK,mCAAmC;AAEhE,MAAI;AACF,SAAK,sCAAsC;AAC3C,UAAM,MAAM,qBAAqB,GAAG;AACpC,UAAM,SAAS,MAAM,UAAU,iBAAiB,WAAW,GAAG;AAC9D,YAAQ,+CAA+C;AAGvD,UAAM,OAAO,kBAAkB,GAAG;AAClC,UAAM,YAAsB,CAAC;AAE7B,QAAI,wBAAwB,IAAI,GAAG;AACjC,YAAM,aAAa,eAAe,KAAK,IAAI;AAC3C,UAAI,WAAW,gBAAiB,WAAU,KAAK,+BAA+B;AAC9E,UAAI,WAAW,kBAAmB,WAAU,KAAK,wBAAwB;AAEzE,YAAM,WAAqB,CAAC;AAC5B,UAAI,KAAK,SAAS,SAAS,GAAG;AAC5B,iBAAS,KAAK,KAAK,SAAS,IAAI,aAAW,QAAQ,QAAQ,EAAE,KAAK,IAAI,CAAC;AAAA,MACzE;AACA,UAAI,KAAK,cAAc,KAAK,WAAW,SAAS,GAAG;AACjD,iBAAS,KAAK,eAAe,KAAK,WAAW,KAAK,IAAI,CAAC,EAAE;AAAA,MAC3D;AACA,WAAK;AAAA,yBAA4B,SAAS,KAAK,IAAI,CAAC,EAAE;AACtD,WAAK,wDAAwD;AAAA,IAC/D;AAEA,wBAAoB,KAAK,OAAO,mBAAmB,sBAAsB;AAEzE,QAAI,CAAC,UAAU,GAAG,GAAG;AACnB,YAAM,mBAAmB,QAAQ,OAAO,CAAC,QAAQ,MAAM;AACvD;AAAA,QACE,mBACI,4CACA;AAAA,MACN;AACA,UAAI,CAAC;AACH,aAAK,oEAAoE;AAAA,IAC7E;AAEA,sBAAkB,QAAQ,oBAAoB,SAAS;AAAA,EACzD,SAAS,QAAQ;AACf,UAAM,iBAAiB,kBAAkB,QAAQ,OAAO,UAAU,eAAe,EAAE;AACnF,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}
@@ -4,8 +4,10 @@ import {
4
4
  import {
5
5
  SAFEWORD_SCHEMA,
6
6
  createProjectContext,
7
+ detectPackageManager,
8
+ installDependencies,
7
9
  reconcile
8
- } from "./chunk-O4LAXZK3.js";
10
+ } from "./chunk-XLOXGDJG.js";
9
11
  import {
10
12
  VERSION
11
13
  } from "./chunk-ORQHKDT2.js";
@@ -26,7 +28,7 @@ function getProjectVersion(safewordDirectory) {
26
28
  const versionPath = nodePath.join(safewordDirectory, "version");
27
29
  return readFileSafe(versionPath)?.trim() ?? "0.0.0";
28
30
  }
29
- function printUpgradeSummary(result, projectVersion) {
31
+ function printUpgradeSummary(result, projectVersion, cwd) {
30
32
  header("Upgrade Complete");
31
33
  info(`
32
34
  Version: v${projectVersion} \u2192 v${VERSION}`);
@@ -39,13 +41,15 @@ Version: v${projectVersion} \u2192 v${VERSION}`);
39
41
  for (const file of result.updated) listItem(file);
40
42
  }
41
43
  if (result.packagesToRemove.length > 0) {
44
+ const pm = detectPackageManager(cwd);
45
+ const uninstallCmd = pm === "yarn" ? "yarn remove" : `${pm} uninstall`;
42
46
  warn(
43
47
  `
44
48
  ${result.packagesToRemove.length} package(s) are now bundled in eslint-plugin-safeword:`
45
49
  );
46
50
  for (const pkg of result.packagesToRemove) listItem(pkg);
47
51
  info("\nIf you don't use these elsewhere, you can remove them:");
48
- listItem(`npm uninstall ${result.packagesToRemove.join(" ")}`);
52
+ listItem(`${uninstallCmd} ${result.packagesToRemove.join(" ")}`);
49
53
  }
50
54
  success(`
51
55
  Safeword upgraded to v${VERSION}`);
@@ -59,8 +63,9 @@ async function upgrade() {
59
63
  }
60
64
  const projectVersion = getProjectVersion(safewordDirectory);
61
65
  if (compareVersions(VERSION, projectVersion) < 0) {
66
+ const pm = detectPackageManager(cwd);
62
67
  error(`CLI v${VERSION} is older than project v${projectVersion}.`);
63
- error("Update the CLI first: npm install -g safeword");
68
+ error(`Update the CLI first: ${pm} install -g safeword`);
64
69
  process.exit(1);
65
70
  }
66
71
  header("Safeword Upgrade");
@@ -68,7 +73,8 @@ async function upgrade() {
68
73
  try {
69
74
  const ctx = createProjectContext(cwd);
70
75
  const result = await reconcile(SAFEWORD_SCHEMA, "upgrade", ctx);
71
- printUpgradeSummary(result, projectVersion);
76
+ installDependencies(cwd, result.packagesToInstall, "missing packages");
77
+ printUpgradeSummary(result, projectVersion, cwd);
72
78
  } catch (error_) {
73
79
  error(`Upgrade failed: ${error_ instanceof Error ? error_.message : "Unknown error"}`);
74
80
  process.exit(1);
@@ -77,4 +83,4 @@ async function upgrade() {
77
83
  export {
78
84
  upgrade
79
85
  };
80
- //# sourceMappingURL=upgrade-C2I22FAB.js.map
86
+ //# sourceMappingURL=upgrade-BIMRJENC.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 * Uses reconcile() with mode='upgrade' to update all managed files.\n */\n\nimport nodePath from 'node:path';\n\nimport { reconcile, type ReconcileResult } from '../reconcile.js';\nimport { SAFEWORD_SCHEMA } from '../schema.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { exists, readFileSafe } from '../utils/fs.js';\nimport { detectPackageManager, installDependencies } from '../utils/install.js';\nimport { error, header, info, listItem, success, warn } from '../utils/output.js';\nimport { compareVersions } from '../utils/version.js';\nimport { VERSION } from '../version.js';\n\nfunction getProjectVersion(safewordDirectory: string): string {\n const versionPath = nodePath.join(safewordDirectory, 'version');\n return readFileSafe(versionPath)?.trim() ?? '0.0.0';\n}\n\nfunction printUpgradeSummary(result: ReconcileResult, projectVersion: string, cwd: string): void {\n header('Upgrade Complete');\n info(`\\nVersion: v${projectVersion} → v${VERSION}`);\n\n if (result.created.length > 0) {\n info('\\nCreated:');\n for (const file of result.created) listItem(file);\n }\n\n if (result.updated.length > 0) {\n info('\\nUpdated:');\n for (const file of result.updated) listItem(file);\n }\n\n if (result.packagesToRemove.length > 0) {\n const pm = detectPackageManager(cwd);\n const uninstallCmd = pm === 'yarn' ? 'yarn remove' : `${pm} uninstall`;\n warn(\n `\\n${result.packagesToRemove.length} package(s) are now bundled in eslint-plugin-safeword:`,\n );\n for (const pkg of result.packagesToRemove) listItem(pkg);\n info(\"\\nIf you don't use these elsewhere, you can remove them:\");\n listItem(`${uninstallCmd} ${result.packagesToRemove.join(' ')}`);\n }\n\n success(`\\nSafeword upgraded to v${VERSION}`);\n}\n\nexport async function upgrade(): Promise<void> {\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n if (!exists(safewordDirectory)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n const projectVersion = getProjectVersion(safewordDirectory);\n\n if (compareVersions(VERSION, projectVersion) < 0) {\n const pm = detectPackageManager(cwd);\n error(`CLI v${VERSION} is older than project v${projectVersion}.`);\n error(`Update the CLI first: ${pm} install -g safeword`);\n process.exit(1);\n }\n\n header('Safeword Upgrade');\n info(`Upgrading from v${projectVersion} to v${VERSION}`);\n\n try {\n const ctx = createProjectContext(cwd);\n const result = await reconcile(SAFEWORD_SCHEMA, 'upgrade', ctx);\n installDependencies(cwd, result.packagesToInstall, 'missing packages');\n printUpgradeSummary(result, projectVersion, cwd);\n } catch (error_) {\n error(`Upgrade failed: ${error_ instanceof Error ? error_.message : 'Unknown error'}`);\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAMA,OAAO,cAAc;AAWrB,SAAS,kBAAkB,mBAAmC;AAC5D,QAAM,cAAc,SAAS,KAAK,mBAAmB,SAAS;AAC9D,SAAO,aAAa,WAAW,GAAG,KAAK,KAAK;AAC9C;AAEA,SAAS,oBAAoB,QAAyB,gBAAwB,KAAmB;AAC/F,SAAO,kBAAkB;AACzB,OAAK;AAAA,YAAe,cAAc,YAAO,OAAO,EAAE;AAElD,MAAI,OAAO,QAAQ,SAAS,GAAG;AAC7B,SAAK,YAAY;AACjB,eAAW,QAAQ,OAAO,QAAS,UAAS,IAAI;AAAA,EAClD;AAEA,MAAI,OAAO,QAAQ,SAAS,GAAG;AAC7B,SAAK,YAAY;AACjB,eAAW,QAAQ,OAAO,QAAS,UAAS,IAAI;AAAA,EAClD;AAEA,MAAI,OAAO,iBAAiB,SAAS,GAAG;AACtC,UAAM,KAAK,qBAAqB,GAAG;AACnC,UAAM,eAAe,OAAO,SAAS,gBAAgB,GAAG,EAAE;AAC1D;AAAA,MACE;AAAA,EAAK,OAAO,iBAAiB,MAAM;AAAA,IACrC;AACA,eAAW,OAAO,OAAO,iBAAkB,UAAS,GAAG;AACvD,SAAK,0DAA0D;AAC/D,aAAS,GAAG,YAAY,IAAI,OAAO,iBAAiB,KAAK,GAAG,CAAC,EAAE;AAAA,EACjE;AAEA,UAAQ;AAAA,wBAA2B,OAAO,EAAE;AAC9C;AAEA,eAAsB,UAAyB;AAC7C,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,oBAAoB,SAAS,KAAK,KAAK,WAAW;AAExD,MAAI,CAAC,OAAO,iBAAiB,GAAG;AAC9B,UAAM,6CAA6C;AACnD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,iBAAiB,kBAAkB,iBAAiB;AAE1D,MAAI,gBAAgB,SAAS,cAAc,IAAI,GAAG;AAChD,UAAM,KAAK,qBAAqB,GAAG;AACnC,UAAM,QAAQ,OAAO,2BAA2B,cAAc,GAAG;AACjE,UAAM,yBAAyB,EAAE,sBAAsB;AACvD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,SAAO,kBAAkB;AACzB,OAAK,mBAAmB,cAAc,QAAQ,OAAO,EAAE;AAEvD,MAAI;AACF,UAAM,MAAM,qBAAqB,GAAG;AACpC,UAAM,SAAS,MAAM,UAAU,iBAAiB,WAAW,GAAG;AAC9D,wBAAoB,KAAK,OAAO,mBAAmB,kBAAkB;AACrE,wBAAoB,QAAQ,gBAAgB,GAAG;AAAA,EACjD,SAAS,QAAQ;AACf,UAAM,mBAAmB,kBAAkB,QAAQ,OAAO,UAAU,eAAe,EAAE;AACrF,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safeword",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
4
4
  "description": "CLI for setting up and managing safeword development environments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "commander": "^12.1.0",
36
- "eslint-plugin-safeword": "^0.5.1"
36
+ "eslint-plugin-safeword": "^0.5.3"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^20.10.0",
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Cursor adapter for afterFileEdit
3
+ // Auto-lints changed files, sets marker for stop hook
4
+
5
+ import { existsSync } from 'node:fs';
6
+
7
+ import { lintFile } from '../lib/lint.ts';
8
+
9
+ interface CursorInput {
10
+ workspace_roots?: string[];
11
+ file_path?: string;
12
+ conversation_id?: string;
13
+ }
14
+
15
+ // Read hook input from stdin
16
+ let input: CursorInput;
17
+ try {
18
+ input = await Bun.stdin.json();
19
+ } catch (error) {
20
+ if (process.env.DEBUG) console.error('[cursor/after-file-edit] stdin parse error:', error);
21
+ process.exit(0);
22
+ }
23
+
24
+ const workspace = input.workspace_roots?.[0];
25
+ const file = input.file_path;
26
+ const convId = input.conversation_id ?? 'default';
27
+
28
+ // Exit silently if no file or file doesn't exist
29
+ if (!file || !(await Bun.file(file).exists())) {
30
+ process.exit(0);
31
+ }
32
+
33
+ // Change to workspace directory
34
+ if (workspace) {
35
+ process.chdir(workspace);
36
+ }
37
+
38
+ // Check for .safeword directory
39
+ if (!existsSync('.safeword')) {
40
+ process.exit(0);
41
+ }
42
+
43
+ // Set marker file for stop hook to know edits were made
44
+ await Bun.write(`/tmp/safeword-cursor-edited-${convId}`, '');
45
+
46
+ // Lint the file
47
+ await lintFile(file, process.cwd());
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Cursor adapter for stop hook
3
+ // Checks for marker file from afterFileEdit to determine if files were modified
4
+ // Uses followup_message to inject quality review prompt into conversation
5
+
6
+ import { existsSync } from 'node:fs';
7
+ import { unlink } from 'node:fs/promises';
8
+
9
+ import { QUALITY_REVIEW_MESSAGE } from '../lib/quality.ts';
10
+
11
+ interface CursorInput {
12
+ workspace_roots?: string[];
13
+ conversation_id?: string;
14
+ status?: string;
15
+ loop_count?: number;
16
+ }
17
+
18
+ interface StopOutput {
19
+ followup_message?: string;
20
+ }
21
+
22
+ // Read hook input from stdin
23
+ let input: CursorInput;
24
+ try {
25
+ input = await Bun.stdin.json();
26
+ } catch (error) {
27
+ if (process.env.DEBUG) console.error('[cursor/stop] stdin parse error:', error);
28
+ console.log('{}');
29
+ process.exit(0);
30
+ }
31
+
32
+ const workspace = input.workspace_roots?.[0];
33
+
34
+ // Change to workspace directory
35
+ if (workspace) {
36
+ process.chdir(workspace);
37
+ }
38
+
39
+ // Check for .safeword directory
40
+ if (!existsSync('.safeword')) {
41
+ console.log('{}');
42
+ process.exit(0);
43
+ }
44
+
45
+ // Check status - only proceed on completed (not aborted/error)
46
+ if (input.status !== 'completed') {
47
+ console.log('{}');
48
+ process.exit(0);
49
+ }
50
+
51
+ // Get loop_count to prevent infinite review loops
52
+ // When review is triggered, agent runs again with loop_count >= 1
53
+ const loopCount = input.loop_count ?? 0;
54
+ if (loopCount >= 1) {
55
+ console.log('{}');
56
+ process.exit(0);
57
+ }
58
+
59
+ // Check if any file edits occurred in this session by looking for marker file
60
+ const convId = input.conversation_id ?? 'default';
61
+ const markerFile = `/tmp/safeword-cursor-edited-${convId}`;
62
+
63
+ if (await Bun.file(markerFile).exists()) {
64
+ // Clean up marker
65
+ await unlink(markerFile).catch(() => {});
66
+
67
+ const output: StopOutput = {
68
+ followup_message: QUALITY_REVIEW_MESSAGE,
69
+ };
70
+ console.log(JSON.stringify(output));
71
+ } else {
72
+ console.log('{}');
73
+ }
@@ -0,0 +1,49 @@
1
+ // Shared linting logic for Claude Code and Cursor hooks
2
+ // Used by: post-tool-lint.ts, cursor/after-file-edit.ts
3
+
4
+ import { existsSync } from 'node:fs';
5
+
6
+ import { $ } from 'bun';
7
+
8
+ // File extensions for different linting strategies
9
+ const JS_EXTENSIONS = new Set(['js', 'jsx', 'ts', 'tsx', 'mjs', 'mts', 'cjs', 'cts', 'vue', 'svelte', 'astro']);
10
+ const PRETTIER_EXTENSIONS = new Set(['md', 'json', 'css', 'scss', 'html', 'yaml', 'yml', 'graphql']);
11
+
12
+ /**
13
+ * Lint a file based on its extension.
14
+ * Runs ESLint + Prettier for JS/TS, Prettier only for other formats,
15
+ * and shellcheck + Prettier for shell scripts.
16
+ *
17
+ * @param file - Path to the file to lint
18
+ * @param projectDir - Project root directory (for finding prettier-plugin-sh)
19
+ */
20
+ export async function lintFile(file: string, projectDir: string): Promise<void> {
21
+ const extension = file.split('.').pop()?.toLowerCase() ?? '';
22
+
23
+ // JS/TS and framework files - ESLint first (fix code), then Prettier (format)
24
+ if (JS_EXTENSIONS.has(extension)) {
25
+ const eslintResult = await $`npx eslint --fix ${file}`.nothrow().quiet();
26
+ if (eslintResult.exitCode !== 0 && eslintResult.stderr.length > 0) {
27
+ console.log(eslintResult.stderr.toString());
28
+ }
29
+ await $`npx prettier --write ${file}`.nothrow().quiet();
30
+ return;
31
+ }
32
+
33
+ // Other supported formats - prettier only
34
+ if (PRETTIER_EXTENSIONS.has(extension)) {
35
+ await $`npx prettier --write ${file}`.nothrow().quiet();
36
+ return;
37
+ }
38
+
39
+ // Shell scripts - shellcheck (if available), then Prettier (if plugin installed)
40
+ if (extension === 'sh') {
41
+ const shellcheckResult = await $`npx shellcheck ${file}`.nothrow().quiet();
42
+ if (shellcheckResult.exitCode !== 0 && shellcheckResult.stderr.length > 0) {
43
+ console.log(shellcheckResult.stderr.toString());
44
+ }
45
+ if (existsSync(`${projectDir}/node_modules/prettier-plugin-sh`)) {
46
+ await $`npx prettier --write ${file}`.nothrow().quiet();
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,30 @@
1
+ // Shared quality review message for Claude Code and Cursor hooks
2
+ // Used by: stop-quality.ts, cursor/stop.ts
3
+
4
+ /**
5
+ * The quality review prompt shown when changes are made.
6
+ * Used by both Claude Code Stop hook and Cursor stop hook.
7
+ */
8
+ export const QUALITY_REVIEW_MESSAGE = `SAFEWORD Quality Review:
9
+
10
+ Double check and critique your work again just in case.
11
+ Assume you've never seen it before.
12
+
13
+ - Is it correct?
14
+ - Is it elegant?
15
+ - Does it follow latest docs/best practices?
16
+ - Ask me any non-obvious questions.
17
+ - Avoid bloat.
18
+ - If you asked a question above that's still relevant after review, re-ask it.`;
19
+
20
+ export const QUESTION_RESEARCH_MESSAGE = `SAFEWORD Research Prompt:
21
+
22
+ Before asking this question, do your research and investigate.
23
+ Explore and debate the options.
24
+
25
+ - What's most correct?
26
+ - What's most elegant?
27
+ - What's most in line with latest docs and best practices?
28
+ - Think hard and avoid bloat.
29
+
30
+ Then re-ask your question with the context you've gathered.`;
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Auto-lint changed files (PostToolUse)
3
+ // Silently auto-fixes, only outputs unfixable errors
4
+
5
+ import { lintFile } from './lib/lint.ts';
6
+
7
+ interface HookInput {
8
+ tool_input?: {
9
+ file_path?: string;
10
+ notebook_path?: string;
11
+ };
12
+ }
13
+
14
+ // Read hook input from stdin
15
+ let input: HookInput;
16
+ try {
17
+ input = await Bun.stdin.json();
18
+ } catch (error) {
19
+ if (process.env.DEBUG) console.error('[post-tool-lint] stdin parse error:', error);
20
+ process.exit(0);
21
+ }
22
+
23
+ const file = input.tool_input?.file_path ?? input.tool_input?.notebook_path;
24
+
25
+ // Exit silently if no file or file doesn't exist
26
+ if (!file || !(await Bun.file(file).exists())) {
27
+ process.exit(0);
28
+ }
29
+
30
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
31
+ process.chdir(projectDir);
32
+
33
+ await lintFile(file, projectDir);
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Question protocol guidance (UserPromptSubmit)
3
+ // Reminds Claude to ask 1-5 clarifying questions for ambiguous tasks
4
+
5
+ import { existsSync } from 'node:fs';
6
+
7
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
8
+ const safewordDir = `${projectDir}/.safeword`;
9
+
10
+ // Not a safeword project, skip silently
11
+ if (!existsSync(safewordDir)) {
12
+ process.exit(0);
13
+ }
14
+
15
+ // Read the user prompt from stdin
16
+ let input: string;
17
+ try {
18
+ input = await Bun.stdin.text();
19
+ } catch (error) {
20
+ if (process.env.DEBUG) console.error('[prompt-questions] stdin read error:', error);
21
+ process.exit(0);
22
+ }
23
+
24
+ // Only trigger on substantial prompts (more than 20 chars)
25
+ if (input.length < 20) {
26
+ process.exit(0);
27
+ }
28
+
29
+ console.log(`SAFEWORD Question Protocol: For ambiguous or complex requests, ask 1-5 clarifying questions before proceeding. Focus on:
30
+ - Scope boundaries (what's included/excluded)
31
+ - Technical constraints (frameworks, patterns, compatibility)
32
+ - Success criteria (how will we know it's done)`);
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Inject timestamp (UserPromptSubmit)
3
+ // Outputs current timestamp for Claude's context awareness
4
+ // Helps with accurate ticket timestamps and time-based reasoning
5
+
6
+ const now = new Date();
7
+
8
+ // Natural language day/time in UTC
9
+ const natural = now.toLocaleDateString('en-US', {
10
+ weekday: 'long',
11
+ year: 'numeric',
12
+ month: 'long',
13
+ day: 'numeric',
14
+ hour: '2-digit',
15
+ minute: '2-digit',
16
+ timeZone: 'UTC',
17
+ timeZoneName: 'short',
18
+ });
19
+
20
+ // ISO 8601 UTC
21
+ const iso = now.toISOString();
22
+
23
+ // Local timezone
24
+ const local = now.toLocaleTimeString('en-US', {
25
+ hour: '2-digit',
26
+ minute: '2-digit',
27
+ timeZoneName: 'short',
28
+ });
29
+
30
+ console.log(`Current time: ${natural} (${iso}) | Local: ${local}`);
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Lint configuration sync check (SessionStart)
3
+ // Warns if ESLint or Prettier configs are missing or out of sync
4
+
5
+ import { existsSync } from 'node:fs';
6
+
7
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
8
+ const safewordDir = `${projectDir}/.safeword`;
9
+
10
+ // Not a safeword project, skip silently
11
+ if (!existsSync(safewordDir)) {
12
+ process.exit(0);
13
+ }
14
+
15
+ const warnings: string[] = [];
16
+
17
+ // Check for ESLint config
18
+ const eslintConfigs = [
19
+ 'eslint.config.mjs',
20
+ 'eslint.config.js',
21
+ '.eslintrc.json',
22
+ '.eslintrc.js',
23
+ ];
24
+ const hasEslint = await Promise.all(
25
+ eslintConfigs.map(f => Bun.file(`${projectDir}/${f}`).exists()),
26
+ );
27
+ if (!hasEslint.some(Boolean)) {
28
+ warnings.push("ESLint config not found - run 'npm run lint' may fail");
29
+ }
30
+
31
+ // Check for Prettier config
32
+ const prettierConfigs = ['.prettierrc', '.prettierrc.json', 'prettier.config.js'];
33
+ const hasPrettier = await Promise.all(
34
+ prettierConfigs.map(f => Bun.file(`${projectDir}/${f}`).exists()),
35
+ );
36
+ if (!hasPrettier.some(Boolean)) {
37
+ warnings.push('Prettier config not found - formatting may be inconsistent');
38
+ }
39
+
40
+ // Check for required dependencies in package.json
41
+ const pkgJsonFile = Bun.file(`${projectDir}/package.json`);
42
+ if (await pkgJsonFile.exists()) {
43
+ try {
44
+ const pkgJson = await pkgJsonFile.text();
45
+ if (!pkgJson.includes('"eslint"')) {
46
+ warnings.push("ESLint not in package.json - run 'npm install -D eslint'");
47
+ }
48
+ if (!pkgJson.includes('"prettier"')) {
49
+ warnings.push("Prettier not in package.json - run 'npm install -D prettier'");
50
+ }
51
+ } catch (error) {
52
+ if (process.env.DEBUG) console.error('[session-lint-check] package.json parse error:', error);
53
+ }
54
+ }
55
+
56
+ // Output warnings if any
57
+ if (warnings.length > 0) {
58
+ console.log('SAFEWORD Lint Check:');
59
+ for (const warning of warnings) {
60
+ console.log(` ⚠️ ${warning}`);
61
+ }
62
+ }
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Verify AGENTS.md link (SessionStart)
3
+ // Self-heals by restoring the link if removed
4
+
5
+ import { existsSync } from 'node:fs';
6
+
7
+ const LINK = '**⚠️ ALWAYS READ FIRST:** `.safeword/SAFEWORD.md`';
8
+
9
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
10
+ const safewordDir = `${projectDir}/.safeword`;
11
+
12
+ // Not a safeword project, skip silently
13
+ if (!existsSync(safewordDir)) {
14
+ process.exit(0);
15
+ }
16
+
17
+ const agentsFile = Bun.file(`${projectDir}/AGENTS.md`);
18
+
19
+ if (!(await agentsFile.exists())) {
20
+ // AGENTS.md doesn't exist, create it
21
+ await Bun.write(agentsFile, `${LINK}\n`);
22
+ console.log('SAFEWORD: Created AGENTS.md with safeword link');
23
+ process.exit(0);
24
+ }
25
+
26
+ // Check if link is present
27
+ const content = await agentsFile.text();
28
+ if (!content.includes('.safeword/SAFEWORD.md')) {
29
+ // Link missing, prepend it
30
+ await Bun.write(agentsFile, `${LINK}\n\n${content}`);
31
+ console.log('SAFEWORD: Restored AGENTS.md link (was removed)');
32
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bun
2
+ // Safeword: Display version on session start (SessionStart)
3
+ // Shows current safeword version and confirms hooks are active
4
+
5
+ import { existsSync } from 'node:fs';
6
+
7
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
8
+ const safewordDir = `${projectDir}/.safeword`;
9
+
10
+ // Not a safeword project, skip silently
11
+ if (!existsSync(safewordDir)) {
12
+ process.exit(0);
13
+ }
14
+
15
+ const versionFile = Bun.file(`${safewordDir}/version`);
16
+ const version = (await versionFile.exists()) ? (await versionFile.text()).trim() : 'unknown';
17
+
18
+ console.log(`SAFE WORD Claude Config v${version} installed - auto-linting and quality review active`);