safeword 0.12.1 → 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.
- package/dist/{check-3X75X2JL.js → check-X7NR4WAM.js} +5 -4
- package/dist/check-X7NR4WAM.js.map +1 -0
- package/dist/{chunk-O4LAXZK3.js → chunk-XLOXGDJG.js} +103 -41
- package/dist/chunk-XLOXGDJG.js.map +1 -0
- package/dist/cli.js +5 -5
- package/dist/{diff-6HFT7BLG.js → diff-3USPMFT2.js} +2 -2
- package/dist/{reset-XFXLQXOC.js → reset-CM3BNT5S.js} +2 -2
- package/dist/{setup-C6NF3YJ5.js → setup-4PQV6EV2.js} +4 -17
- package/dist/setup-4PQV6EV2.js.map +1 -0
- package/dist/{upgrade-C2I22FAB.js → upgrade-BIMRJENC.js} +12 -6
- package/dist/upgrade-BIMRJENC.js.map +1 -0
- package/package.json +3 -3
- package/templates/hooks/cursor/after-file-edit.ts +47 -0
- package/templates/hooks/cursor/stop.ts +73 -0
- package/templates/hooks/lib/lint.ts +49 -0
- package/templates/hooks/lib/quality.ts +30 -0
- package/templates/hooks/post-tool-lint.ts +33 -0
- package/templates/hooks/prompt-questions.ts +32 -0
- package/templates/hooks/prompt-timestamp.ts +30 -0
- package/templates/hooks/session-lint-check.ts +62 -0
- package/templates/hooks/session-verify-agents.ts +32 -0
- package/templates/hooks/session-version.ts +18 -0
- package/templates/hooks/stop-quality.ts +171 -0
- package/dist/check-3X75X2JL.js.map +0 -1
- package/dist/chunk-O4LAXZK3.js.map +0 -1
- package/dist/setup-C6NF3YJ5.js.map +0 -1
- package/dist/upgrade-C2I22FAB.js.map +0 -1
- package/templates/hooks/cursor/after-file-edit.sh +0 -58
- package/templates/hooks/cursor/stop.sh +0 -50
- package/templates/hooks/post-tool-lint.sh +0 -51
- package/templates/hooks/prompt-questions.sh +0 -27
- package/templates/hooks/prompt-timestamp.sh +0 -13
- package/templates/hooks/session-lint-check.sh +0 -42
- package/templates/hooks/session-verify-agents.sh +0 -31
- package/templates/hooks/session-version.sh +0 -17
- package/templates/hooks/stop-quality.sh +0 -91
- package/templates/lib/common.sh +0 -26
- package/templates/lib/jq-fallback.sh +0 -20
- /package/dist/{diff-6HFT7BLG.js.map → diff-3USPMFT2.js.map} +0 -0
- /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-
|
|
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-
|
|
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-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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-
|
|
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.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "CLI for setting up and managing safeword development environments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
30
|
"lint": "eslint src tests",
|
|
31
31
|
"clean": "rm -rf dist",
|
|
32
|
-
"prepublishOnly": "bun run build && bun run test"
|
|
32
|
+
"prepublishOnly": "node scripts/check-bun-publish.js && bun run build && bun run test"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"commander": "^12.1.0",
|
|
36
|
-
"eslint-plugin-safeword": "
|
|
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`);
|