@gatebolt/cli 0.6.0 → 0.7.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 CHANGED
@@ -56,10 +56,11 @@ gatebolt init
56
56
  ```
57
57
 
58
58
  It **links** the repository (writing a committable `.gatebolt`, exactly like `gatebolt link`),
59
- **wires** your agent's declare → observe hooks — for Claude Code, appended to
60
- `.claude/settings.local.json` — and installs a **pre-push backstop** at `.git/hooks/pre-push`
61
- that reconciles an open declaration before a push. It reuses a saved profile (it never signs you
62
- in), and is safe to re-run — it never duplicates hooks.
59
+ **wires** your agent's declare → observe hooks — for Claude Code, merged into the committable
60
+ `.claude/settings.json` so enforcement travels to every clone and worktree — and installs a
61
+ **pre-push backstop** at `.git/hooks/pre-push` that reconciles an open declaration before a push.
62
+ It reuses a saved profile (it never signs you in), and is safe to re-run — it never duplicates
63
+ hooks.
63
64
 
64
65
  | Flag | Description |
65
66
  | ------------------ | ------------------------------------------------------------------------------------------------------------------- |
@@ -2,9 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { isRecord } from "../json.js";
4
4
  const CLAUDE_DIR = ".claude";
5
- const SETTINGS_FILE = "settings.local.json";
6
- const GITIGNORE_FILE = ".gitignore";
7
- const GITIGNORE_ENTRY = `${CLAUDE_DIR}/${SETTINGS_FILE}`;
5
+ const SETTINGS_FILE = "settings.json";
8
6
  const PRE_TOOL_USE = "PreToolUse";
9
7
  const STOP = "Stop";
10
8
  const EDIT_TOOLS_MATCHER = "Edit|Write|NotebookEdit";
@@ -59,13 +57,6 @@ export function mergeSettings(existing, opts) {
59
57
  }
60
58
  return { settings, changed };
61
59
  }
62
- export function withGitignoreEntry(existing, entry) {
63
- const present = existing.split("\n").some((line) => line.trim() === entry);
64
- if (present)
65
- return null;
66
- const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
67
- return `${existing}${prefix}${entry}\n`;
68
- }
69
60
  function plainObject(value, label) {
70
61
  if (value === undefined)
71
62
  return {};
@@ -116,11 +107,9 @@ async function wireClaudeCode(root, opts) {
116
107
  await mkdir(join(root, CLAUDE_DIR), { recursive: true });
117
108
  await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
118
109
  }
119
- const gitignoreChanged = await ensureGitignore(root);
120
110
  return {
121
111
  settingsPath,
122
112
  settingsChanged: changed,
123
- gitignoreChanged,
124
113
  enforce: effectiveEnforce(settings),
125
114
  };
126
115
  }
@@ -147,24 +136,6 @@ async function readSettings(path) {
147
136
  }
148
137
  return data;
149
138
  }
150
- async function ensureGitignore(root) {
151
- const path = join(root, GITIGNORE_FILE);
152
- const updated = withGitignoreEntry(await readTextOrEmpty(path), GITIGNORE_ENTRY);
153
- if (updated === null)
154
- return false;
155
- await writeFile(path, updated, "utf8");
156
- return true;
157
- }
158
- async function readTextOrEmpty(path) {
159
- try {
160
- return await readFile(path, "utf8");
161
- }
162
- catch (error) {
163
- if (isNotFound(error))
164
- return "";
165
- throw error;
166
- }
167
- }
168
139
  function safeParse(raw) {
169
140
  try {
170
141
  return JSON.parse(raw);
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { parseArgs } from "node:util";
6
6
  import { claudeCodeAdapter } from "../agents/claude-code.js";
7
7
  import { repoIdentityFromName, resolveConnection } from "../config.js";
8
- import { hooksPath, repoContext } from "../git.js";
8
+ import { hooksPath, isPathIgnored, isUncommitted, repoContext, } from "../git.js";
9
9
  import { isRecord } from "../json.js";
10
10
  import { pinRepoProfile, profileLoader, readRepoLink } from "../profiles.js";
11
11
  import { deriveIdentity, linkRepository } from "./link.js";
@@ -15,10 +15,10 @@ const PRE_PUSH_FILE = "pre-push";
15
15
  const PRE_PUSH_MARKER = "GateBolt pre-push backstop";
16
16
  const HOOK_MODE = 0o755;
17
17
  const ID_PREFIX_LEN = 8;
18
- const GITIGNORE_ENTRY = ".claude/settings.local.json";
19
18
  const NO_REMOTE = "GateBolt cannot derive this repository's identity from its `origin` remote. Re-run with --name <a-name-for-this-repo>.";
20
19
  const NO_KEY = "No GateBolt key found. Run `gatebolt login` (or `gatebolt configure`) first, then re-run `gatebolt init`.";
21
- const EPHEMERAL_CLI = "GateBolt is running from a temporary npx/dlx cache, so the hooks would break when the package manager prunes it. Install it durably first — `npm i -g @gatebolt/cli`, or add it as a project dependency so `gatebolt` is on PATH — then re-run `gatebolt init`. To link only, pass --no-hooks --no-pre-push.";
20
+ const EPHEMERAL_CLI = "GateBolt is running from a temporary npx/dlx cache, so the hooks would break when the package manager prunes it. Install it durably so `gatebolt` is on PATH on every machine — `npm i -g @gatebolt/cli` — then re-run `gatebolt init`. To link only, pass --no-hooks --no-pre-push.";
21
+ const NOT_ON_PATH = "GateBolt isn't on your PATH, so a committed hook command would only name a path that exists on this machine. Install it durably so `gatebolt` is on PATH on every machine — `npm i -g @gatebolt/cli` — then re-run `gatebolt init`. To link only, pass --no-hooks --no-pre-push.";
22
22
  export async function runInit(argv) {
23
23
  const { values } = parseArgs({
24
24
  args: argv,
@@ -47,6 +47,7 @@ export async function runInit(argv) {
47
47
  const prePush = values["no-pre-push"]
48
48
  ? null
49
49
  : await installPrePush(command, Boolean(values.force));
50
+ const settingsState = await settingsGitState(wired[0]?.result.settingsPath);
50
51
  process.stdout.write(summary({
51
52
  root: repo.root,
52
53
  link,
@@ -54,6 +55,7 @@ export async function runInit(argv) {
54
55
  prePush,
55
56
  enforce: wired[0]?.result.enforce ?? enforce ?? "lenient",
56
57
  noHooks: Boolean(values["no-hooks"]),
58
+ ...settingsState,
57
59
  }));
58
60
  }
59
61
  export function parseEnforce(value) {
@@ -94,14 +96,25 @@ async function ensureLink(connection, root, name) {
94
96
  created: result.created,
95
97
  };
96
98
  }
99
+ async function settingsGitState(settingsPath) {
100
+ if (!settingsPath) {
101
+ return { settingsIgnored: false, settingsUncommitted: false };
102
+ }
103
+ const settingsIgnored = await isPathIgnored(settingsPath);
104
+ const settingsUncommitted = settingsIgnored
105
+ ? false
106
+ : await isUncommitted(settingsPath);
107
+ return { settingsIgnored, settingsUncommitted };
108
+ }
97
109
  function resolveCommand(root) {
98
110
  const cli = fileURLToPath(new URL("../index.js", import.meta.url));
99
111
  if (isEphemeralPath(cli)) {
100
112
  throw new Error(EPHEMERAL_CLI);
101
113
  }
102
- if (onDurablePath("gatebolt", root))
103
- return "gatebolt";
104
- return `${quote(process.execPath)} ${quote(cli)}`;
114
+ if (!onDurablePath("gatebolt", root)) {
115
+ throw new Error(NOT_ON_PATH);
116
+ }
117
+ return "gatebolt";
105
118
  }
106
119
  // npx/dlx run from throwaway caches that get pruned, breaking any hook wired
107
120
  // here: npm marks them with an `_npx` segment, pnpm with `dlx`, yarn with `xfs-…`.
@@ -122,9 +135,6 @@ function isInsideRoot(dir, root) {
122
135
  const rel = relative(root, dir);
123
136
  return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
124
137
  }
125
- function quote(value) {
126
- return /\s/.test(value) ? `"${value}"` : value;
127
- }
128
138
  async function wireAgents(root, command, enforce) {
129
139
  const wired = [];
130
140
  for (const adapter of ADAPTERS) {
@@ -175,6 +185,12 @@ function summary(parts) {
175
185
  "",
176
186
  enforceLine(parts.enforce),
177
187
  ];
188
+ if (!parts.noHooks && parts.settingsIgnored) {
189
+ lines.push("Warning: .claude/settings.json is gitignored, so committing it won't work — enforcement won't travel to other clones or worktrees until you remove the ignore rule and commit it.");
190
+ }
191
+ else if (!parts.noHooks && parts.settingsUncommitted) {
192
+ lines.push("Commit .claude/settings.json so every worktree, clone, and machine enforces this policy.");
193
+ }
178
194
  if (!parts.link.existed) {
179
195
  lines.push("Commit .gatebolt so every clone and CI job resolves the same repository.");
180
196
  }
@@ -194,9 +210,6 @@ function hookLines(root, wired, noHooks) {
194
210
  for (const { displayName, result } of wired) {
195
211
  const verb = result.settingsChanged ? "wired" : "already wired";
196
212
  lines.push(`${displayName}: ${verb} → ${relative(root, result.settingsPath)}`);
197
- if (result.gitignoreChanged) {
198
- lines.push(` ignored ${GITIGNORE_ENTRY} in .gitignore`);
199
- }
200
213
  }
201
214
  return lines;
202
215
  }
package/dist/git.js CHANGED
@@ -34,6 +34,21 @@ export async function hooksPath() {
34
34
  const output = await git(["rev-parse", "--git-path", "hooks"]);
35
35
  return resolve(output.trim());
36
36
  }
37
+ export async function isPathIgnored(path) {
38
+ try {
39
+ await git(["check-ignore", "--quiet", path]);
40
+ return true;
41
+ }
42
+ catch (error) {
43
+ if (gitExitedNonZero(error))
44
+ return false;
45
+ throw gitFailure(error);
46
+ }
47
+ }
48
+ export async function isUncommitted(path) {
49
+ const output = await git(["status", "--porcelain", "--", path]);
50
+ return output.trim() !== "";
51
+ }
37
52
  export async function remoteOriginUrl() {
38
53
  try {
39
54
  const output = await git(["remote", "get-url", "origin"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gatebolt/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",