@gatebolt/cli 0.3.0 → 0.4.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 +10 -4
- package/dist/commands/configure.js +22 -2
- package/dist/commands/declare.js +4 -3
- package/dist/commands/observe.js +30 -4
- package/dist/config.js +1 -1
- package/dist/diff.js +4 -3
- package/dist/errors.js +33 -0
- package/dist/git.js +9 -1
- package/dist/help.js +94 -0
- package/dist/index.js +30 -43
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,11 +16,14 @@ npx @gatebolt/cli declare …
|
|
|
16
16
|
|
|
17
17
|
Requires Node ≥ 20 and `git` on your `PATH`.
|
|
18
18
|
|
|
19
|
+
`gatebolt --help` lists the commands and `gatebolt <command> --help` details one;
|
|
20
|
+
`gatebolt --version` prints the version of the CLI itself.
|
|
21
|
+
|
|
19
22
|
## Configure
|
|
20
23
|
|
|
21
24
|
Every command needs three settings — an API **key**, the GateBolt **URL**, and the **repo
|
|
22
|
-
slug**
|
|
23
|
-
(written owner-only, `0600`):
|
|
25
|
+
slug** to record the work against. Store them once as a named **profile** in
|
|
26
|
+
`~/.gatebolt/config.json` (written owner-only, `0600`):
|
|
24
27
|
|
|
25
28
|
```
|
|
26
29
|
gatebolt configure --profile payments-api \
|
|
@@ -60,7 +63,7 @@ to `<git-dir>/gatebolt/session.json`. The repository must have at least one comm
|
|
|
60
63
|
| `--name <name>` | yes | Agent name |
|
|
61
64
|
| `--model <model>` | yes | Model id |
|
|
62
65
|
| `--file <path>` | yes\* | Intended file (repeatable, one per path) |
|
|
63
|
-
| `--version <v>`
|
|
66
|
+
| `--agent-version <v>` | no | Agent/tool version (`--version` prints the CLI's own version) |
|
|
64
67
|
| `--launched-by <email>` | no | Who launched the agent |
|
|
65
68
|
| `--profile <name>` | no | Profile to resolve `key`/`url`/`repo` from |
|
|
66
69
|
|
|
@@ -101,7 +104,10 @@ gatebolt observe
|
|
|
101
104
|
untracked files, so uncommitted edits and newly created files are both captured. It accepts a
|
|
102
105
|
base that is an ancestor of HEAD or one HEAD amended/squashed in place on the same branch, but
|
|
103
106
|
**refuses if the working tree has diverged from the recorded base** — a branch switch or reset
|
|
104
|
-
would fold in unrelated work, so re-run `declare`. It
|
|
107
|
+
would fold in unrelated work, so re-run `declare`. It never blocks on ambiguity, though: git
|
|
108
|
+
records nothing that separates the agent's commits from a colleague's, so when commits have
|
|
109
|
+
landed since `declare` — or a merge is in progress — `observe` **says so in a `Note` and
|
|
110
|
+
reconciles anyway**, leaving you to judge whose work the diff contains. It is **advisory** — it exits `0` regardless of the
|
|
105
111
|
drift level (errors exit non-zero) — and on success it clears the session. A diff touching more
|
|
106
112
|
than 2000 files is rejected; narrow the change.
|
|
107
113
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { stdin, stdout } from "node:process";
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { Writable } from "node:stream";
|
|
3
4
|
import { parseArgs } from "node:util";
|
|
4
5
|
import { slugifyRepo } from "../config.js";
|
|
5
6
|
import { readConfigForWrite, writeConfig } from "../profiles.js";
|
|
@@ -33,11 +34,30 @@ function requireFlag(value, flag) {
|
|
|
33
34
|
return value;
|
|
34
35
|
}
|
|
35
36
|
async function promptForKey() {
|
|
36
|
-
|
|
37
|
+
let muted = false;
|
|
38
|
+
const output = new Writable({
|
|
39
|
+
write(chunk, encoding, done) {
|
|
40
|
+
if (!muted)
|
|
41
|
+
stdout.write(chunk, encoding);
|
|
42
|
+
done();
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
// `terminal` moves echoing from the tty to readline, which is what makes it
|
|
46
|
+
// suppressible; muting the stream alone leaves the tty echoing in canonical
|
|
47
|
+
// mode. Off a tty nothing echoes anyway, and leaving it off keeps readline's
|
|
48
|
+
// cursor escapes out of piped output.
|
|
49
|
+
const rl = createInterface({
|
|
50
|
+
input: stdin,
|
|
51
|
+
output,
|
|
52
|
+
terminal: stdin.isTTY === true,
|
|
53
|
+
});
|
|
37
54
|
try {
|
|
38
|
-
|
|
55
|
+
const answer = rl.question("API key: ");
|
|
56
|
+
muted = true;
|
|
57
|
+
return await answer;
|
|
39
58
|
}
|
|
40
59
|
finally {
|
|
41
60
|
rl.close();
|
|
61
|
+
stdout.write("\n");
|
|
42
62
|
}
|
|
43
63
|
}
|
package/dist/commands/declare.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function runDeclare(argv) {
|
|
|
20
20
|
vendor: { type: "string" },
|
|
21
21
|
name: { type: "string" },
|
|
22
22
|
model: { type: "string" },
|
|
23
|
-
version: { type: "string" },
|
|
23
|
+
"agent-version": { type: "string" },
|
|
24
24
|
"launched-by": { type: "string" },
|
|
25
25
|
file: { type: "string", multiple: true },
|
|
26
26
|
},
|
|
@@ -38,8 +38,9 @@ export async function runDeclare(argv) {
|
|
|
38
38
|
name: requireFlag(values.name, "--name"),
|
|
39
39
|
model: requireFlag(values.model, "--model"),
|
|
40
40
|
};
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const agentVersion = values["agent-version"];
|
|
42
|
+
if (agentVersion)
|
|
43
|
+
agent.version = agentVersion;
|
|
43
44
|
const launchedBy = values["launched-by"];
|
|
44
45
|
if (launchedBy && !isValidEmail(launchedBy)) {
|
|
45
46
|
throw new Error(`Invalid --launched-by '${launchedBy}'. Expected an email address.`);
|
package/dist/commands/observe.js
CHANGED
|
@@ -2,7 +2,7 @@ import { parseArgs } from "node:util";
|
|
|
2
2
|
import { resolveConnection } from "../config.js";
|
|
3
3
|
import { 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
7
|
import { profileLoader } from "../profiles.js";
|
|
8
8
|
import { clearPendingDeclare, clearSession, readSession } from "../session.js";
|
|
@@ -50,7 +50,8 @@ export async function runObserve(argv) {
|
|
|
50
50
|
if (!(await isReconcilableBase(base, declaredBranch))) {
|
|
51
51
|
throw new Error("The working tree has moved off the declared base (branch switch or divergent rebase?). Re-run `gatebolt declare`.");
|
|
52
52
|
}
|
|
53
|
-
const [nameStatus, numstat, untracked] = await Promise.all([
|
|
53
|
+
const [notes, nameStatus, numstat, untracked] = await Promise.all([
|
|
54
|
+
attributionNotes(base),
|
|
54
55
|
diffNameStatus(base),
|
|
55
56
|
diffNumstat(base),
|
|
56
57
|
listUntracked(),
|
|
@@ -64,7 +65,7 @@ export async function runObserve(argv) {
|
|
|
64
65
|
if (!isReconciliation(result)) {
|
|
65
66
|
throw new Error("Unexpected response from server.");
|
|
66
67
|
}
|
|
67
|
-
printVerdict(result);
|
|
68
|
+
printVerdict(result, notes);
|
|
68
69
|
if (observingSessionChange) {
|
|
69
70
|
await clearSession(dir);
|
|
70
71
|
await clearPendingDeclare(dir);
|
|
@@ -87,7 +88,26 @@ export function isReconciliation(value) {
|
|
|
87
88
|
Array.isArray(value.missing) &&
|
|
88
89
|
Array.isArray(value.reasons));
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
+
// Never blocked on: observe is advisory, and only the person at the keyboard
|
|
92
|
+
// knows whose commits these are.
|
|
93
|
+
export async function attributionNotes(base) {
|
|
94
|
+
const notes = [];
|
|
95
|
+
if (await mergeInProgress()) {
|
|
96
|
+
notes.push("A merge is in progress; the diff includes the merged branch.");
|
|
97
|
+
}
|
|
98
|
+
// isReconcilableBase also accepts an in-place amend/squash, where base shares
|
|
99
|
+
// HEAD's parent but isn't its ancestor — and there base..HEAD would count the
|
|
100
|
+
// agent's own rewritten commit (the very work being reconciled) as foreign.
|
|
101
|
+
if (await isAncestor(base)) {
|
|
102
|
+
const commits = await commitsSinceBase(base);
|
|
103
|
+
if (commits > 0) {
|
|
104
|
+
const plural = commits === 1 ? "commit" : "commits";
|
|
105
|
+
notes.push(`${commits} ${plural} landed since declare; GateBolt cannot tell whose.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return notes;
|
|
109
|
+
}
|
|
110
|
+
function printVerdict(result, notes) {
|
|
91
111
|
const lines = [
|
|
92
112
|
`Change ${result.changeId}`,
|
|
93
113
|
`Drift ${result.driftLevel} (${result.driftScore})`,
|
|
@@ -96,5 +116,11 @@ function printVerdict(result) {
|
|
|
96
116
|
for (const reason of result.reasons) {
|
|
97
117
|
lines.push(` ${reason}`);
|
|
98
118
|
}
|
|
119
|
+
for (const note of notes) {
|
|
120
|
+
lines.push(`Note ${note}`);
|
|
121
|
+
}
|
|
122
|
+
if (notes.length > 0) {
|
|
123
|
+
lines.push(" Files above may include work from elsewhere.");
|
|
124
|
+
}
|
|
99
125
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
100
126
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const DEFAULT_BASE_URL = "https://gatebolt.com";
|
|
1
|
+
export const DEFAULT_BASE_URL = "https://api.gatebolt.com";
|
|
2
2
|
export async function resolveConnection(flags, loadProfile) {
|
|
3
3
|
const key = flags.key ?? process.env.GBOLT_KEY;
|
|
4
4
|
const url = flags.url ?? process.env.GBOLT_URL;
|
package/dist/diff.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const PARSER_BUG_HINT = "This is likely a GateBolt bug — please report it.";
|
|
1
2
|
export function observedFilesFromGit(nameStatusOutput, numstatOutput, untrackedOutput) {
|
|
2
3
|
const counts = parseNumstat(numstatOutput);
|
|
3
4
|
const observed = parseNameStatus(nameStatusOutput).map((entry) => {
|
|
@@ -44,7 +45,7 @@ function parseNumstat(output) {
|
|
|
44
45
|
const firstTab = head.indexOf("\t");
|
|
45
46
|
const secondTab = head.indexOf("\t", firstTab + 1);
|
|
46
47
|
if (firstTab < 0 || secondTab < 0) {
|
|
47
|
-
throw new Error(`
|
|
48
|
+
throw new Error(`Could not read the line counts in this repository's diff — git returned an unexpected record: "${head}". ${PARSER_BUG_HINT}`);
|
|
48
49
|
}
|
|
49
50
|
const additions = head.slice(0, firstTab);
|
|
50
51
|
const deletions = head.slice(firstTab + 1, secondTab);
|
|
@@ -72,7 +73,7 @@ function statusToAction(status) {
|
|
|
72
73
|
case "D":
|
|
73
74
|
return "deleted";
|
|
74
75
|
default:
|
|
75
|
-
throw new Error(`
|
|
76
|
+
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
77
|
}
|
|
77
78
|
}
|
|
78
79
|
function splitFields(output) {
|
|
@@ -89,7 +90,7 @@ function createReader(fields) {
|
|
|
89
90
|
take: () => {
|
|
90
91
|
const value = fields[index];
|
|
91
92
|
if (value === undefined) {
|
|
92
|
-
throw new Error(
|
|
93
|
+
throw new Error(`Could not read this repository's diff — git's output ended sooner than expected. ${PARSER_BUG_HINT}`);
|
|
93
94
|
}
|
|
94
95
|
index += 1;
|
|
95
96
|
return value;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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 STATUS_RATE_LIMITED = 429;
|
|
7
|
+
export function formatError(error) {
|
|
8
|
+
if (error instanceof RequestError) {
|
|
9
|
+
return messageForStatus(error);
|
|
10
|
+
}
|
|
11
|
+
if (error instanceof Error) {
|
|
12
|
+
return error.message;
|
|
13
|
+
}
|
|
14
|
+
return "Unexpected error.";
|
|
15
|
+
}
|
|
16
|
+
export function messageForStatus(error) {
|
|
17
|
+
switch (error.status) {
|
|
18
|
+
case STATUS_BAD_REQUEST:
|
|
19
|
+
return "Invalid request — check the declared values and try again.";
|
|
20
|
+
// The server collapses a missing/invalid key and a spent rate limit into one
|
|
21
|
+
// 401, so this cannot name which without guessing.
|
|
22
|
+
case STATUS_UNAUTHORIZED:
|
|
23
|
+
return "Unauthorized — the API key is invalid or has hit its rate limit.";
|
|
24
|
+
case STATUS_NOT_FOUND:
|
|
25
|
+
return "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`.";
|
|
26
|
+
case STATUS_CONFLICT:
|
|
27
|
+
return "Conflict — this change was already observed with different files.";
|
|
28
|
+
case STATUS_RATE_LIMITED:
|
|
29
|
+
return "Rate limited — try again later.";
|
|
30
|
+
default:
|
|
31
|
+
return error.message;
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/git.js
CHANGED
|
@@ -85,7 +85,8 @@ export async function isReconcilableBase(base, declaredBranch) {
|
|
|
85
85
|
return true;
|
|
86
86
|
// An in-place amend/squash shares HEAD's parent — but so does a sibling branch
|
|
87
87
|
// cut from that parent, and only the amend is safe to diff. Require the branch
|
|
88
|
-
// to distinguish them.
|
|
88
|
+
// to distinguish them. A same-branch reset onto a sibling still passes: it is
|
|
89
|
+
// shaped exactly like an amend, and git records nothing that separates them.
|
|
89
90
|
if (!declaredBranch || (await currentBranch()) !== declaredBranch) {
|
|
90
91
|
return false;
|
|
91
92
|
}
|
|
@@ -95,6 +96,13 @@ export async function isReconcilableBase(base, declaredBranch) {
|
|
|
95
96
|
]);
|
|
96
97
|
return baseParent !== null && baseParent === headParent;
|
|
97
98
|
}
|
|
99
|
+
export async function commitsSinceBase(base) {
|
|
100
|
+
const output = await git(["rev-list", "--count", `${base}..HEAD`]);
|
|
101
|
+
return Number(output.trim());
|
|
102
|
+
}
|
|
103
|
+
export function mergeInProgress() {
|
|
104
|
+
return commitExists("MERGE_HEAD");
|
|
105
|
+
}
|
|
98
106
|
export function diffNameStatus(base) {
|
|
99
107
|
return git(["diff", "--name-status", "-M", "-z", base]);
|
|
100
108
|
}
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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 <declare|observe|configure> [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
|
+
declare Record the files the agent intends to touch, before it edits.
|
|
14
|
+
observe Derive the diff from git and reconcile it against the declaration.
|
|
15
|
+
configure Save a per-repo profile (url, repo, key) to ~/.gatebolt/config.json.
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
-h, --help Show this help, or the help of a command.
|
|
19
|
+
--version Print the version of this CLI.
|
|
20
|
+
|
|
21
|
+
Run \`gatebolt <command> --help\` for the options of a command.
|
|
22
|
+
`;
|
|
23
|
+
const DECLARE_HELP = `Usage: gatebolt declare --task <text> --vendor <vendor> --name <name>
|
|
24
|
+
--model <id> --file <path> [--file <path> ...] [options]
|
|
25
|
+
|
|
26
|
+
Records the files the agent intends to touch, before it edits. Snapshots the
|
|
27
|
+
current commit as the base and writes the change id under <git-dir>/gatebolt/
|
|
28
|
+
for \`gatebolt observe\` to pick up. The repository must have at least one commit.
|
|
29
|
+
|
|
30
|
+
Required:
|
|
31
|
+
--task <text> What the agent is doing.
|
|
32
|
+
--vendor <vendor> One of: ${AGENT_VENDORS.join(", ")}.
|
|
33
|
+
--name <name> Agent name, e.g. "Claude Code".
|
|
34
|
+
--model <id> Agent model id.
|
|
35
|
+
--file <path> A file the agent plans to touch. Repeat for each file.
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--agent-version <ver> Version of the agent — not of this CLI.
|
|
39
|
+
--launched-by <email> Email of the person who launched the agent.
|
|
40
|
+
--profile <name> Profile to read url, repo and key from.
|
|
41
|
+
--key <key> API key. Overrides the profile.
|
|
42
|
+
--url <url> GateBolt URL. Overrides the profile.
|
|
43
|
+
--repo <slug> Repo slug. Overrides the profile.
|
|
44
|
+
-h, --help Show this help.
|
|
45
|
+
`;
|
|
46
|
+
const OBSERVE_HELP = `Usage: gatebolt observe [options]
|
|
47
|
+
|
|
48
|
+
Derives the diff from git since the declared base commit and posts it to
|
|
49
|
+
GateBolt, which reconciles it against the declaration and scores the drift.
|
|
50
|
+
Observes the change recorded by \`gatebolt declare\` unless --change-id is given.
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
--change-id <uuid> Change to observe. Defaults to the declared change.
|
|
54
|
+
--base <commit> Commit to diff against. Defaults to the declared base.
|
|
55
|
+
--profile <name> Profile to read url and key from.
|
|
56
|
+
--key <key> API key. Overrides the profile.
|
|
57
|
+
--url <url> GateBolt URL. Overrides the profile.
|
|
58
|
+
-h, --help Show this help.
|
|
59
|
+
`;
|
|
60
|
+
const CONFIGURE_HELP = `Usage: gatebolt configure --repo <slug> [options]
|
|
61
|
+
|
|
62
|
+
Saves a profile to ~/.gatebolt/config.json, owner-readable only. Prompts for the
|
|
63
|
+
API key unless --key is given; what you type is not echoed.
|
|
64
|
+
|
|
65
|
+
Required:
|
|
66
|
+
--repo <slug> Slug of the repo this agent works in, created on the first
|
|
67
|
+
declare. Normalized to lowercase-hyphens.
|
|
68
|
+
|
|
69
|
+
Options:
|
|
70
|
+
--profile <name> Profile name. Defaults to the repo slug.
|
|
71
|
+
--url <url> GateBolt URL. Defaults to ${DEFAULT_BASE_URL}.
|
|
72
|
+
--key <key> API key. Skips the prompt, for scripting.
|
|
73
|
+
-h, --help Show this help.
|
|
74
|
+
|
|
75
|
+
Set GATEBOLT_CONFIG to keep the config somewhere other than the default path.
|
|
76
|
+
`;
|
|
77
|
+
const COMMAND_HELP = new Map([
|
|
78
|
+
["declare", DECLARE_HELP],
|
|
79
|
+
["observe", OBSERVE_HELP],
|
|
80
|
+
["configure", CONFIGURE_HELP],
|
|
81
|
+
]);
|
|
82
|
+
export function isCommand(command) {
|
|
83
|
+
return COMMAND_HELP.has(command);
|
|
84
|
+
}
|
|
85
|
+
export function helpFor(command) {
|
|
86
|
+
return COMMAND_HELP.get(command) ?? ROOT_HELP;
|
|
87
|
+
}
|
|
88
|
+
export async function cliVersion() {
|
|
89
|
+
const raw = await readFile(new URL("../package.json", import.meta.url), "utf8");
|
|
90
|
+
const pkg = JSON.parse(raw);
|
|
91
|
+
return isRecord(pkg) && typeof pkg.version === "string"
|
|
92
|
+
? pkg.version
|
|
93
|
+
: "unknown";
|
|
94
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,61 +2,48 @@
|
|
|
2
2
|
import { runConfigure } from "./commands/configure.js";
|
|
3
3
|
import { runDeclare } from "./commands/declare.js";
|
|
4
4
|
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;
|
|
5
|
+
import { formatError } from "./errors.js";
|
|
6
|
+
import { cliVersion, helpFor, isCommand, USAGE } from "./help.js";
|
|
12
7
|
async function main() {
|
|
13
8
|
const [, , command = "", ...argv] = process.argv;
|
|
14
9
|
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
|
-
}
|
|
10
|
+
await dispatch(command, argv);
|
|
29
11
|
}
|
|
30
12
|
catch (error) {
|
|
31
13
|
process.stderr.write(`${formatError(error)}\n`);
|
|
32
14
|
process.exitCode = 1;
|
|
33
15
|
}
|
|
34
16
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
17
|
+
async function dispatch(command, argv) {
|
|
18
|
+
// The commands' parseArgs is strict, so it would reject these as unknown
|
|
19
|
+
// options: they have to be answered before argv reaches one.
|
|
20
|
+
if (wantsHelp(command, argv)) {
|
|
21
|
+
process.stdout.write(helpFor(command));
|
|
22
|
+
return;
|
|
38
23
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
24
|
+
if (command === "--version") {
|
|
25
|
+
process.stdout.write(`${await cliVersion()}\n`);
|
|
26
|
+
return;
|
|
41
27
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return "Unauthorized — the API key is invalid or has hit its rate limit.";
|
|
50
|
-
case STATUS_FORBIDDEN:
|
|
51
|
-
return "Forbidden — the repository slug does not match this API key.";
|
|
52
|
-
case STATUS_NOT_FOUND:
|
|
53
|
-
return "Not found — no matching change for this key and repository.";
|
|
54
|
-
case STATUS_CONFLICT:
|
|
55
|
-
return "Conflict — this change was already observed with different files.";
|
|
56
|
-
case STATUS_RATE_LIMITED:
|
|
57
|
-
return "Rate limited — try again later.";
|
|
28
|
+
switch (command) {
|
|
29
|
+
case "declare":
|
|
30
|
+
return runDeclare(argv);
|
|
31
|
+
case "observe":
|
|
32
|
+
return runObserve(argv);
|
|
33
|
+
case "configure":
|
|
34
|
+
return runConfigure(argv);
|
|
58
35
|
default:
|
|
59
|
-
|
|
36
|
+
process.stderr.write(`${USAGE}\nRun \`gatebolt --help\` for details.\n`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
60
39
|
}
|
|
61
40
|
}
|
|
41
|
+
function wantsHelp(command, argv) {
|
|
42
|
+
if (isHelpFlag(command))
|
|
43
|
+
return true;
|
|
44
|
+
return isCommand(command) && argv.some(isHelpFlag);
|
|
45
|
+
}
|
|
46
|
+
function isHelpFlag(arg) {
|
|
47
|
+
return arg === "--help" || arg === "-h";
|
|
48
|
+
}
|
|
62
49
|
void main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gatebolt/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Declare-then-observe agent client for GateBolt — records the files an AI agent intends to touch, then reconciles the actual git diff.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gatebolt",
|
|
@@ -40,6 +40,6 @@
|
|
|
40
40
|
"build": "tsc -p tsconfig.build.json",
|
|
41
41
|
"typecheck": "tsc --noEmit --emitDeclarationOnly false && tsc -p tsconfig.test.json",
|
|
42
42
|
"test": "vitest run",
|
|
43
|
-
"test:it": "vitest run --config vitest.it.config.ts"
|
|
43
|
+
"test:it": "pnpm build && vitest run --config vitest.it.config.ts"
|
|
44
44
|
}
|
|
45
45
|
}
|