@soleri/cli 9.4.0 → 9.6.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/commands/create.js +3 -6
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/hooks.js +126 -0
- package/dist/commands/hooks.js.map +1 -1
- package/dist/commands/install.js +5 -0
- package/dist/commands/install.js.map +1 -1
- package/dist/hook-packs/converter/README.md +99 -0
- package/dist/hook-packs/converter/template.d.ts +36 -0
- package/dist/hook-packs/converter/template.js +127 -0
- package/dist/hook-packs/converter/template.js.map +1 -0
- package/dist/hook-packs/converter/template.test.ts +133 -0
- package/dist/hook-packs/converter/template.ts +163 -0
- package/dist/hook-packs/flock-guard/README.md +65 -0
- package/dist/hook-packs/flock-guard/manifest.json +36 -0
- package/dist/hook-packs/flock-guard/scripts/flock-guard-post.sh +48 -0
- package/dist/hook-packs/flock-guard/scripts/flock-guard-pre.sh +85 -0
- package/dist/hook-packs/full/manifest.json +8 -1
- package/dist/hook-packs/graduation.d.ts +11 -0
- package/dist/hook-packs/graduation.js +48 -0
- package/dist/hook-packs/graduation.js.map +1 -0
- package/dist/hook-packs/graduation.ts +65 -0
- package/dist/hook-packs/installer.js +3 -1
- package/dist/hook-packs/installer.js.map +1 -1
- package/dist/hook-packs/installer.ts +3 -1
- package/dist/hook-packs/marketing-research/README.md +37 -0
- package/dist/hook-packs/marketing-research/manifest.json +24 -0
- package/dist/hook-packs/marketing-research/scripts/marketing-research.sh +70 -0
- package/dist/hook-packs/registry.d.ts +1 -0
- package/dist/hook-packs/registry.js +14 -4
- package/dist/hook-packs/registry.js.map +1 -1
- package/dist/hook-packs/registry.ts +18 -4
- package/dist/hook-packs/safety/README.md +50 -0
- package/dist/hook-packs/safety/manifest.json +23 -0
- package/{src/hook-packs/yolo-safety → dist/hook-packs/safety}/scripts/anti-deletion.sh +7 -1
- package/dist/hook-packs/validator.d.ts +32 -0
- package/dist/hook-packs/validator.js +126 -0
- package/dist/hook-packs/validator.js.map +1 -0
- package/dist/hook-packs/validator.ts +158 -0
- package/dist/hook-packs/yolo-safety/manifest.json +3 -19
- package/dist/prompts/create-wizard.js +1 -1
- package/dist/prompts/create-wizard.js.map +1 -1
- package/dist/utils/checks.js +6 -1
- package/dist/utils/checks.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/flock-guard.test.ts +232 -0
- package/src/__tests__/graduation.test.ts +199 -0
- package/src/__tests__/hook-packs.test.ts +44 -19
- package/src/__tests__/hooks-convert.test.ts +344 -0
- package/src/__tests__/validator.test.ts +265 -0
- package/src/commands/create.ts +3 -7
- package/src/commands/hooks.ts +172 -0
- package/src/commands/install.ts +6 -0
- package/src/hook-packs/converter/README.md +99 -0
- package/src/hook-packs/converter/template.ts +163 -0
- package/src/hook-packs/flock-guard/README.md +65 -0
- package/src/hook-packs/flock-guard/manifest.json +36 -0
- package/src/hook-packs/flock-guard/scripts/flock-guard-post.sh +48 -0
- package/src/hook-packs/flock-guard/scripts/flock-guard-pre.sh +85 -0
- package/src/hook-packs/full/manifest.json +8 -1
- package/src/hook-packs/graduation.ts +65 -0
- package/src/hook-packs/installer.ts +3 -1
- package/src/hook-packs/marketing-research/README.md +37 -0
- package/src/hook-packs/marketing-research/manifest.json +24 -0
- package/src/hook-packs/marketing-research/scripts/marketing-research.sh +70 -0
- package/src/hook-packs/registry.ts +18 -4
- package/src/hook-packs/safety/README.md +50 -0
- package/src/hook-packs/safety/manifest.json +23 -0
- package/src/hook-packs/safety/scripts/anti-deletion.sh +280 -0
- package/src/hook-packs/validator.ts +158 -0
- package/src/hook-packs/yolo-safety/manifest.json +3 -19
- package/src/prompts/create-wizard.ts +1 -1
- package/src/utils/checks.ts +6 -1
- package/src/prompts/playbook.ts +0 -487
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook pack validation framework.
|
|
3
|
+
* Generates test fixtures, runs dry-run tests, reports false positives/negatives.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import type { HookEvent } from './converter/template.js';
|
|
7
|
+
|
|
8
|
+
export interface TestFixture {
|
|
9
|
+
name: string;
|
|
10
|
+
event: HookEvent;
|
|
11
|
+
payload: Record<string, unknown>;
|
|
12
|
+
shouldMatch: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DryRunResult {
|
|
16
|
+
fixture: TestFixture;
|
|
17
|
+
exitCode: number;
|
|
18
|
+
stdout: string;
|
|
19
|
+
matched: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ValidationReport {
|
|
23
|
+
total: number;
|
|
24
|
+
passed: number;
|
|
25
|
+
falsePositives: DryRunResult[];
|
|
26
|
+
falseNegatives: DryRunResult[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generate test fixtures for a hook event.
|
|
31
|
+
* Returns 5 matching + 10 non-matching payloads.
|
|
32
|
+
*/
|
|
33
|
+
export function generateFixtures(
|
|
34
|
+
event: HookEvent,
|
|
35
|
+
toolMatcher?: string,
|
|
36
|
+
filePatterns?: string[],
|
|
37
|
+
): TestFixture[] {
|
|
38
|
+
const fixtures: TestFixture[] = [];
|
|
39
|
+
|
|
40
|
+
if (event === 'PreToolUse' || event === 'PostToolUse') {
|
|
41
|
+
const matchTools = toolMatcher ? toolMatcher.split('|').map((t) => t.trim()) : ['Write'];
|
|
42
|
+
const matchPath = filePatterns?.[0] ?? '**/src/**';
|
|
43
|
+
// Convert glob to a sample path
|
|
44
|
+
const samplePath = matchPath
|
|
45
|
+
.replace('**/', 'src/')
|
|
46
|
+
.replace('**', 'components')
|
|
47
|
+
.replace('*', 'file.tsx');
|
|
48
|
+
|
|
49
|
+
// 5 matching fixtures
|
|
50
|
+
for (let i = 0; i < 5; i++) {
|
|
51
|
+
const tool = matchTools[i % matchTools.length];
|
|
52
|
+
fixtures.push({
|
|
53
|
+
name: `match-${tool}-${i}`,
|
|
54
|
+
event,
|
|
55
|
+
payload: {
|
|
56
|
+
tool_name: tool,
|
|
57
|
+
tool_input: {
|
|
58
|
+
file_path: `${samplePath.replace('file.tsx', `file-${i}.tsx`)}`,
|
|
59
|
+
command: `echo test-${i}`,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
shouldMatch: true,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 10 non-matching fixtures
|
|
67
|
+
const nonMatchTools = [
|
|
68
|
+
'Bash',
|
|
69
|
+
'Read',
|
|
70
|
+
'Glob',
|
|
71
|
+
'Grep',
|
|
72
|
+
'Agent',
|
|
73
|
+
'WebSearch',
|
|
74
|
+
'WebFetch',
|
|
75
|
+
'TaskCreate',
|
|
76
|
+
'Skill',
|
|
77
|
+
'ToolSearch',
|
|
78
|
+
];
|
|
79
|
+
for (let i = 0; i < 10; i++) {
|
|
80
|
+
fixtures.push({
|
|
81
|
+
name: `no-match-${nonMatchTools[i]}-${i}`,
|
|
82
|
+
event,
|
|
83
|
+
payload: {
|
|
84
|
+
tool_name: nonMatchTools[i],
|
|
85
|
+
tool_input: {
|
|
86
|
+
file_path: `/unrelated/path/other-${i}.js`,
|
|
87
|
+
command: `ls -la`,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
shouldMatch: false,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// PreCompact, Notification, Stop — simpler payloads
|
|
95
|
+
// 5 matching (any invocation matches these events)
|
|
96
|
+
for (let i = 0; i < 5; i++) {
|
|
97
|
+
fixtures.push({
|
|
98
|
+
name: `match-event-${i}`,
|
|
99
|
+
event,
|
|
100
|
+
payload: { session_id: `test-session-${i}`, context: `test context ${i}` },
|
|
101
|
+
shouldMatch: true,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// 10 non-matching (empty/malformed payloads)
|
|
105
|
+
for (let i = 0; i < 10; i++) {
|
|
106
|
+
fixtures.push({
|
|
107
|
+
name: `no-match-empty-${i}`,
|
|
108
|
+
event,
|
|
109
|
+
payload: {},
|
|
110
|
+
shouldMatch: false,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return fixtures;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Run a hook script against a single fixture in dry-run mode.
|
|
120
|
+
*/
|
|
121
|
+
export function runSingleDryRun(scriptPath: string, fixture: TestFixture): DryRunResult {
|
|
122
|
+
const input = JSON.stringify(fixture.payload);
|
|
123
|
+
try {
|
|
124
|
+
const stdout = execSync(`printf '%s' '${input.replace(/'/g, "'\\''")}' | sh "${scriptPath}"`, {
|
|
125
|
+
encoding: 'utf-8',
|
|
126
|
+
timeout: 5000,
|
|
127
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
128
|
+
});
|
|
129
|
+
const matched = stdout.trim().length > 0 && stdout.includes('"continue"');
|
|
130
|
+
return { fixture, exitCode: 0, stdout: stdout.trim(), matched };
|
|
131
|
+
} catch (err: unknown) {
|
|
132
|
+
const error = err as { status?: number; stdout?: string };
|
|
133
|
+
return {
|
|
134
|
+
fixture,
|
|
135
|
+
exitCode: error.status ?? 1,
|
|
136
|
+
stdout: (error.stdout as string) ?? '',
|
|
137
|
+
matched: false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run all fixtures against a script and produce a validation report.
|
|
144
|
+
*/
|
|
145
|
+
export function validateHookScript(scriptPath: string, fixtures: TestFixture[]): ValidationReport {
|
|
146
|
+
const results = fixtures.map((f) => runSingleDryRun(scriptPath, f));
|
|
147
|
+
|
|
148
|
+
const falsePositives = results.filter((r) => !r.fixture.shouldMatch && r.matched);
|
|
149
|
+
const falseNegatives = results.filter((r) => r.fixture.shouldMatch && !r.matched);
|
|
150
|
+
const passed = results.length - falsePositives.length - falseNegatives.length;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
total: results.length,
|
|
154
|
+
passed,
|
|
155
|
+
falsePositives,
|
|
156
|
+
falseNegatives,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yolo-safety",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Safety guardrails for YOLO mode —
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Safety guardrails for YOLO mode — composes the safety pack (anti-deletion, staging) with YOLO-specific defaults",
|
|
5
5
|
"hooks": [],
|
|
6
|
-
"
|
|
7
|
-
{
|
|
8
|
-
"name": "anti-deletion",
|
|
9
|
-
"file": "anti-deletion.sh",
|
|
10
|
-
"targetDir": "hooks"
|
|
11
|
-
}
|
|
12
|
-
],
|
|
13
|
-
"lifecycleHooks": [
|
|
14
|
-
{
|
|
15
|
-
"event": "PreToolUse",
|
|
16
|
-
"matcher": "Bash",
|
|
17
|
-
"type": "command",
|
|
18
|
-
"command": "sh ~/.claude/hooks/anti-deletion.sh",
|
|
19
|
-
"timeout": 10,
|
|
20
|
-
"statusMessage": "Checking for destructive commands..."
|
|
21
|
-
}
|
|
22
|
-
]
|
|
6
|
+
"composedFrom": ["safety"]
|
|
23
7
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* from usage, not configured upfront.
|
|
7
7
|
*/
|
|
8
8
|
import * as p from '@clack/prompts';
|
|
9
|
-
import { ITALIAN_CRAFTSPERSON } from '@soleri/core';
|
|
9
|
+
import { ITALIAN_CRAFTSPERSON } from '@soleri/core/personas';
|
|
10
10
|
/** Slugify a display name into a kebab-case ID. */
|
|
11
11
|
function slugify(name) {
|
|
12
12
|
return name
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-wizard.js","sourceRoot":"","sources":["../../src/prompts/create-wizard.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,CAAC,MAAM,gBAAgB,CAAC;AAEpC,OAAO,EAAE,oBAAoB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"create-wizard.js","sourceRoot":"","sources":["../../src/prompts/create-wizard.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/checks.js
CHANGED
|
@@ -16,7 +16,11 @@ export function checkNodeVersion() {
|
|
|
16
16
|
}
|
|
17
17
|
export function checkNpm() {
|
|
18
18
|
try {
|
|
19
|
-
|
|
19
|
+
// shell: true is needed on Windows where npm is installed as npm.cmd
|
|
20
|
+
const version = execFileSync('npm', ['--version'], {
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
shell: process.platform === 'win32',
|
|
23
|
+
}).trim();
|
|
20
24
|
return { status: 'pass', label: 'npm', detail: `v${version}` };
|
|
21
25
|
}
|
|
22
26
|
catch {
|
|
@@ -28,6 +32,7 @@ function checkTsx() {
|
|
|
28
32
|
const version = execFileSync('npx', ['tsx', '--version'], {
|
|
29
33
|
encoding: 'utf-8',
|
|
30
34
|
timeout: 10_000,
|
|
35
|
+
shell: process.platform === 'win32',
|
|
31
36
|
}).trim();
|
|
32
37
|
return { status: 'pass', label: 'tsx', detail: `v${version}` };
|
|
33
38
|
}
|
package/dist/utils/checks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checks.js","sourceRoot":"","sources":["../../src/utils/checks.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAQ9D,MAAM,UAAU,gBAAgB;IAC9B,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7D,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;QAChB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;IACnF,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,kBAAkB,EAAE,CAAC;AACnG,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,WAAW,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"checks.js","sourceRoot":"","sources":["../../src/utils/checks.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAQ9D,MAAM,UAAU,gBAAgB;IAC9B,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7D,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;QAChB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;IACnF,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,kBAAkB,EAAE,CAAC;AACnG,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,IAAI,CAAC;QACH,qEAAqE;QACrE,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,WAAW,CAAC,EAAE;YACjD,QAAQ,EAAE,OAAO;YACjB,KAAK,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;SACpC,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,OAAO,EAAE,EAAE,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAC/D,CAAC;AACH,CAAC;AAED,SAAS,QAAQ;IACf,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,EAAE;YACxD,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,MAAM;YACf,KAAK,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;SACpC,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,OAAO,EAAE,EAAE,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,mCAAmC,EAAE,CAAC;IACvF,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAY;IAC5C,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,mCAAmC,EAAE,CAAC;IACjG,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,KAAK,GAAG,CAAC,WAAW,GAAG,EAAE,CAAC;AACnG,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAEvF,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,qCAAqC,EAAE,CAAC;IACjG,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC;QACzD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,KAAK,EAAE,aAAa;YACpB,MAAM,EAAE,6CAA6C;SACtD,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;AAClF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAY;IAC3C,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAExF,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC;QACrD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,KAAK,EAAE,cAAc;YACrB,MAAM,EAAE,2CAA2C;SACpD,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;AACnF,CAAC;AAED,SAAS,oBAAoB,CAAC,GAAY;IACxC,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAE5F,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,cAAc,CAAC,CAAC;IACvD,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QAChC,OAAO;YACL,MAAM,EAAE,MAAM;YACd,KAAK,EAAE,kBAAkB;YACzB,MAAM,EAAE,0BAA0B;SACnC,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC;QACxC,IAAI,GAAG,CAAC,OAAO,IAAI,OAAO,EAAE,CAAC;YAC3B,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,KAAK,EAAE,kBAAkB;gBACzB,MAAM,EAAE,kBAAkB,GAAG,CAAC,OAAO,GAAG;aACzC,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,KAAK,EAAE,kBAAkB;YACzB,MAAM,EAAE,IAAI,GAAG,CAAC,OAAO,+BAA+B;SACvD,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,EAAE,MAAM,EAAE,gCAAgC,EAAE,CAAC;IACjG,CAAC;AACH,CAAC;AAED,SAAS,WAAW;IAClB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,wBAAwB,CAAC;IAC/D,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,uBAAuB,GAAG,EAAE,EAAE,CAAC;IACnF,CAAC;IACD,IAAI,CAAC;QACH,YAAY,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5F,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,gBAAgB,IAAI,EAAE,EAAE,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,MAAM,EAAE,MAAM;YACd,KAAK,EAAE,QAAQ;YACf,MAAM,EAAE,kBAAkB,IAAI,8CAA8C;SAC7E,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,cAAc;IACrB,MAAM,SAAS,GAAG,iBAAiB,EAAE,CAAC;IACtC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO;YACL,MAAM,EAAE,MAAM;YACd,KAAK,EAAE,YAAY;YACnB,MAAM,EAAE,8CAA8C;SACvD,CAAC;IACJ,CAAC;IACD,OAAO;QACL,MAAM,EAAE,MAAM;QACd,KAAK,EAAE,YAAY;QACnB,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;KAC7B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,OAAO;QACL,gBAAgB,EAAE;QAClB,QAAQ,EAAE;QACV,QAAQ,EAAE;QACV,iBAAiB,CAAC,GAAG,CAAC;QACtB,gBAAgB,CAAC,GAAG,CAAC;QACrB,eAAe,CAAC,GAAG,CAAC;QACpB,oBAAoB,CAAC,GAAG,CAAC;QACzB,cAAc,EAAE;QAChB,WAAW,EAAE;KACd,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
|
|
8
|
+
const SCRIPTS_DIR = join(__dirname, '..', 'hook-packs', 'flock-guard', 'scripts');
|
|
9
|
+
// Normalize to forward slashes for use in shell commands on Windows (Git Bash)
|
|
10
|
+
const PRE_SCRIPT = join(SCRIPTS_DIR, 'flock-guard-pre.sh').replace(/\\/g, '/');
|
|
11
|
+
const POST_SCRIPT = join(SCRIPTS_DIR, 'flock-guard-post.sh').replace(/\\/g, '/');
|
|
12
|
+
|
|
13
|
+
// The scripts use `git rev-parse --show-toplevel` which resolves to the repo root.
|
|
14
|
+
// Compute the same hash the scripts will produce.
|
|
15
|
+
const PROJECT_ROOT = execSync('git rev-parse --show-toplevel', {
|
|
16
|
+
cwd: join(__dirname, '..'),
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
}).trim();
|
|
19
|
+
const PROJECT_HASH = execSync(`printf '%s' '${PROJECT_ROOT}' | shasum | cut -c1-8`, {
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
}).trim();
|
|
22
|
+
// Scripts use ${TMPDIR:-${TEMP:-/tmp}} — match that resolution for the test environment.
|
|
23
|
+
// Normalize backslashes to forward slashes so Node.js paths match POSIX shell paths on Windows.
|
|
24
|
+
const TMP_BASE = (process.env.TMPDIR || process.env.TEMP || tmpdir()).replace(/\\/g, '/');
|
|
25
|
+
const LOCK_DIR = `${TMP_BASE}/soleri-guard-${PROJECT_HASH}.lock`;
|
|
26
|
+
|
|
27
|
+
function makePayload(command: string): string {
|
|
28
|
+
return JSON.stringify({ tool_name: 'Bash', tool_input: { command } });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function runPre(
|
|
32
|
+
command: string,
|
|
33
|
+
env?: Record<string, string>,
|
|
34
|
+
): { stdout: string; exitCode: number } {
|
|
35
|
+
try {
|
|
36
|
+
const stdout = execSync(
|
|
37
|
+
`printf '%s' '${escapeShell(makePayload(command))}' | sh '${PRE_SCRIPT}'`,
|
|
38
|
+
{
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
stdio: 'pipe',
|
|
41
|
+
cwd: PROJECT_ROOT,
|
|
42
|
+
env: { ...process.env, TMPDIR: TMP_BASE, ...env },
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
return { stdout, exitCode: 0 };
|
|
46
|
+
} catch (err: any) {
|
|
47
|
+
return { stdout: err.stdout ?? '', exitCode: err.status ?? 1 };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runPost(
|
|
52
|
+
command: string,
|
|
53
|
+
env?: Record<string, string>,
|
|
54
|
+
): { stdout: string; exitCode: number } {
|
|
55
|
+
try {
|
|
56
|
+
const stdout = execSync(
|
|
57
|
+
`printf '%s' '${escapeShell(makePayload(command))}' | sh '${POST_SCRIPT}'`,
|
|
58
|
+
{
|
|
59
|
+
encoding: 'utf-8',
|
|
60
|
+
stdio: 'pipe',
|
|
61
|
+
cwd: PROJECT_ROOT,
|
|
62
|
+
env: { ...process.env, TMPDIR: TMP_BASE, ...env },
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
return { stdout, exitCode: 0 };
|
|
66
|
+
} catch (err: any) {
|
|
67
|
+
return { stdout: err.stdout ?? '', exitCode: err.status ?? 1 };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function escapeShell(s: string): string {
|
|
72
|
+
// Escape single quotes for use inside single-quoted shell string
|
|
73
|
+
return s.replace(/'/g, "'\\''");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function cleanLock(): void {
|
|
77
|
+
if (existsSync(LOCK_DIR)) {
|
|
78
|
+
rmSync(LOCK_DIR, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// flock-guard uses POSIX shell scripts (sh, mkdir, jq, shasum, grep -qE) that
|
|
83
|
+
// are incompatible with Windows even under Git Bash due to path separator issues.
|
|
84
|
+
const isWindows = process.platform === 'win32';
|
|
85
|
+
|
|
86
|
+
describe.skipIf(isWindows)('flock-guard hook pack', () => {
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
cleanLock();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// 1. Pre: allows non-lockfile commands
|
|
92
|
+
it('pre: allows non-lockfile commands (exit 0, no output)', () => {
|
|
93
|
+
const { stdout, exitCode } = runPre('echo hello');
|
|
94
|
+
expect(exitCode).toBe(0);
|
|
95
|
+
expect(stdout.trim()).toBe('');
|
|
96
|
+
expect(existsSync(LOCK_DIR)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// 2. Pre: acquires lock on npm install
|
|
100
|
+
it('pre: acquires lock on npm install', () => {
|
|
101
|
+
const sessionId = `test-acquire-${Date.now()}`;
|
|
102
|
+
const { exitCode } = runPre('npm install', { CLAUDE_SESSION_ID: sessionId });
|
|
103
|
+
expect(exitCode).toBe(0);
|
|
104
|
+
expect(existsSync(LOCK_DIR)).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// 3. Pre: lock dir contains valid JSON with agentId and timestamp
|
|
108
|
+
it('pre: lock dir contains valid JSON with agentId and timestamp', () => {
|
|
109
|
+
const sessionId = `test-json-${Date.now()}`;
|
|
110
|
+
runPre('npm install', { CLAUDE_SESSION_ID: sessionId });
|
|
111
|
+
|
|
112
|
+
const lockJson = JSON.parse(readFileSync(join(LOCK_DIR, 'lock.json'), 'utf-8'));
|
|
113
|
+
expect(lockJson).toHaveProperty('agentId', sessionId);
|
|
114
|
+
expect(lockJson).toHaveProperty('timestamp');
|
|
115
|
+
expect(typeof lockJson.timestamp).toBe('number');
|
|
116
|
+
expect(lockJson.timestamp).toBeGreaterThan(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 4. Post: releases lock after npm install
|
|
120
|
+
it('post: releases lock after npm install', () => {
|
|
121
|
+
const sessionId = `test-release-${Date.now()}`;
|
|
122
|
+
runPre('npm install', { CLAUDE_SESSION_ID: sessionId });
|
|
123
|
+
expect(existsSync(LOCK_DIR)).toBe(true);
|
|
124
|
+
|
|
125
|
+
const { exitCode } = runPost('npm install', { CLAUDE_SESSION_ID: sessionId });
|
|
126
|
+
expect(exitCode).toBe(0);
|
|
127
|
+
expect(existsSync(LOCK_DIR)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// 5. Pre: blocks when lock held by another agent
|
|
131
|
+
it('pre: blocks when lock held by another agent', () => {
|
|
132
|
+
// Manually create lock with a different agentId
|
|
133
|
+
mkdirSync(LOCK_DIR, { recursive: true });
|
|
134
|
+
const now = Math.floor(Date.now() / 1000);
|
|
135
|
+
writeFileSync(
|
|
136
|
+
join(LOCK_DIR, 'lock.json'),
|
|
137
|
+
JSON.stringify({ agentId: 'other-agent-999', timestamp: now, command: 'npm install' }),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const mySession = `test-blocked-${Date.now()}`;
|
|
141
|
+
const { stdout, exitCode } = runPre('npm install', { CLAUDE_SESSION_ID: mySession });
|
|
142
|
+
|
|
143
|
+
// Script exits 0 but outputs JSON with continue: false
|
|
144
|
+
expect(exitCode).toBe(0);
|
|
145
|
+
const output = JSON.parse(stdout.trim());
|
|
146
|
+
expect(output.continue).toBe(false);
|
|
147
|
+
expect(output.stopReason).toContain('BLOCKED');
|
|
148
|
+
expect(output.stopReason).toContain('other-agent-999');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// 6. Pre: cleans stale lock (timestamp older than 30s)
|
|
152
|
+
it('pre: cleans stale lock and acquires', () => {
|
|
153
|
+
mkdirSync(LOCK_DIR, { recursive: true });
|
|
154
|
+
const staleTime = Math.floor(Date.now() / 1000) - 60; // 60s ago
|
|
155
|
+
writeFileSync(
|
|
156
|
+
join(LOCK_DIR, 'lock.json'),
|
|
157
|
+
JSON.stringify({ agentId: 'stale-agent', timestamp: staleTime, command: 'npm install' }),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const mySession = `test-stale-${Date.now()}`;
|
|
161
|
+
const { stdout, exitCode } = runPre('npm install', { CLAUDE_SESSION_ID: mySession });
|
|
162
|
+
expect(exitCode).toBe(0);
|
|
163
|
+
// Should not contain "continue: false" — lock was stale and cleaned
|
|
164
|
+
if (stdout.trim()) {
|
|
165
|
+
const output = JSON.parse(stdout.trim());
|
|
166
|
+
expect(output.continue).not.toBe(false);
|
|
167
|
+
}
|
|
168
|
+
// Lock should now be held by our session
|
|
169
|
+
expect(existsSync(LOCK_DIR)).toBe(true);
|
|
170
|
+
const lockJson = JSON.parse(readFileSync(join(LOCK_DIR, 'lock.json'), 'utf-8'));
|
|
171
|
+
expect(lockJson.agentId).toBe(mySession);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// 7. Pre: allows same agent reentry
|
|
175
|
+
it('pre: allows same agent reentry', () => {
|
|
176
|
+
const sessionId = `test-reentry-${Date.now()}`;
|
|
177
|
+
const env = { CLAUDE_SESSION_ID: sessionId };
|
|
178
|
+
|
|
179
|
+
// First acquisition
|
|
180
|
+
const first = runPre('npm install', env);
|
|
181
|
+
expect(first.exitCode).toBe(0);
|
|
182
|
+
expect(existsSync(LOCK_DIR)).toBe(true);
|
|
183
|
+
|
|
184
|
+
// Second acquisition with same session — should succeed (reentry)
|
|
185
|
+
const second = runPre('npm install', env);
|
|
186
|
+
expect(second.exitCode).toBe(0);
|
|
187
|
+
// No "continue: false" in output
|
|
188
|
+
if (second.stdout.trim()) {
|
|
189
|
+
const output = JSON.parse(second.stdout.trim());
|
|
190
|
+
expect(output.continue).not.toBe(false);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 8. Post: only releases own lock (does not release lock held by other agent)
|
|
195
|
+
it('post: only releases own lock — does not remove lock held by another agent', () => {
|
|
196
|
+
// Create lock with a different agent
|
|
197
|
+
mkdirSync(LOCK_DIR, { recursive: true });
|
|
198
|
+
const now = Math.floor(Date.now() / 1000);
|
|
199
|
+
writeFileSync(
|
|
200
|
+
join(LOCK_DIR, 'lock.json'),
|
|
201
|
+
JSON.stringify({ agentId: 'other-agent-777', timestamp: now, command: 'npm install' }),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const mySession = `test-norelease-${Date.now()}`;
|
|
205
|
+
const { exitCode } = runPost('npm install', { CLAUDE_SESSION_ID: mySession });
|
|
206
|
+
expect(exitCode).toBe(0);
|
|
207
|
+
// Lock dir should still exist — we don't own it
|
|
208
|
+
expect(existsSync(LOCK_DIR)).toBe(true);
|
|
209
|
+
const lockJson = JSON.parse(readFileSync(join(LOCK_DIR, 'lock.json'), 'utf-8'));
|
|
210
|
+
expect(lockJson.agentId).toBe('other-agent-777');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// 9. Pre: detects other lockfile commands (yarn, pnpm, cargo, pip)
|
|
214
|
+
it('pre: detects yarn, pnpm install, cargo build, pip install', () => {
|
|
215
|
+
const commands = ['yarn', 'yarn install', 'pnpm install', 'cargo build', 'pip install'];
|
|
216
|
+
for (const cmd of commands) {
|
|
217
|
+
cleanLock();
|
|
218
|
+
const sessionId = `test-detect-${Date.now()}`;
|
|
219
|
+
const { exitCode } = runPre(cmd, { CLAUDE_SESSION_ID: sessionId });
|
|
220
|
+
expect(exitCode).toBe(0);
|
|
221
|
+
expect(existsSync(LOCK_DIR)).toBe(true);
|
|
222
|
+
cleanLock();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// 10. Post: ignores non-lockfile commands
|
|
227
|
+
it('post: ignores non-lockfile commands (no crash, no lock interaction)', () => {
|
|
228
|
+
const { exitCode, stdout } = runPost('echo hello');
|
|
229
|
+
expect(exitCode).toBe(0);
|
|
230
|
+
expect(stdout.trim()).toBe('');
|
|
231
|
+
});
|
|
232
|
+
});
|