@gatebolt/cli 0.7.0 → 0.8.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/agents/claude-code.js +27 -9
- package/dist/commands/hook.js +60 -23
- package/dist/commands/init.js +5 -1
- package/dist/index.js +2 -2
- package/package.json +1 -1
|
@@ -11,18 +11,39 @@ const GUARD_SUB = "guard";
|
|
|
11
11
|
const POST_TURN_SUB = "post-turn";
|
|
12
12
|
const ENFORCE_STRICT = "strict";
|
|
13
13
|
const ENFORCE_LENIENT = "lenient";
|
|
14
|
+
const DENY_DECISION = "deny";
|
|
15
|
+
export const MISSING_CLI_MESSAGE = "GateBolt: CLI not found on PATH - install it: npm i -g @gatebolt/cli";
|
|
16
|
+
function missingCliDenyPayload() {
|
|
17
|
+
return JSON.stringify({
|
|
18
|
+
hookSpecificOutput: {
|
|
19
|
+
hookEventName: PRE_TOOL_USE,
|
|
20
|
+
permissionDecision: DENY_DECISION,
|
|
21
|
+
permissionDecisionReason: MISSING_CLI_MESSAGE,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function guardCommand(cli) {
|
|
26
|
+
return (`command -v ${cli} >/dev/null 2>&1 && exec ${cli} hook ${GUARD_SUB} || ` +
|
|
27
|
+
`{ [ "$GATEBOLT_ENFORCE" = ${ENFORCE_STRICT} ] && ` +
|
|
28
|
+
`{ echo "${MISSING_CLI_MESSAGE}" >&2; ` +
|
|
29
|
+
`printf '%s\\n' '${missingCliDenyPayload()}'; }; exit 0; }`);
|
|
30
|
+
}
|
|
31
|
+
function postTurnCommand(cli) {
|
|
32
|
+
return (`command -v ${cli} >/dev/null 2>&1 && exec ${cli} hook ${POST_TURN_SUB} || ` +
|
|
33
|
+
`echo "${MISSING_CLI_MESSAGE}" >&2`);
|
|
34
|
+
}
|
|
14
35
|
export function mergeSettings(existing, opts) {
|
|
15
36
|
const settings = structuredClone(existing);
|
|
16
37
|
const hooks = plainObject(settings.hooks, "hooks");
|
|
17
38
|
settings.hooks = hooks;
|
|
18
39
|
let changed = false;
|
|
19
40
|
const preToolUse = arrayField(hooks[PRE_TOOL_USE], `hooks.${PRE_TOOL_USE}`);
|
|
20
|
-
if (ensureHookCommand(preToolUse, `hook ${GUARD_SUB}`, opts.command, EDIT_TOOLS_MATCHER)) {
|
|
41
|
+
if (ensureHookCommand(preToolUse, `hook ${GUARD_SUB}`, guardCommand(opts.command), EDIT_TOOLS_MATCHER)) {
|
|
21
42
|
changed = true;
|
|
22
43
|
}
|
|
23
44
|
hooks[PRE_TOOL_USE] = preToolUse;
|
|
24
45
|
const stop = arrayField(hooks[STOP], `hooks.${STOP}`);
|
|
25
|
-
if (ensureHookCommand(stop, `hook ${POST_TURN_SUB}`, opts.command)) {
|
|
46
|
+
if (ensureHookCommand(stop, `hook ${POST_TURN_SUB}`, postTurnCommand(opts.command))) {
|
|
26
47
|
changed = true;
|
|
27
48
|
}
|
|
28
49
|
hooks[STOP] = stop;
|
|
@@ -71,11 +92,8 @@ function arrayField(value, label) {
|
|
|
71
92
|
return value;
|
|
72
93
|
throw new Error(`${SETTINGS_FILE}: "${label}" must be a JSON array.`);
|
|
73
94
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
function ensureHookCommand(entries, suffix, command, matcher) {
|
|
77
|
-
const desired = `${command} ${suffix}`;
|
|
78
|
-
const existing = findGateboltHook(entries, suffix);
|
|
95
|
+
function ensureHookCommand(entries, marker, desired, matcher) {
|
|
96
|
+
const existing = findGateboltHook(entries, marker);
|
|
79
97
|
if (existing) {
|
|
80
98
|
if (existing.command === desired)
|
|
81
99
|
return false;
|
|
@@ -86,14 +104,14 @@ function ensureHookCommand(entries, suffix, command, matcher) {
|
|
|
86
104
|
entries.push(matcher === undefined ? { hooks: [hook] } : { matcher, hooks: [hook] });
|
|
87
105
|
return true;
|
|
88
106
|
}
|
|
89
|
-
function findGateboltHook(entries,
|
|
107
|
+
function findGateboltHook(entries, marker) {
|
|
90
108
|
for (const entry of entries) {
|
|
91
109
|
if (!isRecord(entry) || !Array.isArray(entry.hooks))
|
|
92
110
|
continue;
|
|
93
111
|
for (const hook of entry.hooks) {
|
|
94
112
|
if (isRecord(hook) &&
|
|
95
113
|
typeof hook.command === "string" &&
|
|
96
|
-
hook.command.
|
|
114
|
+
hook.command.includes(marker)) {
|
|
97
115
|
return hook;
|
|
98
116
|
}
|
|
99
117
|
}
|
package/dist/commands/hook.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
2
3
|
import { formatError } from "../errors.js";
|
|
3
4
|
import { hasCommits, repoContext } from "../git.js";
|
|
5
|
+
import { isRecord } from "../json.js";
|
|
4
6
|
import { readSession } from "../session.js";
|
|
5
7
|
import { RequestError } from "../transport.js";
|
|
6
8
|
import { runObserve } from "./observe.js";
|
|
@@ -21,33 +23,68 @@ task — specific repo-relative paths, not broad globs like src/** :
|
|
|
21
23
|
The GateBolt CLI is already configured for this repository. Once it
|
|
22
24
|
prints "Declared change …", retry the edit.`;
|
|
23
25
|
}
|
|
24
|
-
export async function runHook(argv) {
|
|
25
|
-
anchorToProjectDir();
|
|
26
|
-
drainStdin();
|
|
26
|
+
export async function runHook(argv, input = {}) {
|
|
27
27
|
const sub = argv[0] ?? "";
|
|
28
28
|
switch (sub) {
|
|
29
29
|
case "guard":
|
|
30
|
-
return runGuard();
|
|
30
|
+
return runGuard(input);
|
|
31
31
|
case "post-turn":
|
|
32
|
-
return runPostTurn();
|
|
32
|
+
return runPostTurn(input);
|
|
33
33
|
case "pre-push":
|
|
34
|
-
return runPrePush();
|
|
34
|
+
return runPrePush(input);
|
|
35
35
|
default:
|
|
36
36
|
throw new Error(`Unknown hook '${sub}'. Expected one of: guard, post-turn, pre-push.`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (dir && existsSync(dir))
|
|
39
|
+
async function resolveRepo(input) {
|
|
40
|
+
const candidates = [
|
|
41
|
+
input.filePath ? dirname(input.filePath) : undefined,
|
|
42
|
+
input.cwd,
|
|
43
|
+
process.env.CLAUDE_PROJECT_DIR,
|
|
44
|
+
process.cwd(),
|
|
45
|
+
];
|
|
46
|
+
for (const dir of candidates) {
|
|
47
|
+
if (!dir || !existsSync(dir))
|
|
48
|
+
continue;
|
|
50
49
|
process.chdir(dir);
|
|
50
|
+
const repo = await hookRepoContext();
|
|
51
|
+
if (repo)
|
|
52
|
+
return repo;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
export async function readHookPayload() {
|
|
57
|
+
const data = safeJsonParse(await readStdin());
|
|
58
|
+
if (!isRecord(data))
|
|
59
|
+
return {};
|
|
60
|
+
const toolInput = isRecord(data.tool_input) ? data.tool_input : undefined;
|
|
61
|
+
return {
|
|
62
|
+
cwd: typeof data.cwd === "string" ? data.cwd : undefined,
|
|
63
|
+
filePath: toolInput && typeof toolInput.file_path === "string"
|
|
64
|
+
? toolInput.file_path
|
|
65
|
+
: undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function safeJsonParse(raw) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(raw);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function readStdin() {
|
|
77
|
+
if (process.stdin.isTTY)
|
|
78
|
+
return Promise.resolve("");
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const chunks = [];
|
|
81
|
+
const done = () => resolve(Buffer.concat(chunks).toString("utf8"));
|
|
82
|
+
process.stdin.on("data", (chunk) => {
|
|
83
|
+
chunks.push(chunk);
|
|
84
|
+
});
|
|
85
|
+
process.stdin.on("end", done);
|
|
86
|
+
process.stdin.on("error", done);
|
|
87
|
+
});
|
|
51
88
|
}
|
|
52
89
|
async function hookRepoContext() {
|
|
53
90
|
try {
|
|
@@ -57,10 +94,10 @@ async function hookRepoContext() {
|
|
|
57
94
|
return null;
|
|
58
95
|
}
|
|
59
96
|
}
|
|
60
|
-
async function runGuard() {
|
|
97
|
+
async function runGuard(input) {
|
|
61
98
|
if (process.env.GATEBOLT_ENFORCE !== ENFORCE_STRICT)
|
|
62
99
|
return;
|
|
63
|
-
const repo = await
|
|
100
|
+
const repo = await resolveRepo(input);
|
|
64
101
|
if (!repo)
|
|
65
102
|
return;
|
|
66
103
|
if (await readSession(repo.gitDir))
|
|
@@ -71,8 +108,8 @@ async function runGuard() {
|
|
|
71
108
|
const cli = process.env.GATEBOLT_CLI ?? DEFAULT_CLI;
|
|
72
109
|
process.stdout.write(denyPayload(declareInstructions(cli)));
|
|
73
110
|
}
|
|
74
|
-
async function runPostTurn() {
|
|
75
|
-
const repo = await
|
|
111
|
+
async function runPostTurn(input) {
|
|
112
|
+
const repo = await resolveRepo(input);
|
|
76
113
|
if (!repo)
|
|
77
114
|
return;
|
|
78
115
|
if (!(await readSession(repo.gitDir)))
|
|
@@ -86,8 +123,8 @@ async function runPostTurn() {
|
|
|
86
123
|
}
|
|
87
124
|
// Backstop for the interrupt case the Stop hook misses: an open, un-observed
|
|
88
125
|
// declaration.
|
|
89
|
-
async function runPrePush() {
|
|
90
|
-
const repo = await
|
|
126
|
+
async function runPrePush(input) {
|
|
127
|
+
const repo = await resolveRepo(input);
|
|
91
128
|
if (!repo)
|
|
92
129
|
return;
|
|
93
130
|
if (!(await readSession(repo.gitDir)))
|
package/dist/commands/init.js
CHANGED
|
@@ -3,7 +3,7 @@ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { delimiter, dirname, isAbsolute, join, relative } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
|
-
import { claudeCodeAdapter } from "../agents/claude-code.js";
|
|
6
|
+
import { claudeCodeAdapter, MISSING_CLI_MESSAGE, } from "../agents/claude-code.js";
|
|
7
7
|
import { repoIdentityFromName, resolveConnection } from "../config.js";
|
|
8
8
|
import { hooksPath, isPathIgnored, isUncommitted, repoContext, } from "../git.js";
|
|
9
9
|
import { isRecord } from "../json.js";
|
|
@@ -163,6 +163,10 @@ export function prePushScript(command) {
|
|
|
163
163
|
return [
|
|
164
164
|
"#!/usr/bin/env sh",
|
|
165
165
|
`# ${PRE_PUSH_MARKER} — reconciles an open declaration before a push.`,
|
|
166
|
+
`if ! command -v ${command} >/dev/null 2>&1; then`,
|
|
167
|
+
` echo "${MISSING_CLI_MESSAGE}" >&2`,
|
|
168
|
+
" exit 0",
|
|
169
|
+
"fi",
|
|
166
170
|
`exec ${command} hook ${PRE_PUSH_FILE}`,
|
|
167
171
|
"",
|
|
168
172
|
].join("\n");
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runConfigure } from "./commands/configure.js";
|
|
3
3
|
import { runDeclare } from "./commands/declare.js";
|
|
4
|
-
import { runHook } from "./commands/hook.js";
|
|
4
|
+
import { readHookPayload, runHook } from "./commands/hook.js";
|
|
5
5
|
import { runInit } from "./commands/init.js";
|
|
6
6
|
import { runLink } from "./commands/link.js";
|
|
7
7
|
import { runLogin } from "./commands/login.js";
|
|
@@ -43,7 +43,7 @@ async function dispatch(command, argv) {
|
|
|
43
43
|
case "init":
|
|
44
44
|
return runInit(argv);
|
|
45
45
|
case "hook":
|
|
46
|
-
return runHook(argv);
|
|
46
|
+
return runHook(argv, await readHookPayload());
|
|
47
47
|
default:
|
|
48
48
|
process.stderr.write(`${USAGE}\nRun \`gatebolt --help\` for details.\n`);
|
|
49
49
|
process.exitCode = 1;
|
package/package.json
CHANGED