@gatebolt/cli 0.3.1 → 0.5.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/README.md +68 -19
- package/dist/agents/claude-code.js +183 -0
- package/dist/commands/configure.js +30 -13
- package/dist/commands/declare.js +54 -12
- package/dist/commands/hook.js +128 -0
- package/dist/commands/init.js +221 -0
- package/dist/commands/link.js +61 -0
- package/dist/commands/login.js +317 -0
- package/dist/commands/observe.js +46 -14
- package/dist/config.js +48 -12
- package/dist/contracts.js +6 -0
- package/dist/diff.js +6 -4
- package/dist/errors.js +35 -0
- package/dist/git.js +26 -1
- package/dist/help.js +161 -0
- package/dist/index.js +42 -43
- package/dist/profiles.js +48 -13
- package/dist/rest.js +49 -0
- package/dist/session.js +2 -1
- package/dist/transport.js +32 -12
- package/package.json +3 -4
- package/recipes/claude-code/README.md +0 -64
- package/recipes/claude-code/hooks/gatebolt-guard.sh +0 -48
- package/recipes/claude-code/hooks/gatebolt-observe.sh +0 -24
- package/recipes/claude-code/hooks/pre-push +0 -23
- package/recipes/claude-code/settings.json +0 -25
package/dist/commands/observe.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { resolveConnection } from "../config.js";
|
|
3
|
-
import { OBSERVED_FILES_MAX } from "../contracts.js";
|
|
3
|
+
import { isUuid, OBSERVED_FILES_MAX } from "../contracts.js";
|
|
4
4
|
import { observedFilesFromGit } from "../diff.js";
|
|
5
|
-
import { commitExists, diffNameStatus, diffNumstat, isReconcilableBase, listUntracked, repoContext, } from "../git.js";
|
|
5
|
+
import { commitExists, commitsSinceBase, diffNameStatus, diffNumstat, isAncestor, isReconcilableBase, listUntracked, mergeInProgress, repoContext, } from "../git.js";
|
|
6
6
|
import { isRecord } from "../json.js";
|
|
7
|
-
import { profileLoader } from "../profiles.js";
|
|
7
|
+
import { profileLoader, readRepoLink } from "../profiles.js";
|
|
8
8
|
import { clearPendingDeclare, clearSession, readSession } from "../session.js";
|
|
9
9
|
import { postChange } from "../transport.js";
|
|
10
10
|
function resolveTarget(values, session) {
|
|
@@ -23,6 +23,11 @@ function resolveTarget(values, session) {
|
|
|
23
23
|
}
|
|
24
24
|
return { changeId, base, observingSessionChange };
|
|
25
25
|
}
|
|
26
|
+
export function sessionProfileName(values, session) {
|
|
27
|
+
if (values["change-id"])
|
|
28
|
+
return undefined;
|
|
29
|
+
return session?.profile;
|
|
30
|
+
}
|
|
26
31
|
export async function runObserve(argv) {
|
|
27
32
|
const { values } = parseArgs({
|
|
28
33
|
args: argv,
|
|
@@ -39,9 +44,16 @@ export async function runObserve(argv) {
|
|
|
39
44
|
throw new Error("Not inside a git repository.");
|
|
40
45
|
}
|
|
41
46
|
const dir = repo.gitDir;
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
// Called for its throw: a malformed .gatebolt must fail here, not silently
|
|
48
|
+
// resolve to the wrong profile.
|
|
49
|
+
await readRepoLink(repo.root);
|
|
44
50
|
const session = await readSession(dir);
|
|
51
|
+
const loadProfile = profileLoader({
|
|
52
|
+
name: values.profile,
|
|
53
|
+
preferred: sessionProfileName(values, session),
|
|
54
|
+
root: repo.root,
|
|
55
|
+
});
|
|
56
|
+
const connection = await resolveConnection({ key: values.key, url: values.url }, loadProfile);
|
|
45
57
|
const { changeId, base, observingSessionChange } = resolveTarget(values, session);
|
|
46
58
|
if (!(await commitExists(base))) {
|
|
47
59
|
throw new Error(`Base commit ${base} no longer exists. Re-run \`gatebolt declare\`.`);
|
|
@@ -50,7 +62,8 @@ export async function runObserve(argv) {
|
|
|
50
62
|
if (!(await isReconcilableBase(base, declaredBranch))) {
|
|
51
63
|
throw new Error("The working tree has moved off the declared base (branch switch or divergent rebase?). Re-run `gatebolt declare`.");
|
|
52
64
|
}
|
|
53
|
-
const [nameStatus, numstat, untracked] = await Promise.all([
|
|
65
|
+
const [notes, nameStatus, numstat, untracked] = await Promise.all([
|
|
66
|
+
attributionNotes(base),
|
|
54
67
|
diffNameStatus(base),
|
|
55
68
|
diffNumstat(base),
|
|
56
69
|
listUntracked(),
|
|
@@ -64,18 +77,12 @@ export async function runObserve(argv) {
|
|
|
64
77
|
if (!isReconciliation(result)) {
|
|
65
78
|
throw new Error("Unexpected response from server.");
|
|
66
79
|
}
|
|
67
|
-
printVerdict(result);
|
|
80
|
+
printVerdict(result, notes);
|
|
68
81
|
if (observingSessionChange) {
|
|
69
82
|
await clearSession(dir);
|
|
70
83
|
await clearPendingDeclare(dir);
|
|
71
84
|
}
|
|
72
85
|
}
|
|
73
|
-
// Mirrors z.uuid() in @gatebolt/validators; keep in sync by hand (the CLI
|
|
74
|
-
// takes no dependency on the validators package).
|
|
75
|
-
const SERVER_UUID = /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;
|
|
76
|
-
export function isUuid(value) {
|
|
77
|
-
return SERVER_UUID.test(value);
|
|
78
|
-
}
|
|
79
86
|
export function isReconciliation(value) {
|
|
80
87
|
if (!isRecord(value))
|
|
81
88
|
return false;
|
|
@@ -87,7 +94,26 @@ export function isReconciliation(value) {
|
|
|
87
94
|
Array.isArray(value.missing) &&
|
|
88
95
|
Array.isArray(value.reasons));
|
|
89
96
|
}
|
|
90
|
-
|
|
97
|
+
// Never blocked on: observe is advisory, and only the person at the keyboard
|
|
98
|
+
// knows whose commits these are.
|
|
99
|
+
export async function attributionNotes(base) {
|
|
100
|
+
const notes = [];
|
|
101
|
+
if (await mergeInProgress()) {
|
|
102
|
+
notes.push("A merge is in progress; the diff includes the merged branch.");
|
|
103
|
+
}
|
|
104
|
+
// isReconcilableBase also accepts an in-place amend/squash, where base shares
|
|
105
|
+
// HEAD's parent but isn't its ancestor — and there base..HEAD would count the
|
|
106
|
+
// agent's own rewritten commit (the very work being reconciled) as foreign.
|
|
107
|
+
if (await isAncestor(base)) {
|
|
108
|
+
const commits = await commitsSinceBase(base);
|
|
109
|
+
if (commits > 0) {
|
|
110
|
+
const plural = commits === 1 ? "commit" : "commits";
|
|
111
|
+
notes.push(`${commits} ${plural} landed since declare; GateBolt cannot tell whose.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return notes;
|
|
115
|
+
}
|
|
116
|
+
function printVerdict(result, notes) {
|
|
91
117
|
const lines = [
|
|
92
118
|
`Change ${result.changeId}`,
|
|
93
119
|
`Drift ${result.driftLevel} (${result.driftScore})`,
|
|
@@ -96,5 +122,11 @@ function printVerdict(result) {
|
|
|
96
122
|
for (const reason of result.reasons) {
|
|
97
123
|
lines.push(` ${reason}`);
|
|
98
124
|
}
|
|
125
|
+
for (const note of notes) {
|
|
126
|
+
lines.push(`Note ${note}`);
|
|
127
|
+
}
|
|
128
|
+
if (notes.length > 0) {
|
|
129
|
+
lines.push(" Files above may include work from elsewhere.");
|
|
130
|
+
}
|
|
99
131
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
100
132
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,32 +1,68 @@
|
|
|
1
1
|
export const DEFAULT_BASE_URL = "https://api.gatebolt.com";
|
|
2
2
|
export async function resolveConnection(flags, loadProfile) {
|
|
3
|
-
const key = flags.key ?? process.env.
|
|
4
|
-
const url = flags.url ?? process.env.
|
|
3
|
+
const key = flags.key ?? process.env.GATEBOLT_KEY;
|
|
4
|
+
const url = flags.url ?? process.env.GATEBOLT_URL;
|
|
5
5
|
const profile = key && url ? null : await loadProfile();
|
|
6
6
|
const resolvedKey = key ?? profile?.key;
|
|
7
7
|
const baseUrl = (url ?? profile?.url ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
8
8
|
if (!resolvedKey) {
|
|
9
|
-
throw new Error("Missing API key. Set
|
|
9
|
+
throw new Error("Missing API key. Set GATEBOLT_KEY, pass --key, or run `gatebolt configure`.");
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
const connection = { key: resolvedKey, baseUrl };
|
|
12
|
+
const profileName = connectionProfileName(key, profile);
|
|
13
|
+
if (profileName)
|
|
14
|
+
connection.profileName = profileName;
|
|
15
|
+
return connection;
|
|
12
16
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
return slugifyRepo(raw);
|
|
17
|
+
function connectionProfileName(key, profile) {
|
|
18
|
+
if (key || !profile)
|
|
19
|
+
return undefined;
|
|
20
|
+
return profile.name;
|
|
19
21
|
}
|
|
20
22
|
const SLUG_MAX_LENGTH = 48;
|
|
21
|
-
|
|
23
|
+
function toSlug(value) {
|
|
22
24
|
const slug = value
|
|
23
25
|
.toLowerCase()
|
|
24
26
|
.trim()
|
|
25
27
|
.replace(/[^a-z0-9]+/g, "-")
|
|
26
|
-
.slice(0, SLUG_MAX_LENGTH)
|
|
27
28
|
.replace(/^-+|-+$/g, "");
|
|
28
29
|
if (!slug) {
|
|
29
30
|
throw new Error(`Could not derive a repository slug from "${value}".`);
|
|
30
31
|
}
|
|
32
|
+
if (slug.length > SLUG_MAX_LENGTH) {
|
|
33
|
+
throw new Error(`"${value}" makes a ${slug.length}-character slug, over the ${SLUG_MAX_LENGTH} limit. ` +
|
|
34
|
+
"Run `gatebolt link --name <shorter-name>` to name this repository yourself.");
|
|
35
|
+
}
|
|
31
36
|
return slug;
|
|
32
37
|
}
|
|
38
|
+
export function repoIdentityFromName(name) {
|
|
39
|
+
return { slug: toSlug(name), name };
|
|
40
|
+
}
|
|
41
|
+
const REMOTE_SCHEMES = new Set(["ssh:", "https:", "http:", "git:"]);
|
|
42
|
+
const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
43
|
+
const SCP_LIKE = /^(?:[^@]+@)?[^:/]{2,}:(.+)$/;
|
|
44
|
+
function remotePath(url) {
|
|
45
|
+
const trimmed = url.trim();
|
|
46
|
+
if (!HAS_SCHEME.test(trimmed)) {
|
|
47
|
+
return SCP_LIKE.exec(trimmed)?.[1] ?? null;
|
|
48
|
+
}
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = new URL(trimmed);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return REMOTE_SCHEMES.has(parsed.protocol) ? parsed.pathname : null;
|
|
57
|
+
}
|
|
58
|
+
export function parseRemoteUrl(url) {
|
|
59
|
+
const path = remotePath(url);
|
|
60
|
+
if (!path)
|
|
61
|
+
return null;
|
|
62
|
+
const segments = path.split("/").filter(Boolean);
|
|
63
|
+
const repo = segments.pop()?.replace(/\.git$/i, "");
|
|
64
|
+
if (!repo || segments.length === 0)
|
|
65
|
+
return null;
|
|
66
|
+
const name = `${segments.join("/")}/${repo}`;
|
|
67
|
+
return { slug: toSlug(name), name };
|
|
68
|
+
}
|
package/dist/contracts.js
CHANGED
|
@@ -12,3 +12,9 @@ export const AGENT_VENDORS = [
|
|
|
12
12
|
export function isAgentVendor(value) {
|
|
13
13
|
return AGENT_VENDORS.includes(value);
|
|
14
14
|
}
|
|
15
|
+
// Mirrors z.uuid() in @gatebolt/validators; keep in sync by hand (the CLI
|
|
16
|
+
// takes no dependency on the validators package).
|
|
17
|
+
const SERVER_UUID = /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;
|
|
18
|
+
export function isUuid(value) {
|
|
19
|
+
return SERVER_UUID.test(value);
|
|
20
|
+
}
|
package/dist/diff.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { REPO_LINK_FILE } from "./profiles.js";
|
|
2
|
+
const PARSER_BUG_HINT = "This is likely a GateBolt bug — please report it.";
|
|
1
3
|
export function observedFilesFromGit(nameStatusOutput, numstatOutput, untrackedOutput) {
|
|
2
4
|
const counts = parseNumstat(numstatOutput);
|
|
3
5
|
const observed = parseNameStatus(nameStatusOutput).map((entry) => {
|
|
@@ -17,7 +19,7 @@ export function observedFilesFromGit(nameStatusOutput, numstatOutput, untrackedO
|
|
|
17
19
|
continue;
|
|
18
20
|
observed.push({ path, action: "added" });
|
|
19
21
|
}
|
|
20
|
-
return observed;
|
|
22
|
+
return observed.filter((file) => file.path !== REPO_LINK_FILE);
|
|
21
23
|
}
|
|
22
24
|
function parseNameStatus(output) {
|
|
23
25
|
const reader = createReader(splitFields(output));
|
|
@@ -44,7 +46,7 @@ function parseNumstat(output) {
|
|
|
44
46
|
const firstTab = head.indexOf("\t");
|
|
45
47
|
const secondTab = head.indexOf("\t", firstTab + 1);
|
|
46
48
|
if (firstTab < 0 || secondTab < 0) {
|
|
47
|
-
throw new Error(`
|
|
49
|
+
throw new Error(`Could not read the line counts in this repository's diff — git returned an unexpected record: "${head}". ${PARSER_BUG_HINT}`);
|
|
48
50
|
}
|
|
49
51
|
const additions = head.slice(0, firstTab);
|
|
50
52
|
const deletions = head.slice(firstTab + 1, secondTab);
|
|
@@ -72,7 +74,7 @@ function statusToAction(status) {
|
|
|
72
74
|
case "D":
|
|
73
75
|
return "deleted";
|
|
74
76
|
default:
|
|
75
|
-
throw new Error(`
|
|
77
|
+
throw new Error(`Could not read this repository's diff — git reported the file status "${status}", which GateBolt does not recognise. ${PARSER_BUG_HINT}`);
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
function splitFields(output) {
|
|
@@ -89,7 +91,7 @@ function createReader(fields) {
|
|
|
89
91
|
take: () => {
|
|
90
92
|
const value = fields[index];
|
|
91
93
|
if (value === undefined) {
|
|
92
|
-
throw new Error(
|
|
94
|
+
throw new Error(`Could not read this repository's diff — git's output ended sooner than expected. ${PARSER_BUG_HINT}`);
|
|
93
95
|
}
|
|
94
96
|
index += 1;
|
|
95
97
|
return value;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { RequestError } from "./transport.js";
|
|
2
|
+
const STATUS_BAD_REQUEST = 400;
|
|
3
|
+
const STATUS_UNAUTHORIZED = 401;
|
|
4
|
+
const STATUS_NOT_FOUND = 404;
|
|
5
|
+
const STATUS_CONFLICT = 409;
|
|
6
|
+
const WRONG_WIRE_MESSAGE = "Not found — no GateBolt agent wire answered. Check the URL points at your GateBolt instance.";
|
|
7
|
+
const NOT_FOUND_MESSAGE = {
|
|
8
|
+
"change.declare": "Not found — the repository id in .gatebolt is not one this API key can record against. The repository may have been deleted, or the file may have come from another workspace. Run `gatebolt link` from the repo root to re-establish it, then commit the .gatebolt it writes.",
|
|
9
|
+
"change.observe": "Not found — no change matches this API key. `observe` must use the same key that ran `declare`; if the key was rotated in between, re-run `gatebolt declare`.",
|
|
10
|
+
"repository.link": WRONG_WIRE_MESSAGE,
|
|
11
|
+
"agentKey.create": WRONG_WIRE_MESSAGE,
|
|
12
|
+
};
|
|
13
|
+
export function formatError(error) {
|
|
14
|
+
if (error instanceof RequestError) {
|
|
15
|
+
return messageForStatus(error);
|
|
16
|
+
}
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
return error.message;
|
|
19
|
+
}
|
|
20
|
+
return "Unexpected error.";
|
|
21
|
+
}
|
|
22
|
+
export function messageForStatus(error) {
|
|
23
|
+
switch (error.status) {
|
|
24
|
+
case STATUS_BAD_REQUEST:
|
|
25
|
+
return `Invalid request — ${error.fieldErrors.join(" ") || "check the declared values and try again."}`;
|
|
26
|
+
case STATUS_UNAUTHORIZED:
|
|
27
|
+
return "Unauthorized — check the API key. Run `gatebolt configure` to set a valid key for this repo.";
|
|
28
|
+
case STATUS_NOT_FOUND:
|
|
29
|
+
return NOT_FOUND_MESSAGE[error.procedure];
|
|
30
|
+
case STATUS_CONFLICT:
|
|
31
|
+
return "Conflict — this change was already observed with different files.";
|
|
32
|
+
default:
|
|
33
|
+
return error.message;
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/git.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { resolve } from "node:path";
|
|
2
3
|
import { promisify } from "node:util";
|
|
3
4
|
const run = promisify(execFile);
|
|
4
5
|
const MAX_GIT_OUTPUT_BYTES = 67_108_864;
|
|
@@ -28,6 +29,22 @@ export async function repoContext() {
|
|
|
28
29
|
return null;
|
|
29
30
|
return { gitDir: dir, root };
|
|
30
31
|
}
|
|
32
|
+
// --git-path resolves the shared hooks dir — correct even inside a linked worktree.
|
|
33
|
+
export async function hooksPath() {
|
|
34
|
+
const output = await git(["rev-parse", "--git-path", "hooks"]);
|
|
35
|
+
return resolve(output.trim());
|
|
36
|
+
}
|
|
37
|
+
export async function remoteOriginUrl() {
|
|
38
|
+
try {
|
|
39
|
+
const output = await git(["remote", "get-url", "origin"]);
|
|
40
|
+
return output.trim() || null;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (gitExitedNonZero(error))
|
|
44
|
+
return null;
|
|
45
|
+
throw gitFailure(error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
31
48
|
export async function currentHead() {
|
|
32
49
|
const output = await git(["rev-parse", "HEAD"]);
|
|
33
50
|
return output.trim();
|
|
@@ -85,7 +102,8 @@ export async function isReconcilableBase(base, declaredBranch) {
|
|
|
85
102
|
return true;
|
|
86
103
|
// An in-place amend/squash shares HEAD's parent — but so does a sibling branch
|
|
87
104
|
// cut from that parent, and only the amend is safe to diff. Require the branch
|
|
88
|
-
// to distinguish them.
|
|
105
|
+
// to distinguish them. A same-branch reset onto a sibling still passes: it is
|
|
106
|
+
// shaped exactly like an amend, and git records nothing that separates them.
|
|
89
107
|
if (!declaredBranch || (await currentBranch()) !== declaredBranch) {
|
|
90
108
|
return false;
|
|
91
109
|
}
|
|
@@ -95,6 +113,13 @@ export async function isReconcilableBase(base, declaredBranch) {
|
|
|
95
113
|
]);
|
|
96
114
|
return baseParent !== null && baseParent === headParent;
|
|
97
115
|
}
|
|
116
|
+
export async function commitsSinceBase(base) {
|
|
117
|
+
const output = await git(["rev-list", "--count", `${base}..HEAD`]);
|
|
118
|
+
return Number(output.trim());
|
|
119
|
+
}
|
|
120
|
+
export function mergeInProgress() {
|
|
121
|
+
return commitExists("MERGE_HEAD");
|
|
122
|
+
}
|
|
98
123
|
export function diffNameStatus(base) {
|
|
99
124
|
return git(["diff", "--name-status", "-M", "-z", base]);
|
|
100
125
|
}
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { DEFAULT_BASE_URL } from "./config.js";
|
|
3
|
+
import { AGENT_VENDORS } from "./contracts.js";
|
|
4
|
+
import { isRecord } from "./json.js";
|
|
5
|
+
export const USAGE = "Usage: gatebolt <login|init|declare|observe|configure|link> [options]";
|
|
6
|
+
const ROOT_HELP = `gatebolt — the GateBolt agent client. An agent declares the files it intends to
|
|
7
|
+
touch before it edits, then observes the actual diff when the work lands;
|
|
8
|
+
GateBolt reconciles the two and scores the drift.
|
|
9
|
+
|
|
10
|
+
${USAGE}
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
login Sign in through your browser and save an agent key for this machine.
|
|
14
|
+
init Set up GateBolt in a repository: link it and wire the agent hooks.
|
|
15
|
+
declare Record the files the agent intends to touch, before it edits.
|
|
16
|
+
observe Derive the diff from git and reconcile it against the declaration.
|
|
17
|
+
configure Save a profile (url, key) to ~/.gatebolt/config.json.
|
|
18
|
+
link Record which repository this checkout is, in a committable file.
|
|
19
|
+
hook Internal — the declare/observe hook commands gatebolt init wires up.
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-h, --help Show this help, or the help of a command.
|
|
23
|
+
--version Print the version of this CLI.
|
|
24
|
+
|
|
25
|
+
Run \`gatebolt <command> --help\` for the options of a command.
|
|
26
|
+
`;
|
|
27
|
+
const LOGIN_HELP = `Usage: gatebolt login [options]
|
|
28
|
+
|
|
29
|
+
Signs in through your browser and saves an agent API key for this machine.
|
|
30
|
+
Opens a GateBolt page to approve the device, then mints an org-scoped key and
|
|
31
|
+
writes a profile to ~/.gatebolt/config.json. Run once per machine; the profile
|
|
32
|
+
is named after the organization you pick.
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--key-name <name> Label for the key in the dashboard. Defaults to
|
|
36
|
+
"gatebolt-cli (<hostname>)".
|
|
37
|
+
--url <url> GateBolt URL. Defaults to ${DEFAULT_BASE_URL}.
|
|
38
|
+
-h, --help Show this help.
|
|
39
|
+
`;
|
|
40
|
+
const DECLARE_HELP = `Usage: gatebolt declare --task <text> --vendor <vendor> --name <name>
|
|
41
|
+
--model <id> --file <path> [--file <path> ...] [options]
|
|
42
|
+
gatebolt declare --abort
|
|
43
|
+
|
|
44
|
+
Records the files the agent intends to touch, before it edits. Reads the
|
|
45
|
+
repository id from .gatebolt, or establishes it from the \`origin\` remote when
|
|
46
|
+
the file is absent. Snapshots the current commit as the base and writes the
|
|
47
|
+
change id under <git-dir>/gatebolt/ for \`gatebolt observe\` to pick up. The
|
|
48
|
+
repository must have at least one commit.
|
|
49
|
+
|
|
50
|
+
Required:
|
|
51
|
+
--task <text> What the agent is doing.
|
|
52
|
+
--vendor <vendor> One of: ${AGENT_VENDORS.join(", ")}.
|
|
53
|
+
--name <name> Agent name, e.g. "Claude Code".
|
|
54
|
+
--model <id> Agent model id.
|
|
55
|
+
--file <path> A file the agent plans to touch. Repeat for each file.
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
--agent-version <ver> Version of the agent — not of this CLI.
|
|
59
|
+
--launched-by <email> Email of the person who launched the agent.
|
|
60
|
+
--profile <name> Profile to read url and key from.
|
|
61
|
+
--key <key> API key. Overrides the profile.
|
|
62
|
+
--url <url> GateBolt URL. Overrides the profile.
|
|
63
|
+
--abort Clear the open declaration for this repo (local only;
|
|
64
|
+
use it to unstick a declaration that can no longer be
|
|
65
|
+
reconciled), then declare again. Ignores other flags.
|
|
66
|
+
-h, --help Show this help.
|
|
67
|
+
`;
|
|
68
|
+
const OBSERVE_HELP = `Usage: gatebolt observe [options]
|
|
69
|
+
|
|
70
|
+
Derives the diff from git since the declared base commit and posts it to
|
|
71
|
+
GateBolt, which reconciles it against the declaration and scores the drift.
|
|
72
|
+
Observes the change recorded by \`gatebolt declare\` unless --change-id is given.
|
|
73
|
+
|
|
74
|
+
Options:
|
|
75
|
+
--change-id <uuid> Change to observe. Defaults to the declared change.
|
|
76
|
+
--base <commit> Commit to diff against. Defaults to the declared base.
|
|
77
|
+
--profile <name> Profile to read url and key from.
|
|
78
|
+
--key <key> API key. Overrides the profile.
|
|
79
|
+
--url <url> GateBolt URL. Overrides the profile.
|
|
80
|
+
-h, --help Show this help.
|
|
81
|
+
`;
|
|
82
|
+
const CONFIGURE_HELP = `Usage: gatebolt configure [options]
|
|
83
|
+
|
|
84
|
+
Saves a profile to ~/.gatebolt/config.json, owner-readable only. Prompts for the
|
|
85
|
+
API key unless --key is given; what you type is not echoed. Run \`gatebolt link\`
|
|
86
|
+
in a repo to record which repository it is.
|
|
87
|
+
|
|
88
|
+
Options:
|
|
89
|
+
--profile <name> Profile name. Defaults to "default".
|
|
90
|
+
--url <url> GateBolt URL. Defaults to ${DEFAULT_BASE_URL}.
|
|
91
|
+
--key <key> API key. Skips the prompt, for scripting.
|
|
92
|
+
-h, --help Show this help.
|
|
93
|
+
|
|
94
|
+
Set GATEBOLT_CONFIG to keep the config somewhere other than the default path.
|
|
95
|
+
`;
|
|
96
|
+
const LINK_HELP = `Usage: gatebolt link [options]
|
|
97
|
+
|
|
98
|
+
Records which repository this checkout is, so every clone, worktree and CI job
|
|
99
|
+
resolves the same one. Derives the name from the \`origin\` remote unless --name
|
|
100
|
+
is given, then writes the repository id to a .gatebolt file at the repo root —
|
|
101
|
+
commit that file.
|
|
102
|
+
|
|
103
|
+
Options:
|
|
104
|
+
--name <name> Name this repository yourself. Required when there is no
|
|
105
|
+
\`origin\` remote to derive from.
|
|
106
|
+
--profile <name> Profile to read url and key from. Recorded in .gatebolt.
|
|
107
|
+
--key <key> API key. Overrides the profile.
|
|
108
|
+
--url <url> GateBolt URL. Overrides the profile.
|
|
109
|
+
-h, --help Show this help.
|
|
110
|
+
`;
|
|
111
|
+
const INIT_HELP = `Usage: gatebolt init [options]
|
|
112
|
+
|
|
113
|
+
Sets up GateBolt in this repository: links it (writing a committable .gatebolt),
|
|
114
|
+
wires your agent's declare → observe hooks into the repo, and installs a pre-push
|
|
115
|
+
backstop. Reuses a saved profile, so run \`gatebolt login\` or \`gatebolt configure\`
|
|
116
|
+
first. Safe to re-run.
|
|
117
|
+
|
|
118
|
+
Options:
|
|
119
|
+
--name <name> Name this repository yourself. Required when there is no
|
|
120
|
+
\`origin\` remote to derive it from.
|
|
121
|
+
--enforce <mode> \`strict\` blocks edits until you declare; \`lenient\` keeps
|
|
122
|
+
it advisory. Omit to leave a repo's current mode unchanged
|
|
123
|
+
(a fresh repo stays advisory).
|
|
124
|
+
--no-hooks Only link the repository; don't wire the agent hooks.
|
|
125
|
+
--no-pre-push Don't install the pre-push backstop.
|
|
126
|
+
--force Replace an existing, non-GateBolt pre-push hook.
|
|
127
|
+
--profile <name> Profile to read url and key from.
|
|
128
|
+
-h, --help Show this help.
|
|
129
|
+
`;
|
|
130
|
+
const HOOK_HELP = `Usage: gatebolt hook <guard|post-turn|pre-push>
|
|
131
|
+
|
|
132
|
+
Internal subcommands the hooks installed by \`gatebolt init\` invoke — you don't run
|
|
133
|
+
these by hand. \`guard\` denies an edit until a declaration exists (strict mode only),
|
|
134
|
+
\`post-turn\` reconciles the diff when a turn ends, and \`pre-push\` reconciles an open
|
|
135
|
+
declaration before a push.
|
|
136
|
+
|
|
137
|
+
Options:
|
|
138
|
+
-h, --help Show this help.
|
|
139
|
+
`;
|
|
140
|
+
const COMMAND_HELP = new Map([
|
|
141
|
+
["login", LOGIN_HELP],
|
|
142
|
+
["init", INIT_HELP],
|
|
143
|
+
["declare", DECLARE_HELP],
|
|
144
|
+
["observe", OBSERVE_HELP],
|
|
145
|
+
["configure", CONFIGURE_HELP],
|
|
146
|
+
["link", LINK_HELP],
|
|
147
|
+
["hook", HOOK_HELP],
|
|
148
|
+
]);
|
|
149
|
+
export function isCommand(command) {
|
|
150
|
+
return COMMAND_HELP.has(command);
|
|
151
|
+
}
|
|
152
|
+
export function helpFor(command) {
|
|
153
|
+
return COMMAND_HELP.get(command) ?? ROOT_HELP;
|
|
154
|
+
}
|
|
155
|
+
export async function cliVersion() {
|
|
156
|
+
const raw = await readFile(new URL("../package.json", import.meta.url), "utf8");
|
|
157
|
+
const pkg = JSON.parse(raw);
|
|
158
|
+
return isRecord(pkg) && typeof pkg.version === "string"
|
|
159
|
+
? pkg.version
|
|
160
|
+
: "unknown";
|
|
161
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,62 +1,61 @@
|
|
|
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";
|
|
5
|
+
import { runInit } from "./commands/init.js";
|
|
6
|
+
import { runLink } from "./commands/link.js";
|
|
7
|
+
import { runLogin } from "./commands/login.js";
|
|
4
8
|
import { runObserve } from "./commands/observe.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const STATUS_UNAUTHORIZED = 401;
|
|
8
|
-
const STATUS_FORBIDDEN = 403;
|
|
9
|
-
const STATUS_NOT_FOUND = 404;
|
|
10
|
-
const STATUS_CONFLICT = 409;
|
|
11
|
-
const STATUS_RATE_LIMITED = 429;
|
|
9
|
+
import { formatError } from "./errors.js";
|
|
10
|
+
import { cliVersion, helpFor, isCommand, USAGE } from "./help.js";
|
|
12
11
|
async function main() {
|
|
13
12
|
const [, , command = "", ...argv] = process.argv;
|
|
14
13
|
try {
|
|
15
|
-
|
|
16
|
-
case "declare":
|
|
17
|
-
await runDeclare(argv);
|
|
18
|
-
break;
|
|
19
|
-
case "observe":
|
|
20
|
-
await runObserve(argv);
|
|
21
|
-
break;
|
|
22
|
-
case "configure":
|
|
23
|
-
await runConfigure(argv);
|
|
24
|
-
break;
|
|
25
|
-
default:
|
|
26
|
-
process.stderr.write("Usage: gatebolt <declare|observe|configure> [options]\n");
|
|
27
|
-
process.exitCode = 1;
|
|
28
|
-
}
|
|
14
|
+
await dispatch(command, argv);
|
|
29
15
|
}
|
|
30
16
|
catch (error) {
|
|
31
17
|
process.stderr.write(`${formatError(error)}\n`);
|
|
32
18
|
process.exitCode = 1;
|
|
33
19
|
}
|
|
34
20
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
21
|
+
async function dispatch(command, argv) {
|
|
22
|
+
// The commands' parseArgs is strict, so it would reject these as unknown
|
|
23
|
+
// options: they have to be answered before argv reaches one.
|
|
24
|
+
if (wantsHelp(command, argv)) {
|
|
25
|
+
process.stdout.write(helpFor(command));
|
|
26
|
+
return;
|
|
38
27
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
28
|
+
if (command === "--version") {
|
|
29
|
+
process.stdout.write(`${await cliVersion()}\n`);
|
|
30
|
+
return;
|
|
41
31
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return "Rate limited — try again later.";
|
|
32
|
+
switch (command) {
|
|
33
|
+
case "login":
|
|
34
|
+
return runLogin(argv);
|
|
35
|
+
case "declare":
|
|
36
|
+
return runDeclare(argv);
|
|
37
|
+
case "observe":
|
|
38
|
+
return runObserve(argv);
|
|
39
|
+
case "configure":
|
|
40
|
+
return runConfigure(argv);
|
|
41
|
+
case "link":
|
|
42
|
+
return runLink(argv);
|
|
43
|
+
case "init":
|
|
44
|
+
return runInit(argv);
|
|
45
|
+
case "hook":
|
|
46
|
+
return runHook(argv);
|
|
58
47
|
default:
|
|
59
|
-
|
|
48
|
+
process.stderr.write(`${USAGE}\nRun \`gatebolt --help\` for details.\n`);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
60
51
|
}
|
|
61
52
|
}
|
|
53
|
+
function wantsHelp(command, argv) {
|
|
54
|
+
if (isHelpFlag(command))
|
|
55
|
+
return true;
|
|
56
|
+
return isCommand(command) && argv.some(isHelpFlag);
|
|
57
|
+
}
|
|
58
|
+
function isHelpFlag(arg) {
|
|
59
|
+
return arg === "--help" || arg === "-h";
|
|
60
|
+
}
|
|
62
61
|
void main();
|