@great-arrow/cli 0.1.8 → 0.1.10
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 +23 -0
- package/dist/index.js +1321 -63
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -5253,7 +5253,7 @@ var require_minimist = __commonJS({
|
|
|
5253
5253
|
var require_rc = __commonJS({
|
|
5254
5254
|
"node_modules/.pnpm/rc@1.2.8/node_modules/rc/index.js"(exports, module) {
|
|
5255
5255
|
var cc = require_utils();
|
|
5256
|
-
var
|
|
5256
|
+
var join5 = __require("path").join;
|
|
5257
5257
|
var deepExtend = require_deep_extend();
|
|
5258
5258
|
var etc = "/etc";
|
|
5259
5259
|
var win = process.platform === "win32";
|
|
@@ -5278,15 +5278,15 @@ var require_rc = __commonJS({
|
|
|
5278
5278
|
}
|
|
5279
5279
|
if (!win)
|
|
5280
5280
|
[
|
|
5281
|
-
|
|
5282
|
-
|
|
5281
|
+
join5(etc, name, "config"),
|
|
5282
|
+
join5(etc, name + "rc")
|
|
5283
5283
|
].forEach(addConfigFile);
|
|
5284
5284
|
if (home)
|
|
5285
5285
|
[
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5286
|
+
join5(home, ".config", name, "config"),
|
|
5287
|
+
join5(home, ".config", name),
|
|
5288
|
+
join5(home, "." + name, "config"),
|
|
5289
|
+
join5(home, "." + name + "rc")
|
|
5290
5290
|
].forEach(addConfigFile);
|
|
5291
5291
|
addConfigFile(cc.find("." + name + "rc"));
|
|
5292
5292
|
if (env4.config) addConfigFile(env4.config);
|
|
@@ -6624,11 +6624,11 @@ var require_util = __commonJS({
|
|
|
6624
6624
|
if (files.includes("node_modules") || files.includes("package.json") || files.includes("package.json5") || files.includes("package.yaml") || files.includes("pnpm-workspace.yaml")) {
|
|
6625
6625
|
return name2;
|
|
6626
6626
|
}
|
|
6627
|
-
const
|
|
6628
|
-
if (
|
|
6627
|
+
const dirname4 = path9.dirname(name2);
|
|
6628
|
+
if (dirname4 === name2) {
|
|
6629
6629
|
return original;
|
|
6630
6630
|
}
|
|
6631
|
-
return find(
|
|
6631
|
+
return find(dirname4, original);
|
|
6632
6632
|
} catch (error) {
|
|
6633
6633
|
if (name2 === original) {
|
|
6634
6634
|
if (error.code === "ENOENT") {
|
|
@@ -9278,6 +9278,13 @@ function networkError(message, hint) {
|
|
|
9278
9278
|
hint ?? "Check your internet connection or run 'gad doctor'."
|
|
9279
9279
|
);
|
|
9280
9280
|
}
|
|
9281
|
+
function validationError(message, hint) {
|
|
9282
|
+
return new CliError(
|
|
9283
|
+
message,
|
|
9284
|
+
3 /* VALIDATION_ERROR */,
|
|
9285
|
+
hint ?? "Check the command usage with --help."
|
|
9286
|
+
);
|
|
9287
|
+
}
|
|
9281
9288
|
function mapStatusToExitCode(status) {
|
|
9282
9289
|
if (status === 401 || status === 403) {
|
|
9283
9290
|
return 1 /* AUTH_ERROR */;
|
|
@@ -9302,8 +9309,9 @@ function isNodeError(value) {
|
|
|
9302
9309
|
|
|
9303
9310
|
// src/lib/version.ts
|
|
9304
9311
|
var CLI_PACKAGE_NAME = "@great-arrow/cli";
|
|
9305
|
-
var CLI_VERSION = "0.1.
|
|
9306
|
-
var GAD_INSTALL_VERSION = "2026.06
|
|
9312
|
+
var CLI_VERSION = "0.1.10";
|
|
9313
|
+
var GAD_INSTALL_VERSION = "2026.07.06";
|
|
9314
|
+
var GAD_INSTALL_MIN_VERSION = "2026.06.22";
|
|
9307
9315
|
var GAD_INSTALL_HEADER = "X-GAD-Install-Version";
|
|
9308
9316
|
var GAD_CLI_VERSION_HEADER = "X-GAD-CLI-Version";
|
|
9309
9317
|
|
|
@@ -9543,12 +9551,12 @@ h1{font-size:1.25rem;margin:0 0 .5rem}p{color:#9a9aa3;margin:0}</style></head>
|
|
|
9543
9551
|
<body><div class="card"><h1>${ok ? "\u2713 Signed in to Great Arrow" : "\u2717 Sign-in failed"}</h1>
|
|
9544
9552
|
<p>${escapeHtml(detail)}</p></div></body></html>`;
|
|
9545
9553
|
function listen(port) {
|
|
9546
|
-
return new Promise((
|
|
9554
|
+
return new Promise((resolve3, reject) => {
|
|
9547
9555
|
const server = createServer();
|
|
9548
9556
|
server.on("error", reject);
|
|
9549
9557
|
server.listen(port, LOOPBACK_HOST, () => {
|
|
9550
9558
|
const addr = server.address();
|
|
9551
|
-
|
|
9559
|
+
resolve3({ server, port: addr.port });
|
|
9552
9560
|
});
|
|
9553
9561
|
});
|
|
9554
9562
|
}
|
|
@@ -9573,7 +9581,7 @@ async function startLoopbackServer(opts) {
|
|
|
9573
9581
|
port,
|
|
9574
9582
|
close: () => server.close(),
|
|
9575
9583
|
waitForCallback({ timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
9576
|
-
return new Promise((
|
|
9584
|
+
return new Promise((resolve3, reject) => {
|
|
9577
9585
|
const timer = setTimeout(() => {
|
|
9578
9586
|
server.close();
|
|
9579
9587
|
reject(new Error("Timed out waiting for the browser callback."));
|
|
@@ -9613,7 +9621,7 @@ async function startLoopbackServer(opts) {
|
|
|
9613
9621
|
"You can close this tab and return to your terminal."
|
|
9614
9622
|
)
|
|
9615
9623
|
);
|
|
9616
|
-
settle(() =>
|
|
9624
|
+
settle(() => resolve3({ code, state }));
|
|
9617
9625
|
});
|
|
9618
9626
|
});
|
|
9619
9627
|
}
|
|
@@ -9867,11 +9875,11 @@ async function confirm(message) {
|
|
|
9867
9875
|
input: process.stdin,
|
|
9868
9876
|
output: process.stdout
|
|
9869
9877
|
});
|
|
9870
|
-
return new Promise((
|
|
9878
|
+
return new Promise((resolve3) => {
|
|
9871
9879
|
rl.question(`${message} [y/N] `, (answer) => {
|
|
9872
9880
|
rl.close();
|
|
9873
9881
|
const normalized = answer.trim().toLowerCase();
|
|
9874
|
-
|
|
9882
|
+
resolve3(normalized === "y" || normalized === "yes");
|
|
9875
9883
|
});
|
|
9876
9884
|
});
|
|
9877
9885
|
}
|
|
@@ -10038,10 +10046,10 @@ function maskToken(token) {
|
|
|
10038
10046
|
}
|
|
10039
10047
|
function openBrowser(url) {
|
|
10040
10048
|
if (!/^https?:\/\//i.test(url)) return Promise.resolve(false);
|
|
10041
|
-
return new Promise((
|
|
10049
|
+
return new Promise((resolve3) => {
|
|
10042
10050
|
const platform = process.platform;
|
|
10043
10051
|
const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
10044
|
-
execFile(cmd, args, (error) =>
|
|
10052
|
+
execFile(cmd, args, (error) => resolve3(!error));
|
|
10045
10053
|
});
|
|
10046
10054
|
}
|
|
10047
10055
|
var loginCommand = new Command("login").description("Authenticate the CLI with your Great Arrow Digital account").option(
|
|
@@ -10314,7 +10322,7 @@ function describeError(error) {
|
|
|
10314
10322
|
return String(error);
|
|
10315
10323
|
}
|
|
10316
10324
|
function sleep(ms) {
|
|
10317
|
-
return new Promise((
|
|
10325
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
10318
10326
|
}
|
|
10319
10327
|
var GadClient = class {
|
|
10320
10328
|
token;
|
|
@@ -10603,10 +10611,10 @@ function whoamiCommand() {
|
|
|
10603
10611
|
|
|
10604
10612
|
// src/lib/stdin.ts
|
|
10605
10613
|
function readStream(stream) {
|
|
10606
|
-
return new Promise((
|
|
10614
|
+
return new Promise((resolve3, reject) => {
|
|
10607
10615
|
const chunks = [];
|
|
10608
10616
|
stream.on("data", (chunk) => chunks.push(chunk));
|
|
10609
|
-
stream.on("end", () =>
|
|
10617
|
+
stream.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
|
|
10610
10618
|
stream.on("error", reject);
|
|
10611
10619
|
});
|
|
10612
10620
|
}
|
|
@@ -11228,14 +11236,928 @@ var claimCommand = new Command("claim").description("Preflight + atomically clai
|
|
|
11228
11236
|
);
|
|
11229
11237
|
var claim_default = claimCommand;
|
|
11230
11238
|
|
|
11239
|
+
// src/commands/work.ts
|
|
11240
|
+
import { readFileSync } from "node:fs";
|
|
11241
|
+
|
|
11242
|
+
// src/lib/work-status.ts
|
|
11243
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
11244
|
+
function run(command, args, cwd) {
|
|
11245
|
+
try {
|
|
11246
|
+
const stdout = execFileSync2(command, args, {
|
|
11247
|
+
cwd,
|
|
11248
|
+
encoding: "utf8",
|
|
11249
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
11250
|
+
maxBuffer: 10 * 1024 * 1024
|
|
11251
|
+
});
|
|
11252
|
+
return { status: 0, stdout, stderr: "" };
|
|
11253
|
+
} catch (err) {
|
|
11254
|
+
const e = err;
|
|
11255
|
+
return {
|
|
11256
|
+
status: e.status ?? 1,
|
|
11257
|
+
stdout: e.stdout ?? "",
|
|
11258
|
+
stderr: e.stderr ?? ""
|
|
11259
|
+
};
|
|
11260
|
+
}
|
|
11261
|
+
}
|
|
11262
|
+
function git(args, cwd) {
|
|
11263
|
+
return run("git", args, cwd);
|
|
11264
|
+
}
|
|
11265
|
+
function requireGit(args, cwd) {
|
|
11266
|
+
const result = git(args, cwd);
|
|
11267
|
+
if (result.status !== 0) {
|
|
11268
|
+
throw new Error(
|
|
11269
|
+
`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`
|
|
11270
|
+
);
|
|
11271
|
+
}
|
|
11272
|
+
return result.stdout.trim();
|
|
11273
|
+
}
|
|
11274
|
+
function isAncestor(ancestor, ref, cwd) {
|
|
11275
|
+
return git(["merge-base", "--is-ancestor", ancestor, ref], cwd).status === 0;
|
|
11276
|
+
}
|
|
11277
|
+
function revListCounts(head, cwd) {
|
|
11278
|
+
const result = git(
|
|
11279
|
+
["rev-list", "--left-right", "--count", `origin/main...${head}`],
|
|
11280
|
+
cwd
|
|
11281
|
+
);
|
|
11282
|
+
if (result.status !== 0) return { behindMain: 0, aheadMain: 0 };
|
|
11283
|
+
const [behindRaw, aheadRaw] = result.stdout.trim().split(/\s+/);
|
|
11284
|
+
const behind = Number(behindRaw ?? 0);
|
|
11285
|
+
const ahead = Number(aheadRaw ?? 0);
|
|
11286
|
+
return {
|
|
11287
|
+
behindMain: Number.isFinite(behind) ? behind : 0,
|
|
11288
|
+
aheadMain: Number.isFinite(ahead) ? ahead : 0
|
|
11289
|
+
};
|
|
11290
|
+
}
|
|
11291
|
+
function parseWorktrees(raw) {
|
|
11292
|
+
const worktrees = [];
|
|
11293
|
+
let current = {};
|
|
11294
|
+
function flush() {
|
|
11295
|
+
if (current.path && current.head) {
|
|
11296
|
+
worktrees.push({
|
|
11297
|
+
path: current.path,
|
|
11298
|
+
branch: current.branch ?? "(detached)",
|
|
11299
|
+
head: current.head
|
|
11300
|
+
});
|
|
11301
|
+
}
|
|
11302
|
+
current = {};
|
|
11303
|
+
}
|
|
11304
|
+
for (const line of raw.split("\n")) {
|
|
11305
|
+
if (line === "") {
|
|
11306
|
+
flush();
|
|
11307
|
+
} else if (line.startsWith("worktree ")) {
|
|
11308
|
+
current.path = line.slice("worktree ".length);
|
|
11309
|
+
} else if (line.startsWith("HEAD ")) {
|
|
11310
|
+
current.head = line.slice("HEAD ".length);
|
|
11311
|
+
} else if (line.startsWith("branch refs/heads/")) {
|
|
11312
|
+
current.branch = line.slice("branch refs/heads/".length);
|
|
11313
|
+
} else if (line === "detached") {
|
|
11314
|
+
current.branch = "(detached)";
|
|
11315
|
+
}
|
|
11316
|
+
}
|
|
11317
|
+
flush();
|
|
11318
|
+
return worktrees;
|
|
11319
|
+
}
|
|
11320
|
+
function classifyWorktree(audit) {
|
|
11321
|
+
if (audit.path.includes("/.claude/worktrees/")) return "agent-harness";
|
|
11322
|
+
if (audit.dirty) return "review-dirty";
|
|
11323
|
+
if (audit.mergedToMain) return "cleanup-ready";
|
|
11324
|
+
if (audit.upstreamGone) return "upstream-gone";
|
|
11325
|
+
if (!audit.upstream) return "local-rescue";
|
|
11326
|
+
return "active-unmerged";
|
|
11327
|
+
}
|
|
11328
|
+
function auditWorktrees(root) {
|
|
11329
|
+
const raw = requireGit(["worktree", "list", "--porcelain"], root);
|
|
11330
|
+
return parseWorktrees(raw).map((worktree) => {
|
|
11331
|
+
const status = git(["status", "--porcelain"], worktree.path).stdout.trim();
|
|
11332
|
+
const branchStatus = git(["status", "--short", "--branch"], worktree.path).stdout.split("\n")[0]?.trim();
|
|
11333
|
+
const upstream = git(
|
|
11334
|
+
["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
|
|
11335
|
+
worktree.path
|
|
11336
|
+
);
|
|
11337
|
+
const counts = revListCounts(worktree.head, root);
|
|
11338
|
+
const baseAudit = {
|
|
11339
|
+
path: worktree.path,
|
|
11340
|
+
branch: worktree.branch,
|
|
11341
|
+
head: worktree.head,
|
|
11342
|
+
dirty: status.length > 0,
|
|
11343
|
+
mergedToMain: isAncestor(worktree.head, "origin/main", root),
|
|
11344
|
+
upstream: upstream.status === 0 ? upstream.stdout.trim() : null,
|
|
11345
|
+
upstreamGone: branchStatus?.includes("[gone]") ?? false,
|
|
11346
|
+
...counts
|
|
11347
|
+
};
|
|
11348
|
+
return { ...baseAudit, classification: classifyWorktree(baseAudit) };
|
|
11349
|
+
});
|
|
11350
|
+
}
|
|
11351
|
+
function auditDetachedLocalBranches(root, checkedOutBranches) {
|
|
11352
|
+
const raw = requireGit(
|
|
11353
|
+
["for-each-ref", "--format=%(refname:short)", "refs/heads"],
|
|
11354
|
+
root
|
|
11355
|
+
);
|
|
11356
|
+
return raw.split("\n").map((branch) => branch.trim()).filter(Boolean).filter((branch) => !checkedOutBranches.has(branch)).map((branch) => {
|
|
11357
|
+
const head = requireGit(["rev-parse", branch], root);
|
|
11358
|
+
const counts = revListCounts(head, root);
|
|
11359
|
+
return {
|
|
11360
|
+
branch,
|
|
11361
|
+
head,
|
|
11362
|
+
mergedToMain: isAncestor(head, "origin/main", root),
|
|
11363
|
+
...counts
|
|
11364
|
+
};
|
|
11365
|
+
});
|
|
11366
|
+
}
|
|
11367
|
+
function readOpenPullRequests(root) {
|
|
11368
|
+
const result = run(
|
|
11369
|
+
"gh",
|
|
11370
|
+
[
|
|
11371
|
+
"pr",
|
|
11372
|
+
"list",
|
|
11373
|
+
"--state",
|
|
11374
|
+
"open",
|
|
11375
|
+
"--limit",
|
|
11376
|
+
"50",
|
|
11377
|
+
"--json",
|
|
11378
|
+
"number,title,headRefName,mergeStateStatus,isDraft,url"
|
|
11379
|
+
],
|
|
11380
|
+
root
|
|
11381
|
+
);
|
|
11382
|
+
if (result.status !== 0) return [];
|
|
11383
|
+
try {
|
|
11384
|
+
return JSON.parse(result.stdout);
|
|
11385
|
+
} catch {
|
|
11386
|
+
return [];
|
|
11387
|
+
}
|
|
11388
|
+
}
|
|
11389
|
+
async function readProductionVersion(apiUrl) {
|
|
11390
|
+
const controller = new AbortController();
|
|
11391
|
+
const timeout2 = setTimeout(() => controller.abort(), 8e3);
|
|
11392
|
+
try {
|
|
11393
|
+
const response = await fetch(`${apiUrl.replace(/\/$/, "")}/api/version`, {
|
|
11394
|
+
signal: controller.signal
|
|
11395
|
+
});
|
|
11396
|
+
if (!response.ok) return null;
|
|
11397
|
+
return await response.json();
|
|
11398
|
+
} catch {
|
|
11399
|
+
return null;
|
|
11400
|
+
} finally {
|
|
11401
|
+
clearTimeout(timeout2);
|
|
11402
|
+
}
|
|
11403
|
+
}
|
|
11404
|
+
function countBy(values) {
|
|
11405
|
+
return values.reduce(
|
|
11406
|
+
(counts, value) => {
|
|
11407
|
+
counts[value] = (counts[value] ?? 0) + 1;
|
|
11408
|
+
return counts;
|
|
11409
|
+
},
|
|
11410
|
+
{}
|
|
11411
|
+
);
|
|
11412
|
+
}
|
|
11413
|
+
async function collectWorkStatus(opts) {
|
|
11414
|
+
const root = requireGit(["rev-parse", "--show-toplevel"]);
|
|
11415
|
+
if (opts.fetchRemote !== false) {
|
|
11416
|
+
git(["fetch", "origin", "main", "--prune"], root);
|
|
11417
|
+
}
|
|
11418
|
+
const originMain = requireGit(["rev-parse", "origin/main"], root);
|
|
11419
|
+
const originMainSubject = requireGit(
|
|
11420
|
+
["log", "-1", "--pretty=%s", "origin/main"],
|
|
11421
|
+
root
|
|
11422
|
+
);
|
|
11423
|
+
const currentBranch = requireGit(["branch", "--show-current"], root);
|
|
11424
|
+
const currentHead = requireGit(["rev-parse", "HEAD"], root);
|
|
11425
|
+
const currentClean = git(["status", "--porcelain"], root).stdout.trim().length === 0;
|
|
11426
|
+
const stashes = git(["stash", "list"], root).stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
11427
|
+
const worktrees = auditWorktrees(root);
|
|
11428
|
+
const checkedOutBranches = new Set(
|
|
11429
|
+
worktrees.map((worktree) => worktree.branch).filter((branch) => branch !== "(detached)")
|
|
11430
|
+
);
|
|
11431
|
+
const detachedBranches = auditDetachedLocalBranches(root, checkedOutBranches);
|
|
11432
|
+
const openPullRequests = readOpenPullRequests(root);
|
|
11433
|
+
const production = opts.includeProduction === false ? null : await readProductionVersion(opts.apiUrl);
|
|
11434
|
+
const productionSha = production?.sha ?? null;
|
|
11435
|
+
const productionContainedByMain = productionSha ? isAncestor(productionSha, "origin/main", root) : null;
|
|
11436
|
+
const productionAtMain = productionSha === originMain;
|
|
11437
|
+
const detachedUnmerged = detachedBranches.filter(
|
|
11438
|
+
(branch) => !branch.mergedToMain
|
|
11439
|
+
);
|
|
11440
|
+
const blockedPrs = openPullRequests.filter(
|
|
11441
|
+
(pr) => pr.isDraft || !["CLEAN", "HAS_HOOKS"].includes(pr.mergeStateStatus)
|
|
11442
|
+
);
|
|
11443
|
+
return {
|
|
11444
|
+
root,
|
|
11445
|
+
current: {
|
|
11446
|
+
branch: currentBranch,
|
|
11447
|
+
head: currentHead,
|
|
11448
|
+
clean: currentClean,
|
|
11449
|
+
containedByMain: isAncestor(currentHead, "origin/main", root)
|
|
11450
|
+
},
|
|
11451
|
+
originMain: { sha: originMain, subject: originMainSubject },
|
|
11452
|
+
production: production ? {
|
|
11453
|
+
...production,
|
|
11454
|
+
containedByMain: productionContainedByMain,
|
|
11455
|
+
atOriginMain: productionAtMain
|
|
11456
|
+
} : null,
|
|
11457
|
+
summary: {
|
|
11458
|
+
worktrees: worktrees.length,
|
|
11459
|
+
worktreeClassifications: countBy(
|
|
11460
|
+
worktrees.map((worktree) => worktree.classification)
|
|
11461
|
+
),
|
|
11462
|
+
detachedLocalBranches: detachedBranches.length,
|
|
11463
|
+
detachedUnmergedBranches: detachedUnmerged.length,
|
|
11464
|
+
stashes: stashes.length,
|
|
11465
|
+
openPullRequests: openPullRequests.length,
|
|
11466
|
+
blockedPullRequests: blockedPrs.length
|
|
11467
|
+
},
|
|
11468
|
+
worktrees,
|
|
11469
|
+
detachedBranches,
|
|
11470
|
+
openPullRequests,
|
|
11471
|
+
strictFailures: []
|
|
11472
|
+
};
|
|
11473
|
+
}
|
|
11474
|
+
|
|
11475
|
+
// src/commands/work.ts
|
|
11476
|
+
function writeJson(value) {
|
|
11477
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
11478
|
+
`);
|
|
11479
|
+
}
|
|
11480
|
+
function shortSha(value) {
|
|
11481
|
+
return typeof value === "string" ? value.slice(0, 12) : "";
|
|
11482
|
+
}
|
|
11483
|
+
function collectOptions(opts) {
|
|
11484
|
+
const result = { apiUrl: opts.apiUrl };
|
|
11485
|
+
if (opts.fetch !== void 0) result.fetchRemote = opts.fetch;
|
|
11486
|
+
if (opts.production !== void 0) result.includeProduction = opts.production;
|
|
11487
|
+
return result;
|
|
11488
|
+
}
|
|
11489
|
+
function workCommand() {
|
|
11490
|
+
const cmd = new Command("work").description(
|
|
11491
|
+
"Inspect and publish unshipped work / work-at-risk state"
|
|
11492
|
+
);
|
|
11493
|
+
cmd.command("status").description("Show canonical work-at-risk state for the active workspace").option("--local", "Run a local git/worktree audit instead of app state").option("--no-fetch", "Skip git fetch when using --local").option(
|
|
11494
|
+
"--no-production",
|
|
11495
|
+
"Skip production version probe when using --local"
|
|
11496
|
+
).option("--limit <n>", "Maximum app rows to show", "20").action(
|
|
11497
|
+
async (options, actionCmd) => {
|
|
11498
|
+
const output = globalOutputOpts(actionCmd);
|
|
11499
|
+
const fmt = createFormatter({
|
|
11500
|
+
...output,
|
|
11501
|
+
isTTY: isTTY()
|
|
11502
|
+
});
|
|
11503
|
+
const auth = await resolveAuth();
|
|
11504
|
+
if (options.local) {
|
|
11505
|
+
const report = await collectWorkStatus({
|
|
11506
|
+
...collectOptions({
|
|
11507
|
+
apiUrl: auth.apiUrl,
|
|
11508
|
+
fetch: options.fetch,
|
|
11509
|
+
production: options.production
|
|
11510
|
+
})
|
|
11511
|
+
});
|
|
11512
|
+
if (output.json) return writeJson(report);
|
|
11513
|
+
fmt.detail(
|
|
11514
|
+
{
|
|
11515
|
+
current: `${report.current.branch || "(detached)"} @ ${shortSha(
|
|
11516
|
+
report.current.head
|
|
11517
|
+
)}`,
|
|
11518
|
+
clean: report.current.clean,
|
|
11519
|
+
worktrees: report.summary.worktrees,
|
|
11520
|
+
dirty: report.summary.worktreeClassifications["review-dirty"] ?? 0,
|
|
11521
|
+
activeUnmerged: report.summary.worktreeClassifications["active-unmerged"] ?? 0,
|
|
11522
|
+
detachedUnmerged: report.summary.detachedUnmergedBranches,
|
|
11523
|
+
blockedPrs: report.summary.blockedPullRequests,
|
|
11524
|
+
stashes: report.summary.stashes
|
|
11525
|
+
},
|
|
11526
|
+
[
|
|
11527
|
+
{ key: "current", header: "Current" },
|
|
11528
|
+
{ key: "clean", header: "Clean" },
|
|
11529
|
+
{ key: "worktrees", header: "Worktrees" },
|
|
11530
|
+
{ key: "dirty", header: "Dirty" },
|
|
11531
|
+
{ key: "activeUnmerged", header: "Unmerged" },
|
|
11532
|
+
{ key: "detachedUnmerged", header: "Detached" },
|
|
11533
|
+
{ key: "blockedPrs", header: "Blocked PRs" },
|
|
11534
|
+
{ key: "stashes", header: "Stashes" }
|
|
11535
|
+
]
|
|
11536
|
+
);
|
|
11537
|
+
return;
|
|
11538
|
+
}
|
|
11539
|
+
const client = new GadClient({
|
|
11540
|
+
token: auth.token,
|
|
11541
|
+
apiUrl: auth.apiUrl
|
|
11542
|
+
});
|
|
11543
|
+
const data = await client.get("/api/work/status", {
|
|
11544
|
+
limit: String(Number.parseInt(options.limit ?? "20", 10) || 20)
|
|
11545
|
+
});
|
|
11546
|
+
if (output.json) return writeJson(data);
|
|
11547
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
11548
|
+
fmt.table(items, [
|
|
11549
|
+
{ key: "riskLevel", header: "Risk" },
|
|
11550
|
+
{ key: "title", header: "Item" },
|
|
11551
|
+
{ key: "recommendedAction", header: "Action" }
|
|
11552
|
+
]);
|
|
11553
|
+
}
|
|
11554
|
+
);
|
|
11555
|
+
cmd.command("publish").description("Publish the local release audit into the active workspace").option("--file <path>", "Publish a precomputed release-audit JSON file").option("--no-fetch", "Skip git fetch before collecting local status").option("--no-production", "Skip production version probe").option("--machine-id <id>", "Stable machine/session identifier").action(
|
|
11556
|
+
async (options, actionCmd) => {
|
|
11557
|
+
const output = globalOutputOpts(actionCmd);
|
|
11558
|
+
const fmt = createFormatter({
|
|
11559
|
+
...output,
|
|
11560
|
+
isTTY: isTTY()
|
|
11561
|
+
});
|
|
11562
|
+
const auth = await resolveAuth();
|
|
11563
|
+
const client = new GadClient({
|
|
11564
|
+
token: auth.token,
|
|
11565
|
+
apiUrl: auth.apiUrl
|
|
11566
|
+
});
|
|
11567
|
+
const report = options.file ? JSON.parse(readFileSync(options.file, "utf8")) : await collectWorkStatus({
|
|
11568
|
+
...collectOptions({
|
|
11569
|
+
apiUrl: auth.apiUrl,
|
|
11570
|
+
fetch: options.fetch,
|
|
11571
|
+
production: options.production
|
|
11572
|
+
})
|
|
11573
|
+
});
|
|
11574
|
+
const result = await client.post("/api/work/publish", {
|
|
11575
|
+
schemaVersion: 1,
|
|
11576
|
+
kind: "release-logjam",
|
|
11577
|
+
clientSource: "gad-cli",
|
|
11578
|
+
machineId: options.machineId,
|
|
11579
|
+
report
|
|
11580
|
+
});
|
|
11581
|
+
if (output.json) return writeJson(result);
|
|
11582
|
+
const r = result;
|
|
11583
|
+
fmt.success(
|
|
11584
|
+
`Published work-at-risk snapshot (${r.itemCount ?? 0} item${r.itemCount === 1 ? "" : "s"}, ${r.resolvedCount ?? 0} resolved).`
|
|
11585
|
+
);
|
|
11586
|
+
if (Array.isArray(r.topItems) && r.topItems.length > 0) {
|
|
11587
|
+
fmt.table(r.topItems, [
|
|
11588
|
+
{ key: "riskLevel", header: "Risk" },
|
|
11589
|
+
{ key: "title", header: "Item" },
|
|
11590
|
+
{ key: "recommendedAction", header: "Action" }
|
|
11591
|
+
]);
|
|
11592
|
+
}
|
|
11593
|
+
}
|
|
11594
|
+
);
|
|
11595
|
+
return cmd;
|
|
11596
|
+
}
|
|
11597
|
+
var work_default = workCommand;
|
|
11598
|
+
|
|
11599
|
+
// src/commands/worktree.ts
|
|
11600
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
11601
|
+
import { existsSync, readFileSync as readFileSync2, statSync, writeFileSync } from "node:fs";
|
|
11602
|
+
import { basename, dirname as dirname2, isAbsolute, join as join2, resolve } from "node:path";
|
|
11603
|
+
var realExec = (cmd, args, opts) => {
|
|
11604
|
+
const out = execFileSync3(cmd, args, {
|
|
11605
|
+
cwd: opts?.cwd,
|
|
11606
|
+
encoding: "utf8",
|
|
11607
|
+
stdio: opts?.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"]
|
|
11608
|
+
});
|
|
11609
|
+
return typeof out === "string" ? out : "";
|
|
11610
|
+
};
|
|
11611
|
+
function assertCleanTree(statusPorcelain) {
|
|
11612
|
+
if (statusPorcelain.trim().length === 0) return;
|
|
11613
|
+
throw validationError(
|
|
11614
|
+
"Working tree is dirty \u2014 commit, stash, or clean up uncommitted changes first.",
|
|
11615
|
+
"A new task worktree must start from a clean state so another task's in-flight files can't leak into your first commit."
|
|
11616
|
+
);
|
|
11617
|
+
}
|
|
11618
|
+
function parseDefaultBranch(symbolicRef) {
|
|
11619
|
+
if (!symbolicRef) return null;
|
|
11620
|
+
const match = symbolicRef.trim().match(/^refs\/remotes\/origin\/(.+)$/);
|
|
11621
|
+
return match ? match[1] : null;
|
|
11622
|
+
}
|
|
11623
|
+
function detectPackageManager(repoRoot) {
|
|
11624
|
+
if (existsSync(join2(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
11625
|
+
if (existsSync(join2(repoRoot, "bun.lockb"))) return "bun";
|
|
11626
|
+
if (existsSync(join2(repoRoot, "yarn.lock"))) return "yarn";
|
|
11627
|
+
return "npm";
|
|
11628
|
+
}
|
|
11629
|
+
function defaultWorktreeDir(repoRoot, branch) {
|
|
11630
|
+
const slug = (branch.split("/").pop() ?? branch).replace(
|
|
11631
|
+
/[^A-Za-z0-9._-]+/g,
|
|
11632
|
+
"-"
|
|
11633
|
+
);
|
|
11634
|
+
return join2(dirname2(repoRoot), `${basename(repoRoot)}-${slug}`);
|
|
11635
|
+
}
|
|
11636
|
+
var AGENT_PROCESS_RE = /(^|\/)(claude|kiro|cursor|codex)/i;
|
|
11637
|
+
function psField(pid, field) {
|
|
11638
|
+
try {
|
|
11639
|
+
return execFileSync3("ps", ["-o", `${field}=`, "-p", String(pid)], {
|
|
11640
|
+
encoding: "utf8"
|
|
11641
|
+
}).trim();
|
|
11642
|
+
} catch {
|
|
11643
|
+
return "";
|
|
11644
|
+
}
|
|
11645
|
+
}
|
|
11646
|
+
function findAgentPid(startPid) {
|
|
11647
|
+
let pid = Number(startPid);
|
|
11648
|
+
for (let i = 0; i < 40 && pid > 1; i++) {
|
|
11649
|
+
if (AGENT_PROCESS_RE.test(basename(psField(pid, "comm")))) return pid;
|
|
11650
|
+
const ppid = Number(psField(pid, "ppid"));
|
|
11651
|
+
if (!ppid || ppid === pid) break;
|
|
11652
|
+
pid = ppid;
|
|
11653
|
+
}
|
|
11654
|
+
return null;
|
|
11655
|
+
}
|
|
11656
|
+
function ownGitDir(worktreeDir) {
|
|
11657
|
+
const dotGit = join2(worktreeDir, ".git");
|
|
11658
|
+
if (!existsSync(dotGit)) return null;
|
|
11659
|
+
if (statSync(dotGit).isDirectory()) return dotGit;
|
|
11660
|
+
const match = readFileSync2(dotGit, "utf8").trim().match(/^gitdir:\s*(.+)$/m);
|
|
11661
|
+
if (!match) return null;
|
|
11662
|
+
return isAbsolute(match[1]) ? match[1] : resolve(worktreeDir, match[1]);
|
|
11663
|
+
}
|
|
11664
|
+
function stampWorktreeOwner(worktreeDir) {
|
|
11665
|
+
try {
|
|
11666
|
+
if (process.platform === "win32") return null;
|
|
11667
|
+
const gitDir = ownGitDir(worktreeDir);
|
|
11668
|
+
if (!gitDir) return null;
|
|
11669
|
+
const ownerPid = findAgentPid(process.ppid);
|
|
11670
|
+
if (!ownerPid) return null;
|
|
11671
|
+
writeFileSync(
|
|
11672
|
+
join2(gitDir, "agent-owner.json"),
|
|
11673
|
+
JSON.stringify(
|
|
11674
|
+
{
|
|
11675
|
+
ownerPid,
|
|
11676
|
+
comm: basename(psField(ownerPid, "comm")),
|
|
11677
|
+
stampedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11678
|
+
},
|
|
11679
|
+
null,
|
|
11680
|
+
2
|
|
11681
|
+
)
|
|
11682
|
+
);
|
|
11683
|
+
return ownerPid;
|
|
11684
|
+
} catch {
|
|
11685
|
+
return null;
|
|
11686
|
+
}
|
|
11687
|
+
}
|
|
11688
|
+
function createTaskWorktree(opts) {
|
|
11689
|
+
const exec2 = opts.exec ?? realExec;
|
|
11690
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
11691
|
+
const { branch } = opts;
|
|
11692
|
+
let repoRoot;
|
|
11693
|
+
try {
|
|
11694
|
+
repoRoot = exec2("git", ["rev-parse", "--show-toplevel"], { cwd }).trim();
|
|
11695
|
+
} catch {
|
|
11696
|
+
throw validationError(
|
|
11697
|
+
"Not inside a git repository.",
|
|
11698
|
+
"Run this from the repo you want a task worktree for."
|
|
11699
|
+
);
|
|
11700
|
+
}
|
|
11701
|
+
assertCleanTree(exec2("git", ["status", "--porcelain"], { cwd: repoRoot }));
|
|
11702
|
+
let fetched = true;
|
|
11703
|
+
try {
|
|
11704
|
+
exec2("git", ["fetch", "origin", "--quiet"], { cwd: repoRoot });
|
|
11705
|
+
} catch {
|
|
11706
|
+
fetched = false;
|
|
11707
|
+
}
|
|
11708
|
+
let base = null;
|
|
11709
|
+
try {
|
|
11710
|
+
base = parseDefaultBranch(
|
|
11711
|
+
exec2("git", ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], {
|
|
11712
|
+
cwd: repoRoot
|
|
11713
|
+
})
|
|
11714
|
+
);
|
|
11715
|
+
} catch {
|
|
11716
|
+
base = null;
|
|
11717
|
+
}
|
|
11718
|
+
if (!base) {
|
|
11719
|
+
for (const candidate of ["main", "master"]) {
|
|
11720
|
+
try {
|
|
11721
|
+
exec2(
|
|
11722
|
+
"git",
|
|
11723
|
+
[
|
|
11724
|
+
"show-ref",
|
|
11725
|
+
"--verify",
|
|
11726
|
+
"--quiet",
|
|
11727
|
+
`refs/remotes/origin/${candidate}`
|
|
11728
|
+
],
|
|
11729
|
+
{ cwd: repoRoot }
|
|
11730
|
+
);
|
|
11731
|
+
base = candidate;
|
|
11732
|
+
break;
|
|
11733
|
+
} catch {
|
|
11734
|
+
}
|
|
11735
|
+
}
|
|
11736
|
+
}
|
|
11737
|
+
if (!base) {
|
|
11738
|
+
throw validationError(
|
|
11739
|
+
"Could not determine the origin default branch.",
|
|
11740
|
+
"Ensure the repo has an 'origin' remote with a main or master branch (git fetch origin)."
|
|
11741
|
+
);
|
|
11742
|
+
}
|
|
11743
|
+
let branchExists = false;
|
|
11744
|
+
try {
|
|
11745
|
+
exec2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
|
|
11746
|
+
cwd: repoRoot
|
|
11747
|
+
});
|
|
11748
|
+
branchExists = true;
|
|
11749
|
+
} catch {
|
|
11750
|
+
branchExists = false;
|
|
11751
|
+
}
|
|
11752
|
+
if (branchExists) {
|
|
11753
|
+
throw validationError(
|
|
11754
|
+
`Branch '${branch}' already exists locally \u2014 pick a new name or remove it.`
|
|
11755
|
+
);
|
|
11756
|
+
}
|
|
11757
|
+
const dir = opts.dir ? resolve(cwd, opts.dir) : defaultWorktreeDir(repoRoot, branch);
|
|
11758
|
+
const baseRef = `origin/${base}`;
|
|
11759
|
+
exec2("git", ["worktree", "add", dir, "-b", branch, baseRef], {
|
|
11760
|
+
cwd: repoRoot
|
|
11761
|
+
});
|
|
11762
|
+
const stampedPid = opts.stamp === false ? null : stampWorktreeOwner(dir);
|
|
11763
|
+
const packageManager = detectPackageManager(repoRoot);
|
|
11764
|
+
let installed = false;
|
|
11765
|
+
if (opts.install !== false && existsSync(join2(repoRoot, "package.json"))) {
|
|
11766
|
+
exec2(packageManager, ["install"], { cwd: dir, inherit: true });
|
|
11767
|
+
installed = true;
|
|
11768
|
+
}
|
|
11769
|
+
return {
|
|
11770
|
+
dir,
|
|
11771
|
+
branch,
|
|
11772
|
+
baseRef,
|
|
11773
|
+
packageManager,
|
|
11774
|
+
installed,
|
|
11775
|
+
fetched,
|
|
11776
|
+
stampedPid
|
|
11777
|
+
};
|
|
11778
|
+
}
|
|
11779
|
+
var worktreeCommand = new Command("worktree").description(
|
|
11780
|
+
"Isolated task worktrees for parallel agents"
|
|
11781
|
+
);
|
|
11782
|
+
worktreeCommand.command("new").description(
|
|
11783
|
+
"Create a fresh worktree on a new branch off the origin default branch, with a real install"
|
|
11784
|
+
).argument("<branch>", "New branch name (e.g. feat/fix-login)").argument("[dir]", "Target directory (default: sibling <repo>-<slug>)").option("--no-install", "Skip the dependency install step").action((branch, dir, _options, cmd) => {
|
|
11785
|
+
const parentOpts = cmd.optsWithGlobals();
|
|
11786
|
+
const fmt = createFormatter({
|
|
11787
|
+
json: parentOpts.json || false,
|
|
11788
|
+
quiet: parentOpts.quiet || false,
|
|
11789
|
+
isTTY: isTTY(),
|
|
11790
|
+
noColor: parentOpts.noColor || false
|
|
11791
|
+
});
|
|
11792
|
+
const localOpts = cmd.opts();
|
|
11793
|
+
const result = createTaskWorktree({
|
|
11794
|
+
branch,
|
|
11795
|
+
...dir ? { dir } : {},
|
|
11796
|
+
install: localOpts.install
|
|
11797
|
+
});
|
|
11798
|
+
if (parentOpts.json) {
|
|
11799
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
11800
|
+
return;
|
|
11801
|
+
}
|
|
11802
|
+
if (!result.fetched) {
|
|
11803
|
+
fmt.warn(
|
|
11804
|
+
"Could not fetch origin \u2014 branched off the last locally known " + result.baseRef + "."
|
|
11805
|
+
);
|
|
11806
|
+
}
|
|
11807
|
+
fmt.success(
|
|
11808
|
+
`Worktree ready: ${result.dir} on ${result.branch} (based on ${result.baseRef}).`
|
|
11809
|
+
);
|
|
11810
|
+
if (result.stampedPid) {
|
|
11811
|
+
fmt.info(
|
|
11812
|
+
"Stamped this agent session as the worktree owner (pairs with 'gad install-hooks')."
|
|
11813
|
+
);
|
|
11814
|
+
}
|
|
11815
|
+
if (!result.installed) {
|
|
11816
|
+
fmt.info(
|
|
11817
|
+
`Install skipped \u2014 run '${result.packageManager} install' inside the worktree before building.`
|
|
11818
|
+
);
|
|
11819
|
+
}
|
|
11820
|
+
fmt.info(`Next: cd ${result.dir}`);
|
|
11821
|
+
fmt.info(`Done: git worktree remove ${result.dir}`);
|
|
11822
|
+
});
|
|
11823
|
+
var worktree_default = worktreeCommand;
|
|
11824
|
+
|
|
11825
|
+
// src/commands/install-hooks.ts
|
|
11826
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
11827
|
+
import {
|
|
11828
|
+
chmodSync,
|
|
11829
|
+
existsSync as existsSync2,
|
|
11830
|
+
mkdirSync,
|
|
11831
|
+
readFileSync as readFileSync3,
|
|
11832
|
+
writeFileSync as writeFileSync2
|
|
11833
|
+
} from "node:fs";
|
|
11834
|
+
import { isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
|
|
11835
|
+
var GUARD_MARKER = "# gad-agent-guard v1";
|
|
11836
|
+
var SCHEMA_AUDIT_MARKER = "# gad-schema-audit v1";
|
|
11837
|
+
var GUARD_FILE = "gad-commit-guard.mjs";
|
|
11838
|
+
var realExec2 = (cmd, args, opts) => execFileSync4(cmd, args, {
|
|
11839
|
+
cwd: opts?.cwd,
|
|
11840
|
+
encoding: "utf8",
|
|
11841
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
11842
|
+
});
|
|
11843
|
+
function buildGuardScript() {
|
|
11844
|
+
return `#!/usr/bin/env node
|
|
11845
|
+
// gad-agent-guard v1 \u2014 pre-commit backstop against cross-worktree clobber.
|
|
11846
|
+
// Installed by \`gad install-hooks\`. Blocks a commit made inside a worktree
|
|
11847
|
+
// owned by a DIFFERENT still-live agent session. FAILS OPEN on every
|
|
11848
|
+
// ambiguity (no marker, dead owner, human committer, unsupported platform).
|
|
11849
|
+
// Deliberate override: ALLOW_FOREIGN_WORKTREE=1.
|
|
11850
|
+
import { existsSync, statSync, readFileSync } from "node:fs";
|
|
11851
|
+
import { join, basename, isAbsolute, resolve } from "node:path";
|
|
11852
|
+
import { execFileSync } from "node:child_process";
|
|
11853
|
+
|
|
11854
|
+
const OWNER_FILE = "agent-owner.json";
|
|
11855
|
+
const AGENT_RE = /(^|\\/)(claude|kiro|cursor|codex)/i;
|
|
11856
|
+
|
|
11857
|
+
function psField(pid, field) {
|
|
11858
|
+
try {
|
|
11859
|
+
return execFileSync("ps", ["-o", field + "=", "-p", String(pid)], {
|
|
11860
|
+
encoding: "utf8",
|
|
11861
|
+
}).trim();
|
|
11862
|
+
} catch {
|
|
11863
|
+
return "";
|
|
11864
|
+
}
|
|
11865
|
+
}
|
|
11866
|
+
|
|
11867
|
+
function ancestry(startPid) {
|
|
11868
|
+
const pids = new Set();
|
|
11869
|
+
let pid = Number(startPid);
|
|
11870
|
+
for (let i = 0; i < 40 && pid > 1; i++) {
|
|
11871
|
+
pids.add(pid);
|
|
11872
|
+
const ppid = Number(psField(pid, "ppid"));
|
|
11873
|
+
if (!ppid || ppid === pid) break;
|
|
11874
|
+
pid = ppid;
|
|
11875
|
+
}
|
|
11876
|
+
return pids;
|
|
11877
|
+
}
|
|
11878
|
+
|
|
11879
|
+
function isAlive(pid) {
|
|
11880
|
+
try {
|
|
11881
|
+
process.kill(pid, 0);
|
|
11882
|
+
return true;
|
|
11883
|
+
} catch {
|
|
11884
|
+
return false;
|
|
11885
|
+
}
|
|
11886
|
+
}
|
|
11887
|
+
|
|
11888
|
+
function ownGitDir(cwd) {
|
|
11889
|
+
const dotGit = join(cwd, ".git");
|
|
11890
|
+
if (!existsSync(dotGit)) return null;
|
|
11891
|
+
if (statSync(dotGit).isDirectory()) return dotGit;
|
|
11892
|
+
const m = readFileSync(dotGit, "utf8").trim().match(/^gitdir:\\s*(.+)$/m);
|
|
11893
|
+
if (!m) return null;
|
|
11894
|
+
return isAbsolute(m[1]) ? m[1] : resolve(cwd, m[1]);
|
|
11895
|
+
}
|
|
11896
|
+
|
|
11897
|
+
function evaluate(cwd, env) {
|
|
11898
|
+
if (env.ALLOW_FOREIGN_WORKTREE) return { block: false, reason: "override" };
|
|
11899
|
+
if (process.platform === "win32")
|
|
11900
|
+
return { block: false, reason: "unsupported-platform" };
|
|
11901
|
+
const isAgent =
|
|
11902
|
+
env.CLAUDECODE ||
|
|
11903
|
+
env.CLAUDE_CODE ||
|
|
11904
|
+
env.KIRO_AGENT ||
|
|
11905
|
+
env.CURSOR_AGENT ||
|
|
11906
|
+
env.CODEX_AGENT ||
|
|
11907
|
+
env.AGENT_SESSION;
|
|
11908
|
+
if (!isAgent) return { block: false, reason: "not-an-agent" };
|
|
11909
|
+
const gd = ownGitDir(cwd);
|
|
11910
|
+
if (!gd) return { block: false, reason: "no-gitdir" };
|
|
11911
|
+
const markerPath = join(gd, OWNER_FILE);
|
|
11912
|
+
if (!existsSync(markerPath)) return { block: false, reason: "no-marker" };
|
|
11913
|
+
let owner;
|
|
11914
|
+
try {
|
|
11915
|
+
owner = JSON.parse(readFileSync(markerPath, "utf8"));
|
|
11916
|
+
} catch {
|
|
11917
|
+
return { block: false, reason: "marker-unreadable" };
|
|
11918
|
+
}
|
|
11919
|
+
if (!owner || !owner.ownerPid)
|
|
11920
|
+
return { block: false, reason: "marker-no-owner" };
|
|
11921
|
+
const anc = ancestry(process.ppid);
|
|
11922
|
+
if (anc.has(Number(owner.ownerPid)))
|
|
11923
|
+
return { block: false, reason: "owner-match" };
|
|
11924
|
+
if (!isAlive(Number(owner.ownerPid)))
|
|
11925
|
+
return { block: false, reason: "owner-dead" };
|
|
11926
|
+
return { block: true, reason: "foreign-owner ownerPid=" + owner.ownerPid };
|
|
11927
|
+
}
|
|
11928
|
+
|
|
11929
|
+
const { block, reason } = evaluate(process.cwd(), process.env);
|
|
11930
|
+
if (block) {
|
|
11931
|
+
console.error("");
|
|
11932
|
+
console.error(
|
|
11933
|
+
"gad-agent-guard: commit blocked \u2014 this worktree belongs to a DIFFERENT live agent session.",
|
|
11934
|
+
);
|
|
11935
|
+
console.error(" " + reason);
|
|
11936
|
+
console.error(
|
|
11937
|
+
" You are about to commit another agent's in-flight work. Use your own worktree:",
|
|
11938
|
+
);
|
|
11939
|
+
console.error(" gad worktree new <branch>");
|
|
11940
|
+
console.error(" Deliberate override: ALLOW_FOREIGN_WORKTREE=1");
|
|
11941
|
+
console.error("");
|
|
11942
|
+
process.exit(1);
|
|
11943
|
+
}
|
|
11944
|
+
`;
|
|
11945
|
+
}
|
|
11946
|
+
function buildHookBlock() {
|
|
11947
|
+
return `${GUARD_MARKER} (managed by \`gad install-hooks\` \u2014 do not edit this block)
|
|
11948
|
+
if command -v node >/dev/null 2>&1; then
|
|
11949
|
+
gad_guard_dir="$(git rev-parse --git-common-dir 2>/dev/null)"
|
|
11950
|
+
if [ -n "$gad_guard_dir" ] && [ -f "$gad_guard_dir/hooks/${GUARD_FILE}" ]; then
|
|
11951
|
+
node "$gad_guard_dir/hooks/${GUARD_FILE}" || exit 1
|
|
11952
|
+
fi
|
|
11953
|
+
fi
|
|
11954
|
+
`;
|
|
11955
|
+
}
|
|
11956
|
+
function buildSchemaAuditHookBlock() {
|
|
11957
|
+
return `${SCHEMA_AUDIT_MARKER} (managed by \`gad install-hooks\` \u2014 do not edit this block)
|
|
11958
|
+
if [ -f package.json ] && grep -q '"schema:audit"' package.json; then
|
|
11959
|
+
if command -v pnpm >/dev/null 2>&1; then
|
|
11960
|
+
pnpm schema:audit --pre-push || exit 1
|
|
11961
|
+
else
|
|
11962
|
+
echo "gad-schema-audit: pnpm not found; skipping schema audit" >&2
|
|
11963
|
+
fi
|
|
11964
|
+
fi
|
|
11965
|
+
`;
|
|
11966
|
+
}
|
|
11967
|
+
function planHookInstall(existing) {
|
|
11968
|
+
if (existing !== null && existing.includes(GUARD_MARKER)) {
|
|
11969
|
+
return { action: "noop", content: existing };
|
|
11970
|
+
}
|
|
11971
|
+
if (existing === null) {
|
|
11972
|
+
return { action: "create", content: `#!/bin/sh
|
|
11973
|
+
${buildHookBlock()}` };
|
|
11974
|
+
}
|
|
11975
|
+
return {
|
|
11976
|
+
action: "append",
|
|
11977
|
+
content: `${existing.trimEnd()}
|
|
11978
|
+
|
|
11979
|
+
${buildHookBlock()}`
|
|
11980
|
+
};
|
|
11981
|
+
}
|
|
11982
|
+
function planSchemaAuditHookInstall(existing) {
|
|
11983
|
+
if (existing !== null && existing.includes(SCHEMA_AUDIT_MARKER)) {
|
|
11984
|
+
return { action: "noop", content: existing };
|
|
11985
|
+
}
|
|
11986
|
+
if (existing === null) {
|
|
11987
|
+
return {
|
|
11988
|
+
action: "create",
|
|
11989
|
+
content: `#!/bin/sh
|
|
11990
|
+
${buildSchemaAuditHookBlock()}`
|
|
11991
|
+
};
|
|
11992
|
+
}
|
|
11993
|
+
return {
|
|
11994
|
+
action: "append",
|
|
11995
|
+
content: `${existing.trimEnd()}
|
|
11996
|
+
|
|
11997
|
+
${buildSchemaAuditHookBlock()}`
|
|
11998
|
+
};
|
|
11999
|
+
}
|
|
12000
|
+
function installCommitGuard(opts) {
|
|
12001
|
+
const exec2 = opts?.exec ?? realExec2;
|
|
12002
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
12003
|
+
let repoRoot;
|
|
12004
|
+
let commonDir;
|
|
12005
|
+
try {
|
|
12006
|
+
repoRoot = exec2("git", ["rev-parse", "--show-toplevel"], { cwd }).trim();
|
|
12007
|
+
commonDir = exec2("git", ["rev-parse", "--git-common-dir"], {
|
|
12008
|
+
cwd: repoRoot
|
|
12009
|
+
}).trim();
|
|
12010
|
+
} catch {
|
|
12011
|
+
throw validationError(
|
|
12012
|
+
"Not inside a git repository.",
|
|
12013
|
+
"Run this from the repo whose commits you want guarded."
|
|
12014
|
+
);
|
|
12015
|
+
}
|
|
12016
|
+
if (!isAbsolute2(commonDir)) commonDir = resolve2(repoRoot, commonDir);
|
|
12017
|
+
const hooksDir = join3(commonDir, "hooks");
|
|
12018
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
12019
|
+
const guardPath = join3(hooksDir, GUARD_FILE);
|
|
12020
|
+
writeFileSync2(guardPath, buildGuardScript());
|
|
12021
|
+
chmodSync(guardPath, 493);
|
|
12022
|
+
const huskyHook = join3(repoRoot, ".husky", "pre-commit");
|
|
12023
|
+
const hookPath = existsSync2(huskyHook) ? huskyHook : join3(hooksDir, "pre-commit");
|
|
12024
|
+
const existing = existsSync2(hookPath) ? readFileSync3(hookPath, "utf8") : null;
|
|
12025
|
+
const plan = planHookInstall(existing);
|
|
12026
|
+
if (plan.action !== "noop") {
|
|
12027
|
+
writeFileSync2(hookPath, plan.content);
|
|
12028
|
+
}
|
|
12029
|
+
chmodSync(hookPath, 493);
|
|
12030
|
+
const huskyDir = join3(repoRoot, ".husky");
|
|
12031
|
+
const huskyPrePush = join3(huskyDir, "pre-push");
|
|
12032
|
+
const schemaAuditHookPath = existsSync2(huskyPrePush) || existsSync2(huskyDir) ? huskyPrePush : join3(hooksDir, "pre-push");
|
|
12033
|
+
const existingSchemaAuditHook = existsSync2(schemaAuditHookPath) ? readFileSync3(schemaAuditHookPath, "utf8") : null;
|
|
12034
|
+
const schemaAuditPlan = planSchemaAuditHookInstall(existingSchemaAuditHook);
|
|
12035
|
+
if (schemaAuditPlan.action !== "noop") {
|
|
12036
|
+
writeFileSync2(schemaAuditHookPath, schemaAuditPlan.content);
|
|
12037
|
+
}
|
|
12038
|
+
chmodSync(schemaAuditHookPath, 493);
|
|
12039
|
+
return {
|
|
12040
|
+
guardPath,
|
|
12041
|
+
hookPath,
|
|
12042
|
+
action: plan.action,
|
|
12043
|
+
schemaAuditHookPath,
|
|
12044
|
+
schemaAuditAction: schemaAuditPlan.action
|
|
12045
|
+
};
|
|
12046
|
+
}
|
|
12047
|
+
var installHooksCommand = new Command("install-hooks").description(
|
|
12048
|
+
"Install the fail-open agent commit guard into this repo's pre-commit hook (idempotent)"
|
|
12049
|
+
).action((_options, cmd) => {
|
|
12050
|
+
const parentOpts = cmd.optsWithGlobals();
|
|
12051
|
+
const fmt = createFormatter({
|
|
12052
|
+
json: parentOpts.json || false,
|
|
12053
|
+
quiet: parentOpts.quiet || false,
|
|
12054
|
+
isTTY: isTTY(),
|
|
12055
|
+
noColor: parentOpts.noColor || false
|
|
12056
|
+
});
|
|
12057
|
+
const result = installCommitGuard();
|
|
12058
|
+
if (parentOpts.json) {
|
|
12059
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
12060
|
+
return;
|
|
12061
|
+
}
|
|
12062
|
+
if (result.action === "noop") {
|
|
12063
|
+
fmt.success(`Commit guard already installed at ${result.hookPath}.`);
|
|
12064
|
+
} else {
|
|
12065
|
+
fmt.success(
|
|
12066
|
+
`Commit guard ${result.action === "append" ? "chained into" : "installed at"} ${result.hookPath}.`
|
|
12067
|
+
);
|
|
12068
|
+
}
|
|
12069
|
+
if (result.schemaAuditAction === "noop") {
|
|
12070
|
+
fmt.success(
|
|
12071
|
+
`Schema audit pre-push hook already installed at ${result.schemaAuditHookPath}.`
|
|
12072
|
+
);
|
|
12073
|
+
} else {
|
|
12074
|
+
fmt.success(
|
|
12075
|
+
`Schema audit pre-push hook ${result.schemaAuditAction === "append" ? "chained into" : "installed at"} ${result.schemaAuditHookPath}.`
|
|
12076
|
+
);
|
|
12077
|
+
}
|
|
12078
|
+
fmt.info(
|
|
12079
|
+
"The guard fails open for humans and dead sessions; override deliberately with ALLOW_FOREIGN_WORKTREE=1."
|
|
12080
|
+
);
|
|
12081
|
+
fmt.info(
|
|
12082
|
+
"Pair with 'gad worktree new <branch>' so each agent session owns its worktree."
|
|
12083
|
+
);
|
|
12084
|
+
});
|
|
12085
|
+
var install_hooks_default = installHooksCommand;
|
|
12086
|
+
|
|
12087
|
+
// src/commands/guardrails.ts
|
|
12088
|
+
function writeJson2(value) {
|
|
12089
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
12090
|
+
`);
|
|
12091
|
+
}
|
|
12092
|
+
function normalizeResponse(value) {
|
|
12093
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
12094
|
+
}
|
|
12095
|
+
async function createClient2() {
|
|
12096
|
+
const auth = await resolveAuth();
|
|
12097
|
+
return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
|
|
12098
|
+
}
|
|
12099
|
+
function renderGuardrails(data, cmd) {
|
|
12100
|
+
const output = globalOutputOpts(cmd);
|
|
12101
|
+
const fmt = createFormatter({ ...output, isTTY: isTTY() });
|
|
12102
|
+
if (output.json) return writeJson2(data);
|
|
12103
|
+
const guardrails = Array.isArray(data.guardrails) ? data.guardrails : [];
|
|
12104
|
+
fmt.table(
|
|
12105
|
+
guardrails.map((guardrail) => ({
|
|
12106
|
+
...guardrail,
|
|
12107
|
+
state: guardrail.enabled ? "on" : "off",
|
|
12108
|
+
surfacesLabel: guardrail.surfaces.join(", ")
|
|
12109
|
+
})),
|
|
12110
|
+
[
|
|
12111
|
+
{ key: "state", header: "State" },
|
|
12112
|
+
{ key: "id", header: "ID" },
|
|
12113
|
+
{ key: "title", header: "Guardrail" },
|
|
12114
|
+
{ key: "command", header: "Command" },
|
|
12115
|
+
{ key: "surfacesLabel", header: "Surfaces" }
|
|
12116
|
+
]
|
|
12117
|
+
);
|
|
12118
|
+
}
|
|
12119
|
+
function guardrailsCommand() {
|
|
12120
|
+
const cmd = new Command("guardrails").description(
|
|
12121
|
+
"List and manage optional engineering guardrails for the active workspace"
|
|
12122
|
+
);
|
|
12123
|
+
cmd.command("list").description("List guardrails, explanations, commands, and enabled state").action(async (_options, actionCmd) => {
|
|
12124
|
+
const client = await createClient2();
|
|
12125
|
+
const result = normalizeResponse(
|
|
12126
|
+
await client.mcpCall("engineering_guardrails_list", {})
|
|
12127
|
+
);
|
|
12128
|
+
renderGuardrails(result, actionCmd);
|
|
12129
|
+
});
|
|
12130
|
+
cmd.command("enable").description("Enable a guardrail for the active workspace").argument("<id>", "Guardrail id from `gad guardrails list`").action(async (id, _options, actionCmd) => {
|
|
12131
|
+
const client = await createClient2();
|
|
12132
|
+
const result = normalizeResponse(
|
|
12133
|
+
await client.mcpCall("engineering_guardrails_update", {
|
|
12134
|
+
id,
|
|
12135
|
+
enabled: true
|
|
12136
|
+
})
|
|
12137
|
+
);
|
|
12138
|
+
renderGuardrails(result, actionCmd);
|
|
12139
|
+
});
|
|
12140
|
+
cmd.command("disable").description("Disable a guardrail for the active workspace").argument("<id>", "Guardrail id from `gad guardrails list`").action(async (id, _options, actionCmd) => {
|
|
12141
|
+
const client = await createClient2();
|
|
12142
|
+
const result = normalizeResponse(
|
|
12143
|
+
await client.mcpCall("engineering_guardrails_update", {
|
|
12144
|
+
id,
|
|
12145
|
+
enabled: false
|
|
12146
|
+
})
|
|
12147
|
+
);
|
|
12148
|
+
renderGuardrails(result, actionCmd);
|
|
12149
|
+
});
|
|
12150
|
+
return cmd;
|
|
12151
|
+
}
|
|
12152
|
+
|
|
11231
12153
|
// src/commands/doctor.ts
|
|
11232
|
-
import { existsSync } from "node:fs";
|
|
12154
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
11233
12155
|
import { copyFile, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
11234
12156
|
import { homedir as homedir2 } from "node:os";
|
|
11235
|
-
import { dirname as
|
|
12157
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
11236
12158
|
|
|
11237
12159
|
// src/lib/codex-config.ts
|
|
11238
|
-
import { execFileSync as
|
|
12160
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
11239
12161
|
var CODEX_SERVER_NAME = "great-arrow";
|
|
11240
12162
|
var CODEX_CLIENT_ID = "codex-cli";
|
|
11241
12163
|
var SAFE_GLOBAL_MCP_SERVERS = /* @__PURE__ */ new Set([
|
|
@@ -11258,13 +12180,31 @@ function mcpEndpoint(apiUrl) {
|
|
|
11258
12180
|
function tomlEscape(value) {
|
|
11259
12181
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
11260
12182
|
}
|
|
12183
|
+
function compareVersion(a, b) {
|
|
12184
|
+
if (!a && !b) return 0;
|
|
12185
|
+
if (!a) return -1;
|
|
12186
|
+
if (!b) return 1;
|
|
12187
|
+
const left = a.match(/\d+|[a-z]+/gi) ?? [];
|
|
12188
|
+
const right = b.match(/\d+|[a-z]+/gi) ?? [];
|
|
12189
|
+
const length = Math.max(left.length, right.length);
|
|
12190
|
+
for (let i = 0; i < length; i += 1) {
|
|
12191
|
+
const l = left[i] ?? "0";
|
|
12192
|
+
const r = right[i] ?? "0";
|
|
12193
|
+
const ln = Number(l);
|
|
12194
|
+
const rn = Number(r);
|
|
12195
|
+
const bothNumeric = Number.isFinite(ln) && Number.isFinite(rn);
|
|
12196
|
+
if (bothNumeric && ln !== rn) return ln > rn ? 1 : -1;
|
|
12197
|
+
if (!bothNumeric && l !== r) return l > r ? 1 : -1;
|
|
12198
|
+
}
|
|
12199
|
+
return 0;
|
|
12200
|
+
}
|
|
11261
12201
|
function detectGitOrigin2(cwd = process.cwd()) {
|
|
11262
12202
|
try {
|
|
11263
12203
|
const env4 = { ...process.env };
|
|
11264
12204
|
for (const key of Object.keys(env4)) {
|
|
11265
12205
|
if (key.startsWith("GIT_")) delete env4[key];
|
|
11266
12206
|
}
|
|
11267
|
-
const url =
|
|
12207
|
+
const url = execFileSync5(
|
|
11268
12208
|
"git",
|
|
11269
12209
|
["-C", cwd, "remote", "get-url", "origin"],
|
|
11270
12210
|
{
|
|
@@ -11394,9 +12334,29 @@ function extractConfiguredRepoKeys(text) {
|
|
|
11394
12334
|
}
|
|
11395
12335
|
return [...keys];
|
|
11396
12336
|
}
|
|
11397
|
-
function
|
|
11398
|
-
|
|
11399
|
-
|
|
12337
|
+
function extractInstallVersion(text) {
|
|
12338
|
+
const escapedHeader = GAD_INSTALL_HEADER.replace(
|
|
12339
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
12340
|
+
"\\$&"
|
|
12341
|
+
);
|
|
12342
|
+
const match = text.match(
|
|
12343
|
+
new RegExp(`^\\s*${escapedHeader}\\s*=\\s*"([^"]+)"\\s*$`, "m")
|
|
12344
|
+
);
|
|
12345
|
+
return match?.[1]?.trim() || null;
|
|
12346
|
+
}
|
|
12347
|
+
function localPathArgs(text) {
|
|
12348
|
+
return [...text.matchAll(/"((?:\/|~\/)[^"]+)"/g)].map((m) => m[1] ?? "");
|
|
12349
|
+
}
|
|
12350
|
+
function hasForeignRepoMarker(text, expectedRepoKey) {
|
|
12351
|
+
if (!expectedRepoKey) return false;
|
|
12352
|
+
const repoName = expectedRepoKey.split("/").pop()?.toLowerCase() ?? "";
|
|
12353
|
+
if (!repoName) return false;
|
|
12354
|
+
for (const p of localPathArgs(text)) {
|
|
12355
|
+
const lower = p.toLowerCase();
|
|
12356
|
+
const looksLikeCheckout = /server-filesystem/.test(text) || /\.(c?js|mjs|ts|py|sh)$/.test(lower);
|
|
12357
|
+
if (looksLikeCheckout && !lower.includes(repoName)) return true;
|
|
12358
|
+
}
|
|
12359
|
+
return false;
|
|
11400
12360
|
}
|
|
11401
12361
|
function isUnmanagedServer(server) {
|
|
11402
12362
|
return !SAFE_GLOBAL_MCP_SERVERS.has(server);
|
|
@@ -11407,6 +12367,7 @@ function inspectCodexConfig(content, expectedRepoUrl) {
|
|
|
11407
12367
|
const serverNames = [...grouped.keys()].sort();
|
|
11408
12368
|
const greatArrowText = grouped.get(CODEX_SERVER_NAME) ?? "";
|
|
11409
12369
|
const hasGreatArrowServer = grouped.has(CODEX_SERVER_NAME);
|
|
12370
|
+
const greatArrowInstallVersion = extractInstallVersion(greatArrowText);
|
|
11410
12371
|
const greatArrowRepoKeys = extractConfiguredRepoKeys(greatArrowText);
|
|
11411
12372
|
const greatArrowHasRepoHeader = Boolean(
|
|
11412
12373
|
expectedRepoKey && greatArrowRepoKeys.includes(expectedRepoKey)
|
|
@@ -11417,7 +12378,7 @@ function inspectCodexConfig(content, expectedRepoUrl) {
|
|
|
11417
12378
|
if (server === CODEX_SERVER_NAME) continue;
|
|
11418
12379
|
const keys = extractConfiguredRepoKeys(text);
|
|
11419
12380
|
const mismatchedExplicitRepo = expectedRepoKey && keys.length > 0 && !keys.some((key) => key === expectedRepoKey);
|
|
11420
|
-
if (mismatchedExplicitRepo ||
|
|
12381
|
+
if (mismatchedExplicitRepo || hasForeignRepoMarker(text, expectedRepoKey)) {
|
|
11421
12382
|
staleRepoServers.push(server);
|
|
11422
12383
|
}
|
|
11423
12384
|
}
|
|
@@ -11449,6 +12410,36 @@ function inspectCodexConfig(content, expectedRepoUrl) {
|
|
|
11449
12410
|
}
|
|
11450
12411
|
);
|
|
11451
12412
|
}
|
|
12413
|
+
if (hasGreatArrowServer) {
|
|
12414
|
+
if (!greatArrowInstallVersion) {
|
|
12415
|
+
issues.push({
|
|
12416
|
+
code: "install_version_missing",
|
|
12417
|
+
level: "warn",
|
|
12418
|
+
message: "Great Arrow is missing the install-version header.",
|
|
12419
|
+
hint: "Run `gad mcp install codex` to refresh Codex update tracking."
|
|
12420
|
+
});
|
|
12421
|
+
} else if (compareVersion(greatArrowInstallVersion, GAD_INSTALL_MIN_VERSION) < 0) {
|
|
12422
|
+
issues.push({
|
|
12423
|
+
code: "install_version_required",
|
|
12424
|
+
level: "fail",
|
|
12425
|
+
message: `Great Arrow install ${greatArrowInstallVersion} is below the supported minimum ${GAD_INSTALL_MIN_VERSION}.`,
|
|
12426
|
+
hint: "Update the CLI with `npm install -g @great-arrow/cli@latest`, then run `gad mcp install codex`."
|
|
12427
|
+
});
|
|
12428
|
+
} else if (compareVersion(greatArrowInstallVersion, GAD_INSTALL_VERSION) < 0) {
|
|
12429
|
+
issues.push({
|
|
12430
|
+
code: "install_version_stale",
|
|
12431
|
+
level: "warn",
|
|
12432
|
+
message: `Great Arrow install ${greatArrowInstallVersion} can be refreshed to ${GAD_INSTALL_VERSION}.`,
|
|
12433
|
+
hint: "Run `gad mcp install codex` to rewrite the Codex block with the latest self-healing headers."
|
|
12434
|
+
});
|
|
12435
|
+
} else {
|
|
12436
|
+
issues.push({
|
|
12437
|
+
code: "install_version_current",
|
|
12438
|
+
level: "pass",
|
|
12439
|
+
message: `Great Arrow install version ${greatArrowInstallVersion} is current.`
|
|
12440
|
+
});
|
|
12441
|
+
}
|
|
12442
|
+
}
|
|
11452
12443
|
if (staleRepoServers.length > 0) {
|
|
11453
12444
|
issues.push({
|
|
11454
12445
|
code: "stale_repo_servers",
|
|
@@ -11476,6 +12467,8 @@ function inspectCodexConfig(content, expectedRepoUrl) {
|
|
|
11476
12467
|
expectedRepoKey,
|
|
11477
12468
|
hasGreatArrowServer,
|
|
11478
12469
|
greatArrowHasRepoHeader,
|
|
12470
|
+
greatArrowInstallVersion,
|
|
12471
|
+
greatArrowInstallVersionCurrent: Boolean(greatArrowInstallVersion) && compareVersion(greatArrowInstallVersion, GAD_INSTALL_VERSION) >= 0,
|
|
11479
12472
|
unmanagedServers,
|
|
11480
12473
|
staleRepoServers,
|
|
11481
12474
|
issues
|
|
@@ -11494,8 +12487,134 @@ function repairCodexConfig(content, staleRepoServers) {
|
|
|
11494
12487
|
};
|
|
11495
12488
|
}
|
|
11496
12489
|
|
|
12490
|
+
// src/lib/json-mcp-config.ts
|
|
12491
|
+
var JSON_SERVER_NAME = "great-arrow";
|
|
12492
|
+
function stripBom(content) {
|
|
12493
|
+
return content.codePointAt(0) === 65279 ? content.slice(1) : content;
|
|
12494
|
+
}
|
|
12495
|
+
function asString(v) {
|
|
12496
|
+
return typeof v === "string" ? v : null;
|
|
12497
|
+
}
|
|
12498
|
+
function explicitRepoKeys(entry) {
|
|
12499
|
+
const keys = /* @__PURE__ */ new Set();
|
|
12500
|
+
const headerVal = asString(entry.headers?.["X-Repo-URL"]) ?? asString(entry.headers?.["x-repo-url"]);
|
|
12501
|
+
const envVals = [
|
|
12502
|
+
asString(entry.env?.MEMORY_REPO_URL),
|
|
12503
|
+
asString(entry.env?.GAD_REPO_URL)
|
|
12504
|
+
];
|
|
12505
|
+
for (const v of [headerVal, ...envVals]) {
|
|
12506
|
+
const k = repoKey(v);
|
|
12507
|
+
if (k) keys.add(k);
|
|
12508
|
+
}
|
|
12509
|
+
const text = JSON.stringify([entry.url, entry.args]);
|
|
12510
|
+
const gitUrls = /(?:https:\/\/|ssh:\/\/git@|git@)(?:github\.com|gitlab\.com|bitbucket\.org)[:/][^\s"',\]]+/gi;
|
|
12511
|
+
for (const m of text.matchAll(gitUrls)) {
|
|
12512
|
+
const k = repoKey(m[0]);
|
|
12513
|
+
if (k) keys.add(k);
|
|
12514
|
+
}
|
|
12515
|
+
return [...keys];
|
|
12516
|
+
}
|
|
12517
|
+
function inspectJsonMcpConfig(content, expectedRepoUrl) {
|
|
12518
|
+
const expectedRepoKey = repoKey(expectedRepoUrl);
|
|
12519
|
+
const base = {
|
|
12520
|
+
expectedRepoUrl: expectedRepoUrl ?? null,
|
|
12521
|
+
expectedRepoKey
|
|
12522
|
+
};
|
|
12523
|
+
if (!content.trim()) {
|
|
12524
|
+
return {
|
|
12525
|
+
...base,
|
|
12526
|
+
parseError: null,
|
|
12527
|
+
staleRepoServers: [],
|
|
12528
|
+
issues: [
|
|
12529
|
+
{ code: "no_config", level: "warn", message: "No MCP config found." }
|
|
12530
|
+
]
|
|
12531
|
+
};
|
|
12532
|
+
}
|
|
12533
|
+
let parsed;
|
|
12534
|
+
try {
|
|
12535
|
+
parsed = JSON.parse(stripBom(content));
|
|
12536
|
+
} catch (e) {
|
|
12537
|
+
return {
|
|
12538
|
+
...base,
|
|
12539
|
+
parseError: e instanceof Error ? e.message : String(e),
|
|
12540
|
+
staleRepoServers: [],
|
|
12541
|
+
issues: [
|
|
12542
|
+
{
|
|
12543
|
+
code: "parse_error",
|
|
12544
|
+
level: "warn",
|
|
12545
|
+
message: "Could not parse MCP config (left untouched)."
|
|
12546
|
+
}
|
|
12547
|
+
]
|
|
12548
|
+
};
|
|
12549
|
+
}
|
|
12550
|
+
const serversRaw = parsed.mcpServers;
|
|
12551
|
+
const servers = serversRaw && typeof serversRaw === "object" && !Array.isArray(serversRaw) ? serversRaw : {};
|
|
12552
|
+
const stale = [];
|
|
12553
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
12554
|
+
if (name === JSON_SERVER_NAME) continue;
|
|
12555
|
+
if (!entry || typeof entry !== "object") continue;
|
|
12556
|
+
const keys = explicitRepoKeys(entry);
|
|
12557
|
+
const mismatchedExplicit = expectedRepoKey && keys.length > 0 && !keys.some((k) => k === expectedRepoKey);
|
|
12558
|
+
if (mismatchedExplicit || hasForeignRepoMarker(JSON.stringify(entry), expectedRepoKey)) {
|
|
12559
|
+
stale.push(name);
|
|
12560
|
+
}
|
|
12561
|
+
}
|
|
12562
|
+
stale.sort();
|
|
12563
|
+
const issues = [
|
|
12564
|
+
JSON_SERVER_NAME in servers ? {
|
|
12565
|
+
code: "great_arrow_present",
|
|
12566
|
+
level: "pass",
|
|
12567
|
+
message: "Great Arrow MCP server present."
|
|
12568
|
+
} : {
|
|
12569
|
+
code: "great_arrow_missing",
|
|
12570
|
+
level: "warn",
|
|
12571
|
+
message: "Great Arrow MCP server not found in this config.",
|
|
12572
|
+
hint: "Run `gad mcp install` from the repo root."
|
|
12573
|
+
},
|
|
12574
|
+
stale.length > 0 ? {
|
|
12575
|
+
code: "stale_repo_servers",
|
|
12576
|
+
level: "fail",
|
|
12577
|
+
message: `Found stale repo-scoped MCP server(s): ${stale.join(", ")}.`,
|
|
12578
|
+
hint: "Run `gad doctor clients --enforce` to remove the stale blocks."
|
|
12579
|
+
} : {
|
|
12580
|
+
code: "no_stale_repo_servers",
|
|
12581
|
+
level: "pass",
|
|
12582
|
+
message: "No stale repo-scoped MCP server blocks found."
|
|
12583
|
+
}
|
|
12584
|
+
];
|
|
12585
|
+
return { ...base, parseError: null, staleRepoServers: stale, issues };
|
|
12586
|
+
}
|
|
12587
|
+
function repairJsonMcpConfig(content, staleRepoServers) {
|
|
12588
|
+
if (!content.trim() || staleRepoServers.length === 0) {
|
|
12589
|
+
return { content, removedServers: [] };
|
|
12590
|
+
}
|
|
12591
|
+
let parsed;
|
|
12592
|
+
try {
|
|
12593
|
+
parsed = JSON.parse(stripBom(content));
|
|
12594
|
+
} catch {
|
|
12595
|
+
return { content, removedServers: [] };
|
|
12596
|
+
}
|
|
12597
|
+
const servers = parsed.mcpServers;
|
|
12598
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
12599
|
+
return { content, removedServers: [] };
|
|
12600
|
+
}
|
|
12601
|
+
const removed = [];
|
|
12602
|
+
for (const name of new Set(staleRepoServers)) {
|
|
12603
|
+
if (name in servers) {
|
|
12604
|
+
delete servers[name];
|
|
12605
|
+
removed.push(name);
|
|
12606
|
+
}
|
|
12607
|
+
}
|
|
12608
|
+
removed.sort();
|
|
12609
|
+
if (removed.length === 0) return { content, removedServers: [] };
|
|
12610
|
+
return {
|
|
12611
|
+
content: JSON.stringify(parsed, null, 2) + "\n",
|
|
12612
|
+
removedServers: removed
|
|
12613
|
+
};
|
|
12614
|
+
}
|
|
12615
|
+
|
|
11497
12616
|
// src/commands/doctor.ts
|
|
11498
|
-
function
|
|
12617
|
+
function compareVersion2(a, b) {
|
|
11499
12618
|
const left = a.match(/\d+|[a-z]+/gi) ?? [];
|
|
11500
12619
|
const right = b.match(/\d+|[a-z]+/gi) ?? [];
|
|
11501
12620
|
const length = Math.max(left.length, right.length);
|
|
@@ -11597,7 +12716,7 @@ async function checkCliVersion(auth) {
|
|
|
11597
12716
|
const latest = cli.latest ?? CLI_VERSION;
|
|
11598
12717
|
const minimum = cli.minimum ?? CLI_VERSION;
|
|
11599
12718
|
const updateCommand2 = cli.updateCommand ?? `npm install -g ${CLI_PACKAGE_NAME}@latest`;
|
|
11600
|
-
if (
|
|
12719
|
+
if (compareVersion2(CLI_VERSION, minimum) < 0) {
|
|
11601
12720
|
return {
|
|
11602
12721
|
name: "CLI Version",
|
|
11603
12722
|
status: "fail",
|
|
@@ -11605,7 +12724,7 @@ async function checkCliVersion(auth) {
|
|
|
11605
12724
|
hint: `Update with: ${updateCommand2}`
|
|
11606
12725
|
};
|
|
11607
12726
|
}
|
|
11608
|
-
if (
|
|
12727
|
+
if (compareVersion2(CLI_VERSION, latest) < 0) {
|
|
11609
12728
|
return {
|
|
11610
12729
|
name: "CLI Version",
|
|
11611
12730
|
status: "warn",
|
|
@@ -11695,7 +12814,7 @@ async function checkMcp(auth) {
|
|
|
11695
12814
|
}
|
|
11696
12815
|
}
|
|
11697
12816
|
function defaultCodexConfigPath() {
|
|
11698
|
-
return
|
|
12817
|
+
return join4(homedir2(), ".codex", "config.toml");
|
|
11699
12818
|
}
|
|
11700
12819
|
function codexIssuesToChecks(issues) {
|
|
11701
12820
|
return issues.map((issue) => {
|
|
@@ -11733,13 +12852,13 @@ function codexCommand() {
|
|
|
11733
12852
|
});
|
|
11734
12853
|
const configPath = options.config ?? defaultCodexConfigPath();
|
|
11735
12854
|
const expectedRepoUrl = options.expectedRepo ?? detectGitOrigin2();
|
|
11736
|
-
let content =
|
|
12855
|
+
let content = existsSync3(configPath) ? await readFile2(configPath, "utf8") : "";
|
|
11737
12856
|
let backupPath = null;
|
|
11738
12857
|
let removedServers = [];
|
|
11739
12858
|
if (options.enforce) {
|
|
11740
12859
|
const apiUrl = extractCodexGreatArrowApiUrl(content) ?? (await resolveAuth()).apiUrl;
|
|
11741
12860
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
11742
|
-
await mkdir2(
|
|
12861
|
+
await mkdir2(dirname3(configPath), { recursive: true });
|
|
11743
12862
|
if (content.trim()) {
|
|
11744
12863
|
backupPath = `${configPath}.bak.${timestamp}`;
|
|
11745
12864
|
await copyFile(configPath, backupPath);
|
|
@@ -11821,6 +12940,137 @@ function codexCommand() {
|
|
|
11821
12940
|
}
|
|
11822
12941
|
);
|
|
11823
12942
|
}
|
|
12943
|
+
function defaultJsonClientConfigs() {
|
|
12944
|
+
const home = homedir2();
|
|
12945
|
+
return [
|
|
12946
|
+
{ name: "Claude Code", path: join4(home, ".claude.json") },
|
|
12947
|
+
{ name: "Cursor", path: join4(home, ".cursor", "mcp.json") },
|
|
12948
|
+
{ name: "Kiro", path: join4(home, ".kiro", "settings", "mcp.json") },
|
|
12949
|
+
{ name: "Gemini CLI", path: join4(home, ".gemini", "settings.json") }
|
|
12950
|
+
];
|
|
12951
|
+
}
|
|
12952
|
+
function clientsCommand() {
|
|
12953
|
+
return new Command("clients").description(
|
|
12954
|
+
"Check JSON MCP client configs (Claude Code / Cursor / Kiro / Gemini) for wrong-repo server entries and optionally repair"
|
|
12955
|
+
).option(
|
|
12956
|
+
"--expected-repo <url>",
|
|
12957
|
+
"Expected repo URL (defaults to current git origin)"
|
|
12958
|
+
).option(
|
|
12959
|
+
"--enforce",
|
|
12960
|
+
"Back up each config and remove stale repo-scoped MCP servers"
|
|
12961
|
+
).option("--json", "Output as JSON").option("--quiet", "Suppress decorative output").option("--no-color", "Disable color output").action(
|
|
12962
|
+
async (options, command) => {
|
|
12963
|
+
const g = globalOutputOpts(command);
|
|
12964
|
+
const color2 = createColor(g.noColor);
|
|
12965
|
+
const fmt = createFormatter({
|
|
12966
|
+
json: g.json,
|
|
12967
|
+
quiet: g.quiet,
|
|
12968
|
+
isTTY: isTTY(),
|
|
12969
|
+
noColor: g.noColor
|
|
12970
|
+
});
|
|
12971
|
+
const expectedRepoUrl = options.expectedRepo ?? detectGitOrigin2();
|
|
12972
|
+
const checks = [];
|
|
12973
|
+
const results = [];
|
|
12974
|
+
for (const { name, path: path9 } of defaultJsonClientConfigs()) {
|
|
12975
|
+
if (!existsSync3(path9)) continue;
|
|
12976
|
+
let content = await readFile2(path9, "utf8");
|
|
12977
|
+
let backupPath = null;
|
|
12978
|
+
let removedServers = [];
|
|
12979
|
+
if (options.enforce) {
|
|
12980
|
+
const pre = inspectJsonMcpConfig(content, expectedRepoUrl);
|
|
12981
|
+
if (!pre.parseError && pre.staleRepoServers.length > 0) {
|
|
12982
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
12983
|
+
backupPath = `${path9}.bak.${timestamp}`;
|
|
12984
|
+
await copyFile(path9, backupPath);
|
|
12985
|
+
const repaired = repairJsonMcpConfig(
|
|
12986
|
+
content,
|
|
12987
|
+
pre.staleRepoServers
|
|
12988
|
+
);
|
|
12989
|
+
content = repaired.content;
|
|
12990
|
+
removedServers = repaired.removedServers;
|
|
12991
|
+
await writeFile2(path9, content, "utf8");
|
|
12992
|
+
}
|
|
12993
|
+
}
|
|
12994
|
+
const inspection = inspectJsonMcpConfig(content, expectedRepoUrl);
|
|
12995
|
+
for (const issue of inspection.issues) {
|
|
12996
|
+
const check = {
|
|
12997
|
+
name: `${name}: ${issue.code}`,
|
|
12998
|
+
status: issue.level,
|
|
12999
|
+
message: issue.message
|
|
13000
|
+
};
|
|
13001
|
+
if (issue.hint) check.hint = issue.hint;
|
|
13002
|
+
checks.push(check);
|
|
13003
|
+
}
|
|
13004
|
+
if (options.enforce && removedServers.length > 0) {
|
|
13005
|
+
const enforcementCheck = {
|
|
13006
|
+
name: `${name}: enforcement`,
|
|
13007
|
+
status: "pass",
|
|
13008
|
+
message: `Removed stale server(s): ${removedServers.join(", ")}`
|
|
13009
|
+
};
|
|
13010
|
+
if (backupPath) enforcementCheck.hint = `Backup: ${backupPath}`;
|
|
13011
|
+
checks.push(enforcementCheck);
|
|
13012
|
+
}
|
|
13013
|
+
results.push({
|
|
13014
|
+
name,
|
|
13015
|
+
path: path9,
|
|
13016
|
+
backupPath,
|
|
13017
|
+
removedServers,
|
|
13018
|
+
inspection
|
|
13019
|
+
});
|
|
13020
|
+
}
|
|
13021
|
+
if (results.length === 0) {
|
|
13022
|
+
checks.push({
|
|
13023
|
+
name: "JSON clients",
|
|
13024
|
+
status: "warn",
|
|
13025
|
+
message: "No JSON MCP client configs found on this machine."
|
|
13026
|
+
});
|
|
13027
|
+
}
|
|
13028
|
+
if (g.json) {
|
|
13029
|
+
process.stdout.write(
|
|
13030
|
+
JSON.stringify(
|
|
13031
|
+
{ expectedRepoUrl, enforce: Boolean(options.enforce), results },
|
|
13032
|
+
null,
|
|
13033
|
+
2
|
|
13034
|
+
) + "\n"
|
|
13035
|
+
);
|
|
13036
|
+
} else {
|
|
13037
|
+
if (!expectedRepoUrl) {
|
|
13038
|
+
process.stdout.write(
|
|
13039
|
+
renderCheck(
|
|
13040
|
+
{
|
|
13041
|
+
name: "JSON clients: repo detection",
|
|
13042
|
+
status: "warn",
|
|
13043
|
+
message: "No git origin detected for this directory",
|
|
13044
|
+
hint: "Run from a repo root or pass --expected-repo."
|
|
13045
|
+
},
|
|
13046
|
+
color2
|
|
13047
|
+
) + "\n"
|
|
13048
|
+
);
|
|
13049
|
+
}
|
|
13050
|
+
for (const check of checks) {
|
|
13051
|
+
process.stdout.write(renderCheck(check, color2) + "\n");
|
|
13052
|
+
}
|
|
13053
|
+
if (!g.quiet) {
|
|
13054
|
+
const failed = checks.filter((c) => c.status === "fail").length;
|
|
13055
|
+
const warned = checks.filter((c) => c.status === "warn").length;
|
|
13056
|
+
process.stdout.write("\n");
|
|
13057
|
+
if (failed === 0 && warned === 0) {
|
|
13058
|
+
fmt.success("JSON client config guard passed");
|
|
13059
|
+
} else if (failed === 0) {
|
|
13060
|
+
fmt.warn(`JSON client guard passed with ${warned} warning(s)`);
|
|
13061
|
+
} else {
|
|
13062
|
+
fmt.error(
|
|
13063
|
+
`JSON client guard found ${failed} failure(s), ${warned} warning(s)`
|
|
13064
|
+
);
|
|
13065
|
+
}
|
|
13066
|
+
}
|
|
13067
|
+
}
|
|
13068
|
+
if (checks.some((check) => check.status === "fail")) {
|
|
13069
|
+
process.exitCode = 1;
|
|
13070
|
+
}
|
|
13071
|
+
}
|
|
13072
|
+
);
|
|
13073
|
+
}
|
|
11824
13074
|
function doctorCommand() {
|
|
11825
13075
|
const cmd = new Command("doctor").description("Run a comprehensive health check").option("--json", "Output as JSON").option("--quiet", "Suppress decorative output").option("--no-color", "Disable color output").action(async (options, command) => {
|
|
11826
13076
|
const g = globalOutputOpts(command);
|
|
@@ -11875,6 +13125,7 @@ function doctorCommand() {
|
|
|
11875
13125
|
}
|
|
11876
13126
|
});
|
|
11877
13127
|
cmd.addCommand(codexCommand());
|
|
13128
|
+
cmd.addCommand(clientsCommand());
|
|
11878
13129
|
return cmd;
|
|
11879
13130
|
}
|
|
11880
13131
|
|
|
@@ -11920,7 +13171,7 @@ var integrationListColumns = [
|
|
|
11920
13171
|
}
|
|
11921
13172
|
}
|
|
11922
13173
|
];
|
|
11923
|
-
async function
|
|
13174
|
+
async function createClient3() {
|
|
11924
13175
|
const auth = await resolveAuth();
|
|
11925
13176
|
return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
|
|
11926
13177
|
}
|
|
@@ -11982,7 +13233,7 @@ var listCommand3 = new Command("list").description("List all connected integrati
|
|
|
11982
13233
|
isTTY: isTTY(),
|
|
11983
13234
|
noColor: false
|
|
11984
13235
|
});
|
|
11985
|
-
const client = await
|
|
13236
|
+
const client = await createClient3();
|
|
11986
13237
|
const result = await client.mcpCall("integrations_list", {});
|
|
11987
13238
|
const data = result;
|
|
11988
13239
|
let integrations = [];
|
|
@@ -12007,7 +13258,7 @@ var syncCommand = new Command("sync").description("Trigger a manual sync for the
|
|
|
12007
13258
|
isTTY: isTTY(),
|
|
12008
13259
|
noColor: false
|
|
12009
13260
|
});
|
|
12010
|
-
const client = await
|
|
13261
|
+
const client = await createClient3();
|
|
12011
13262
|
fmt.info(`Triggering sync for "${kind}"...`);
|
|
12012
13263
|
try {
|
|
12013
13264
|
const result = await client.mcpCall("integration_sync", { kind });
|
|
@@ -12045,7 +13296,7 @@ var CLIENT_IDS = {
|
|
|
12045
13296
|
codex: CODEX_CLIENT_ID,
|
|
12046
13297
|
gemini: "gemini-cli"
|
|
12047
13298
|
};
|
|
12048
|
-
async function
|
|
13299
|
+
async function createClient4() {
|
|
12049
13300
|
const auth = await resolveAuth();
|
|
12050
13301
|
const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
|
|
12051
13302
|
return { client, apiUrl: auth.apiUrl };
|
|
@@ -12107,11 +13358,11 @@ async function pathExists(path9) {
|
|
|
12107
13358
|
}
|
|
12108
13359
|
async function installCodexConfig(apiUrl, token, repoUrl) {
|
|
12109
13360
|
const { homedir: homedir3 } = await import("node:os");
|
|
12110
|
-
const { join:
|
|
13361
|
+
const { join: join5 } = await import("node:path");
|
|
12111
13362
|
const { chmod, copyFile: copyFile2, mkdir: mkdir3, readFile: readFile3, writeFile: writeFile3 } = await import("node:fs/promises");
|
|
12112
|
-
const codexDir =
|
|
12113
|
-
const configPath =
|
|
12114
|
-
const envPath =
|
|
13363
|
+
const codexDir = join5(homedir3(), ".codex");
|
|
13364
|
+
const configPath = join5(codexDir, "config.toml");
|
|
13365
|
+
const envPath = join5(codexDir, ".env");
|
|
12115
13366
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
12116
13367
|
await mkdir3(codexDir, { recursive: true });
|
|
12117
13368
|
const existingConfig = await pathExists(configPath) ? await readFile3(configPath, "utf8") : "";
|
|
@@ -12153,7 +13404,7 @@ var statusCommand = new Command("status").description(
|
|
|
12153
13404
|
isTTY: isTTY(),
|
|
12154
13405
|
noColor: false
|
|
12155
13406
|
});
|
|
12156
|
-
const { client, apiUrl } = await
|
|
13407
|
+
const { client, apiUrl } = await createClient4();
|
|
12157
13408
|
let statusData = {
|
|
12158
13409
|
status: "unknown",
|
|
12159
13410
|
proxyVersion: "\u2014",
|
|
@@ -12212,7 +13463,7 @@ var logsCommand = new Command("logs").description("Tail MCP connection log with
|
|
|
12212
13463
|
});
|
|
12213
13464
|
const c = createColor(false);
|
|
12214
13465
|
const lineCount = Number.parseInt(options.lines ?? "20", 10);
|
|
12215
|
-
const { client } = await
|
|
13466
|
+
const { client } = await createClient4();
|
|
12216
13467
|
try {
|
|
12217
13468
|
const result = await client.mcpCall("integration_sync_logs", {
|
|
12218
13469
|
limit: lineCount
|
|
@@ -12378,6 +13629,9 @@ var installCommand = new Command("install").description("Generate MCP configurat
|
|
|
12378
13629
|
repoUrl
|
|
12379
13630
|
);
|
|
12380
13631
|
fmt.success(`Wrote Codex MCP config to ${configPath} and ${envPath}`);
|
|
13632
|
+
fmt.info(
|
|
13633
|
+
"Restart Codex, start a fresh chat, then call tools_discover; tools_discover and tools_enable should be visible before enabling more Great Arrow tools."
|
|
13634
|
+
);
|
|
12381
13635
|
return;
|
|
12382
13636
|
}
|
|
12383
13637
|
const config = generateMcpConfig(
|
|
@@ -12392,8 +13646,8 @@ var installCommand = new Command("install").description("Generate MCP configurat
|
|
|
12392
13646
|
return;
|
|
12393
13647
|
}
|
|
12394
13648
|
const { writeFile: writeFile3 } = await import("node:fs/promises");
|
|
12395
|
-
const { join:
|
|
12396
|
-
const outputPath =
|
|
13649
|
+
const { join: join5 } = await import("node:path");
|
|
13650
|
+
const outputPath = join5(process.cwd(), ".mcp.json");
|
|
12397
13651
|
await writeFile3(outputPath, configJson + "\n", "utf-8");
|
|
12398
13652
|
fmt.success(`Wrote ${clientName} MCP config to .mcp.json`);
|
|
12399
13653
|
}
|
|
@@ -13086,7 +14340,7 @@ var retryifyAsync = (fn, options) => {
|
|
|
13086
14340
|
throw error;
|
|
13087
14341
|
const delay2 = Math.round(interval * Math.random());
|
|
13088
14342
|
if (delay2 > 0) {
|
|
13089
|
-
const delayPromise = new Promise((
|
|
14343
|
+
const delayPromise = new Promise((resolve3) => setTimeout(resolve3, delay2));
|
|
13090
14344
|
return delayPromise.then(() => attempt.apply(void 0, args));
|
|
13091
14345
|
} else {
|
|
13092
14346
|
return attempt.apply(void 0, args);
|
|
@@ -13345,23 +14599,23 @@ var Temp = {
|
|
|
13345
14599
|
}
|
|
13346
14600
|
},
|
|
13347
14601
|
truncate: (filePath) => {
|
|
13348
|
-
const
|
|
13349
|
-
if (
|
|
14602
|
+
const basename2 = path2.basename(filePath);
|
|
14603
|
+
if (basename2.length <= LIMIT_BASENAME_LENGTH)
|
|
13350
14604
|
return filePath;
|
|
13351
|
-
const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(
|
|
14605
|
+
const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename2);
|
|
13352
14606
|
if (!truncable)
|
|
13353
14607
|
return filePath;
|
|
13354
|
-
const truncationLength =
|
|
13355
|
-
return `${filePath.slice(0, -
|
|
14608
|
+
const truncationLength = basename2.length - LIMIT_BASENAME_LENGTH;
|
|
14609
|
+
return `${filePath.slice(0, -basename2.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
|
|
13356
14610
|
}
|
|
13357
14611
|
};
|
|
13358
14612
|
node_default(Temp.purgeSyncAll);
|
|
13359
14613
|
var temp_default = Temp;
|
|
13360
14614
|
|
|
13361
14615
|
// node_modules/.pnpm/atomically@2.1.1/node_modules/atomically/dist/index.js
|
|
13362
|
-
function
|
|
14616
|
+
function writeFileSync3(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
|
|
13363
14617
|
if (isString(options))
|
|
13364
|
-
return
|
|
14618
|
+
return writeFileSync3(filePath, data, { encoding: options });
|
|
13365
14619
|
const timeout2 = options.timeout ?? DEFAULT_TIMEOUT_SYNC;
|
|
13366
14620
|
const retryOptions = { timeout: timeout2 };
|
|
13367
14621
|
let tempDisposer = null;
|
|
@@ -13688,7 +14942,7 @@ var Configstore = class {
|
|
|
13688
14942
|
}
|
|
13689
14943
|
if (error.name === "SyntaxError") {
|
|
13690
14944
|
if (this._clearInvalidConfig) {
|
|
13691
|
-
|
|
14945
|
+
writeFileSync3(this._path, "", writeFileOptions);
|
|
13692
14946
|
return {};
|
|
13693
14947
|
}
|
|
13694
14948
|
throw error;
|
|
@@ -13700,7 +14954,7 @@ var Configstore = class {
|
|
|
13700
14954
|
set all(value) {
|
|
13701
14955
|
try {
|
|
13702
14956
|
import_graceful_fs.default.mkdirSync(path4.dirname(this._path), mkdirOptions);
|
|
13703
|
-
|
|
14957
|
+
writeFileSync3(this._path, JSON.stringify(value, void 0, " "), writeFileOptions);
|
|
13704
14958
|
} catch (error) {
|
|
13705
14959
|
handlePermissionError(error);
|
|
13706
14960
|
}
|
|
@@ -14652,14 +15906,14 @@ var TimeoutError = class extends Error {
|
|
|
14652
15906
|
|
|
14653
15907
|
// node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/timeout.js
|
|
14654
15908
|
async function timeout(request, init, abortController, options) {
|
|
14655
|
-
return new Promise((
|
|
15909
|
+
return new Promise((resolve3, reject) => {
|
|
14656
15910
|
const timeoutId = setTimeout(() => {
|
|
14657
15911
|
if (abortController) {
|
|
14658
15912
|
abortController.abort();
|
|
14659
15913
|
}
|
|
14660
15914
|
reject(new TimeoutError(request));
|
|
14661
15915
|
}, options.timeout);
|
|
14662
|
-
void options.fetch(request, init).then(
|
|
15916
|
+
void options.fetch(request, init).then(resolve3).catch(reject).then(() => {
|
|
14663
15917
|
clearTimeout(timeoutId);
|
|
14664
15918
|
});
|
|
14665
15919
|
});
|
|
@@ -14667,7 +15921,7 @@ async function timeout(request, init, abortController, options) {
|
|
|
14667
15921
|
|
|
14668
15922
|
// node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/delay.js
|
|
14669
15923
|
async function delay(ms, { signal }) {
|
|
14670
|
-
return new Promise((
|
|
15924
|
+
return new Promise((resolve3, reject) => {
|
|
14671
15925
|
if (signal) {
|
|
14672
15926
|
signal.throwIfAborted();
|
|
14673
15927
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
@@ -14678,7 +15932,7 @@ async function delay(ms, { signal }) {
|
|
|
14678
15932
|
}
|
|
14679
15933
|
const timeoutId = setTimeout(() => {
|
|
14680
15934
|
signal?.removeEventListener("abort", abortHandler);
|
|
14681
|
-
|
|
15935
|
+
resolve3();
|
|
14682
15936
|
}, ms);
|
|
14683
15937
|
});
|
|
14684
15938
|
}
|
|
@@ -16396,6 +17650,10 @@ program2.addCommand(memoryCommand);
|
|
|
16396
17650
|
program2.addCommand(workspace_default);
|
|
16397
17651
|
program2.addCommand(todo_default);
|
|
16398
17652
|
program2.addCommand(claim_default);
|
|
17653
|
+
program2.addCommand(work_default());
|
|
17654
|
+
program2.addCommand(worktree_default);
|
|
17655
|
+
program2.addCommand(install_hooks_default);
|
|
17656
|
+
program2.addCommand(guardrailsCommand());
|
|
16399
17657
|
program2.addCommand(doctorCommand());
|
|
16400
17658
|
program2.addCommand(integrations_default);
|
|
16401
17659
|
program2.addCommand(mcp_default);
|