@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.
@@ -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
- const loadProfile = profileLoader({ name: values.profile, root: repo.root });
43
- const connection = await resolveConnection({ key: values.key, url: values.url }, loadProfile);
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
- function printVerdict(result) {
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.GBOLT_KEY;
4
- const url = flags.url ?? process.env.GBOLT_URL;
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 GBOLT_KEY, pass --key, or run `gatebolt configure`.");
9
+ throw new Error("Missing API key. Set GATEBOLT_KEY, pass --key, or run `gatebolt configure`.");
10
10
  }
11
- return { key: resolvedKey, baseUrl };
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
- export async function resolveRepoSlug(flags, loadProfile) {
14
- const raw = flags.repo ?? process.env.GBOLT_REPO ?? (await loadProfile())?.repo;
15
- if (!raw) {
16
- throw new Error("Missing repository slug. Set GBOLT_REPO, pass --repo, or run `gatebolt configure`.");
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
- export function slugifyRepo(value) {
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(`malformed numstat record: ${head}`);
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(`unsupported git status: ${status}`);
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("unexpected end of git output");
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 { RequestError } from "./transport.js";
6
- const STATUS_BAD_REQUEST = 400;
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
- switch (command) {
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 formatError(error) {
36
- if (error instanceof RequestError) {
37
- return messageForStatus(error);
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 (error instanceof Error) {
40
- return error.message;
28
+ if (command === "--version") {
29
+ process.stdout.write(`${await cliVersion()}\n`);
30
+ return;
41
31
  }
42
- return "Unexpected error.";
43
- }
44
- function messageForStatus(error) {
45
- switch (error.status) {
46
- case STATUS_BAD_REQUEST:
47
- return "Invalid request — check the declared values and try again.";
48
- case STATUS_UNAUTHORIZED:
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.";
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
- return error.message;
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();