@hasna/hooks 0.9.4 → 0.9.6
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 +40 -2
- package/bin/hooks-mcp.js +330 -149
- package/bin/index.js +974 -350
- package/bin/native-safety-entry.js +1136 -0
- package/bin/serve.js +559 -165
- package/dist/index.d.ts +1 -1
- package/dist/index.js +902 -290
- package/dist/lib/native-safety.d.ts +15 -0
- package/dist/lib/resolver-types.d.ts +1 -1
- package/dist/lib/sync.d.ts +3 -2
- package/dist/lib/transport.d.ts +5 -1
- package/dist/openapi.d.ts +314 -45
- package/dist/sdk/authority.d.ts +31 -0
- package/dist/sdk/generated.d.ts +97 -0
- package/dist/sdk/index.d.ts +16 -17
- package/dist/sdk/index.js +447 -47
- package/dist/sdk/registry-client.d.ts +75 -0
- package/hooks/native-safety-entry.ts +28 -0
- package/package.json +8 -5
- package/scripts/generate-sdk.ts +60 -0
- package/scripts/validate-package.ts +104 -2
- package/scripts/verify-generated-artifacts.ts +237 -0
package/bin/index.js
CHANGED
|
@@ -2075,6 +2075,7 @@ var init_registry = __esm(() => {
|
|
|
2075
2075
|
},
|
|
2076
2076
|
{
|
|
2077
2077
|
name: "workspace-repos-guard",
|
|
2078
|
+
timeoutSeconds: 10,
|
|
2078
2079
|
displayName: "Workspace Repos Guard",
|
|
2079
2080
|
description: "Blocks writes and deletes that break the protected repo-checkout structure (~/.hasna/repos/clones canonical, plus legacy ~/workspace/repos during the migration window): writes to a protected root, top-level entry creation, non-org folders, and any delete under a protected root",
|
|
2080
2081
|
version: "0.1.0",
|
|
@@ -2093,7 +2094,7 @@ var init_registry = __esm(() => {
|
|
|
2093
2094
|
matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
2094
2095
|
tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
|
|
2095
2096
|
rewritesInput: true,
|
|
2096
|
-
timeoutSeconds:
|
|
2097
|
+
timeoutSeconds: 10
|
|
2097
2098
|
},
|
|
2098
2099
|
{
|
|
2099
2100
|
name: "checktests",
|
|
@@ -4500,6 +4501,139 @@ var init_resolve = __esm(() => {
|
|
|
4500
4501
|
init_manifest();
|
|
4501
4502
|
});
|
|
4502
4503
|
|
|
4504
|
+
// src/lib/native-safety.ts
|
|
4505
|
+
import { createHash } from "crypto";
|
|
4506
|
+
import { closeSync, constants, existsSync as existsSync7, fstatSync, lstatSync, openSync, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
|
|
4507
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join8 } from "path";
|
|
4508
|
+
import { homedir as homedir3 } from "os";
|
|
4509
|
+
function isNativeSafetyName(name) {
|
|
4510
|
+
return name === "trash-guard" || name === "workspace-repos-guard";
|
|
4511
|
+
}
|
|
4512
|
+
function validateBinding(b) {
|
|
4513
|
+
if (!b || !isNativeSafetyName(b.name) || ![b.runtime, b.worker, b.home].every((p) => typeof p === "string" && isAbsolute2(p) && !/[\0\r\n]/.test(p)) || ![b.runtimeSHA256, b.workerSHA256].every((h) => /^[0-9a-f]{64}$/.test(h)) || typeof b.path !== "string" || b.path.split(":").some((p) => !isAbsolute2(p) || /[\0\r\n]/.test(p)))
|
|
4514
|
+
throw new Error("Invalid native safety binding");
|
|
4515
|
+
}
|
|
4516
|
+
function buildNativeSafetyCommand(binding) {
|
|
4517
|
+
validateBinding(binding);
|
|
4518
|
+
const encoded = Buffer.from(JSON.stringify(binding)).toString("base64url");
|
|
4519
|
+
return prefix + [encoded, binding.runtime, binding.home, binding.path].map(quote).join(" ");
|
|
4520
|
+
}
|
|
4521
|
+
function nativeSafetyRegistration(command) {
|
|
4522
|
+
if (!command.startsWith(prefix))
|
|
4523
|
+
return;
|
|
4524
|
+
const encoded = command.slice(prefix.length).match(/^'([A-Za-z0-9_-]+)' /)?.[1];
|
|
4525
|
+
if (!encoded || encoded.length > 16384)
|
|
4526
|
+
return;
|
|
4527
|
+
try {
|
|
4528
|
+
const binding = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
4529
|
+
return buildNativeSafetyCommand(binding) === command ? binding.name : undefined;
|
|
4530
|
+
} catch {
|
|
4531
|
+
return;
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
function pinEligibleFile(file, maximum, executable = false) {
|
|
4535
|
+
if (!isAbsolute2(file) || realpathSync2(file) !== file)
|
|
4536
|
+
throw new Error("Native safety requires canonical files");
|
|
4537
|
+
for (let parent = dirname3(file);; ) {
|
|
4538
|
+
const s = lstatSync(parent);
|
|
4539
|
+
if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || s.mode & 18)
|
|
4540
|
+
throw new Error("Native safety requires trusted, non-writable file ancestry");
|
|
4541
|
+
if (parent === dirname3(parent))
|
|
4542
|
+
break;
|
|
4543
|
+
parent = dirname3(parent);
|
|
4544
|
+
}
|
|
4545
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
4546
|
+
try {
|
|
4547
|
+
const s = fstatSync(fd);
|
|
4548
|
+
if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || s.mode & 18 || s.size > maximum || executable && !(s.mode & 64))
|
|
4549
|
+
throw new Error("Native safety requires an eligible installed file");
|
|
4550
|
+
const bytes = readFileSync2(fd);
|
|
4551
|
+
if (bytes.length > maximum)
|
|
4552
|
+
throw new Error("Native safety file exceeds its bound");
|
|
4553
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
4554
|
+
} finally {
|
|
4555
|
+
closeSync(fd);
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
function installedNativeSafetyCommand(name) {
|
|
4559
|
+
const home = realpathSync2(homedir3());
|
|
4560
|
+
const runtime = realpathSync2(process.execPath);
|
|
4561
|
+
const candidates = [join8(import.meta.dir, "../../bin/native-safety-entry.js"), join8(import.meta.dir, "../bin/native-safety-entry.js"), join8(import.meta.dir, "native-safety-entry.js")];
|
|
4562
|
+
const worker = candidates.find(existsSync7);
|
|
4563
|
+
if (!worker)
|
|
4564
|
+
throw new Error("Bundled native safety runner is missing; build or reinstall @hasna/hooks before installing its guards.");
|
|
4565
|
+
const canonicalWorker = realpathSync2(worker);
|
|
4566
|
+
return buildNativeSafetyCommand({ name, runtime, runtimeSHA256: pinEligibleFile(runtime, 200000000, true), worker: canonicalWorker, workerSHA256: pinEligibleFile(canonicalWorker, 256000), home, path: `${dirname3(runtime)}:${join8(home, ".bun/bin")}:/usr/local/bin:/usr/bin:/bin` });
|
|
4567
|
+
}
|
|
4568
|
+
var supervisorSource, quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'", program2, shell, prefix;
|
|
4569
|
+
var init_native_safety = __esm(() => {
|
|
4570
|
+
supervisorSource = String.raw`async function supervise(encoded) {
|
|
4571
|
+
const fs = require("node:fs");
|
|
4572
|
+
const path = require("node:path");
|
|
4573
|
+
const crypto = require("node:crypto");
|
|
4574
|
+
const cp = require("node:child_process");
|
|
4575
|
+
const fail = () => { process.stderr.write("Native safety guard refused: verification or execution failed.\n"); process.exit(2); };
|
|
4576
|
+
try {
|
|
4577
|
+
const b = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
4578
|
+
const check = (file, hash, maximum) => {
|
|
4579
|
+
if (!path.isAbsolute(file) || fs.realpathSync(file) !== file) throw Error();
|
|
4580
|
+
for (let parent = path.dirname(file);;) {
|
|
4581
|
+
const s = fs.lstatSync(parent);
|
|
4582
|
+
if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022)) throw Error();
|
|
4583
|
+
if (parent === path.dirname(parent)) break;
|
|
4584
|
+
parent = path.dirname(parent);
|
|
4585
|
+
}
|
|
4586
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
4587
|
+
try {
|
|
4588
|
+
const s = fs.fstatSync(fd);
|
|
4589
|
+
if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022) || s.size > maximum) throw Error();
|
|
4590
|
+
const bytes = fs.readFileSync(fd);
|
|
4591
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== hash) throw Error();
|
|
4592
|
+
return bytes;
|
|
4593
|
+
} finally { fs.closeSync(fd); }
|
|
4594
|
+
};
|
|
4595
|
+
check(b.runtime, b.runtimeSHA256, 200_000_000);
|
|
4596
|
+
const bytes = check(b.worker, b.workerSHA256, 256_000);
|
|
4597
|
+
const deadline = setTimeout(fail, 1500);
|
|
4598
|
+
const chunks = []; let size = 0;
|
|
4599
|
+
for await (const chunk of process.stdin) {
|
|
4600
|
+
size += chunk.length;
|
|
4601
|
+
if (size > 1_048_576) throw Error();
|
|
4602
|
+
chunks.push(chunk);
|
|
4603
|
+
}
|
|
4604
|
+
clearTimeout(deadline);
|
|
4605
|
+
const input = Buffer.concat(chunks);
|
|
4606
|
+
const child = cp.spawnSync(b.runtime, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", bytes.toString("utf8"), b.name], {
|
|
4607
|
+
input, env: { HOME: b.home, PATH: b.path }, cwd: "/", timeout: 2500, killSignal: "SIGKILL", maxBuffer: 1_048_576,
|
|
4608
|
+
});
|
|
4609
|
+
if (child.error || child.status !== 0 || child.signal || child.stderr.length) throw Error();
|
|
4610
|
+
const result = JSON.parse(child.stdout.toString("utf8"));
|
|
4611
|
+
if (!result || Array.isArray(result) || Object.keys(result).length !== 1) throw Error();
|
|
4612
|
+
if (result.verdict === null) return;
|
|
4613
|
+
const output = result.verdict?.hookSpecificOutput;
|
|
4614
|
+
if (!output || Object.keys(result.verdict).length !== 1 || output.hookEventName !== "PreToolUse") throw Error();
|
|
4615
|
+
if (output.permissionDecision === "deny") {
|
|
4616
|
+
if (Object.keys(output).sort().join() !== "hookEventName,permissionDecision,permissionDecisionReason" || typeof output.permissionDecisionReason !== "string" || !output.permissionDecisionReason.trim()) throw Error();
|
|
4617
|
+
} else if (output.permissionDecision === "allow") {
|
|
4618
|
+
if (b.name !== "trash-guard" || Object.keys(output).sort().join() !== "hookEventName,permissionDecision,updatedInput" || !output.updatedInput || typeof output.updatedInput.command !== "string" || !output.updatedInput.command.trim()) throw Error();
|
|
4619
|
+
} else throw Error();
|
|
4620
|
+
process.stdout.write(JSON.stringify(result.verdict));
|
|
4621
|
+
} catch { fail(); }
|
|
4622
|
+
}`;
|
|
4623
|
+
program2 = `(${supervisorSource})(process.argv[1]);`;
|
|
4624
|
+
shell = `cd / || exit 2
|
|
4625
|
+
exec 3<&0 || exit 2
|
|
4626
|
+
/usr/bin/env -i HOME="$3" PATH="$4" "$2" --config=/dev/null --no-env-file --no-macros --no-install --eval ${quote(program2)} "$1" <&3 3<&- 2>/dev/null &
|
|
4627
|
+
runner=$!
|
|
4628
|
+
exec 3<&-
|
|
4629
|
+
(trap 'kill "$sleeper" 2>/dev/null; exit 0' TERM; /bin/sleep 6 & sleeper=$!; wait "$sleeper"; kill -KILL "$runner" 2>/dev/null) &
|
|
4630
|
+
watchdog=$!
|
|
4631
|
+
wait "$runner"; result=$?
|
|
4632
|
+
kill "$watchdog" 2>/dev/null; wait "$watchdog" 2>/dev/null
|
|
4633
|
+
if [ "$result" -ne 0 ]; then printf '%s\\n' 'Native safety guard refused: verification or execution failed.' >&2; exit 2; fi`;
|
|
4634
|
+
prefix = `/usr/bin/env -i /bin/sh -c ${quote(shell)} hasna-native-safety-v1 `;
|
|
4635
|
+
});
|
|
4636
|
+
|
|
4503
4637
|
// src/lib/mementos-options.ts
|
|
4504
4638
|
function validateMementosRegistration(name, value) {
|
|
4505
4639
|
if (value === undefined)
|
|
@@ -4570,6 +4704,9 @@ function mementosRunEnvironment(env, value) {
|
|
|
4570
4704
|
function registrationName(command) {
|
|
4571
4705
|
if (typeof command !== "string")
|
|
4572
4706
|
return;
|
|
4707
|
+
const native = nativeSafetyRegistration(command);
|
|
4708
|
+
if (native)
|
|
4709
|
+
return native;
|
|
4573
4710
|
if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
|
|
4574
4711
|
try {
|
|
4575
4712
|
retainedMementosRegistration([command.trim()]);
|
|
@@ -4662,8 +4799,7 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
|
4662
4799
|
if (!Array.isArray(hooks))
|
|
4663
4800
|
return false;
|
|
4664
4801
|
return hooks.some((entry) => entry?.hooks?.some((h) => {
|
|
4665
|
-
|
|
4666
|
-
if (!match || match[1] !== name)
|
|
4802
|
+
if (registrationName(h?.command) !== name)
|
|
4667
4803
|
return false;
|
|
4668
4804
|
if (!matcher)
|
|
4669
4805
|
return true;
|
|
@@ -4675,6 +4811,7 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
|
4675
4811
|
}
|
|
4676
4812
|
var REGISTRATION_COMMAND_RE;
|
|
4677
4813
|
var init_registration = __esm(() => {
|
|
4814
|
+
init_native_safety();
|
|
4678
4815
|
init_resolve();
|
|
4679
4816
|
REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|hook-([\w-]+))$/;
|
|
4680
4817
|
});
|
|
@@ -4873,9 +5010,9 @@ var init_migrations = __esm(() => {
|
|
|
4873
5010
|
});
|
|
4874
5011
|
|
|
4875
5012
|
// src/db/legacy-import.ts
|
|
4876
|
-
import { existsSync as
|
|
4877
|
-
import { join as
|
|
4878
|
-
import { homedir as
|
|
5013
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
5014
|
+
import { join as join9 } from "path";
|
|
5015
|
+
import { homedir as homedir4 } from "os";
|
|
4879
5016
|
function ensureMetaTable(db) {
|
|
4880
5017
|
db.exec(`
|
|
4881
5018
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
@@ -4898,7 +5035,7 @@ function nanoid() {
|
|
|
4898
5035
|
function importJsonlFile(db, filePath) {
|
|
4899
5036
|
let count = 0;
|
|
4900
5037
|
try {
|
|
4901
|
-
const lines =
|
|
5038
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
4902
5039
|
`).filter(Boolean);
|
|
4903
5040
|
for (const line of lines) {
|
|
4904
5041
|
try {
|
|
@@ -4924,7 +5061,7 @@ function importJsonlFile(db, filePath) {
|
|
|
4924
5061
|
function importErrorsLog(db, filePath) {
|
|
4925
5062
|
let count = 0;
|
|
4926
5063
|
try {
|
|
4927
|
-
const lines =
|
|
5064
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
4928
5065
|
`).filter(Boolean);
|
|
4929
5066
|
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
4930
5067
|
for (const line of lines) {
|
|
@@ -4949,25 +5086,25 @@ function importErrorsLog(db, filePath) {
|
|
|
4949
5086
|
} catch {}
|
|
4950
5087
|
return count;
|
|
4951
5088
|
}
|
|
4952
|
-
function runLegacyImport(db, homeDir =
|
|
5089
|
+
function runLegacyImport(db, homeDir = homedir4()) {
|
|
4953
5090
|
try {
|
|
4954
5091
|
if (isAlreadyDone(db))
|
|
4955
5092
|
return;
|
|
4956
5093
|
let total = 0;
|
|
4957
|
-
const claudeProjectsDir =
|
|
4958
|
-
if (
|
|
5094
|
+
const claudeProjectsDir = join9(homeDir, ".claude", "projects");
|
|
5095
|
+
if (existsSync8(claudeProjectsDir)) {
|
|
4959
5096
|
try {
|
|
4960
5097
|
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
4961
5098
|
for (const dir of projectDirs) {
|
|
4962
|
-
const projectDir =
|
|
5099
|
+
const projectDir = join9(claudeProjectsDir, dir);
|
|
4963
5100
|
try {
|
|
4964
5101
|
const files = readdirSync2(projectDir);
|
|
4965
5102
|
for (const file of files) {
|
|
4966
5103
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
4967
|
-
total += importJsonlFile(db,
|
|
5104
|
+
total += importJsonlFile(db, join9(projectDir, file));
|
|
4968
5105
|
}
|
|
4969
5106
|
if (file === "errors.log") {
|
|
4970
|
-
total += importErrorsLog(db,
|
|
5107
|
+
total += importErrorsLog(db, join9(projectDir, file));
|
|
4971
5108
|
}
|
|
4972
5109
|
}
|
|
4973
5110
|
} catch {}
|
|
@@ -5014,8 +5151,8 @@ __export(exports_db, {
|
|
|
5014
5151
|
allowLocalStore: () => allowLocalStore
|
|
5015
5152
|
});
|
|
5016
5153
|
import { Database } from "bun:sqlite";
|
|
5017
|
-
import { existsSync as
|
|
5018
|
-
import { join as
|
|
5154
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
5155
|
+
import { join as join10 } from "path";
|
|
5019
5156
|
function refuseLocalStore(message) {
|
|
5020
5157
|
localStoreRefusal = message;
|
|
5021
5158
|
}
|
|
@@ -5030,8 +5167,8 @@ function localStoreRefusalMessage() {
|
|
|
5030
5167
|
}
|
|
5031
5168
|
function resolveDataDir() {
|
|
5032
5169
|
const effective = getEffectiveDataRoot();
|
|
5033
|
-
const oldDir =
|
|
5034
|
-
if (!
|
|
5170
|
+
const oldDir = join10(getHomeDir(), ".hooks");
|
|
5171
|
+
if (!existsSync9(effective) && existsSync9(oldDir)) {
|
|
5035
5172
|
mkdirSync2(effective, { recursive: true });
|
|
5036
5173
|
cpSync(oldDir, effective, { recursive: true });
|
|
5037
5174
|
}
|
|
@@ -5042,11 +5179,11 @@ function getDbPath() {
|
|
|
5042
5179
|
if (explicitDb)
|
|
5043
5180
|
return explicitDb;
|
|
5044
5181
|
const dataDir3 = resolveDataDir();
|
|
5045
|
-
return
|
|
5182
|
+
return join10(dataDir3, "hooks.db");
|
|
5046
5183
|
}
|
|
5047
5184
|
function ensureDir(dbPath) {
|
|
5048
5185
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
5049
|
-
if (dir && !
|
|
5186
|
+
if (dir && !existsSync9(dir)) {
|
|
5050
5187
|
mkdirSync2(dir, { recursive: true });
|
|
5051
5188
|
}
|
|
5052
5189
|
}
|
|
@@ -5056,7 +5193,7 @@ function getDb() {
|
|
|
5056
5193
|
if (instance)
|
|
5057
5194
|
return instance;
|
|
5058
5195
|
const dbPath = getDbPath();
|
|
5059
|
-
const isNew = dbPath === ":memory:" || !
|
|
5196
|
+
const isNew = dbPath === ":memory:" || !existsSync9(dbPath);
|
|
5060
5197
|
ensureDir(dbPath);
|
|
5061
5198
|
instance = new Database(dbPath);
|
|
5062
5199
|
instance.exec("PRAGMA busy_timeout=5000");
|
|
@@ -5118,12 +5255,12 @@ __export(exports_store, {
|
|
|
5118
5255
|
checkScriptHash: () => checkScriptHash,
|
|
5119
5256
|
LockFileError: () => LockFileError
|
|
5120
5257
|
});
|
|
5121
|
-
import { createHash } from "crypto";
|
|
5122
|
-
import { existsSync as
|
|
5258
|
+
import { createHash as createHash2 } from "crypto";
|
|
5259
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
5123
5260
|
import { readFile as readFile2 } from "fs/promises";
|
|
5124
5261
|
import { randomBytes } from "crypto";
|
|
5125
5262
|
function sha256Of(input) {
|
|
5126
|
-
return
|
|
5263
|
+
return createHash2("sha256").update(input).digest("hex");
|
|
5127
5264
|
}
|
|
5128
5265
|
async function sha256File(path) {
|
|
5129
5266
|
const buf = await readFile2(path);
|
|
@@ -5184,11 +5321,11 @@ function assertLockShape(parsed, path) {
|
|
|
5184
5321
|
}
|
|
5185
5322
|
function readLock() {
|
|
5186
5323
|
const path = getLockPath();
|
|
5187
|
-
if (!
|
|
5324
|
+
if (!existsSync10(path))
|
|
5188
5325
|
return { hooks: {} };
|
|
5189
5326
|
let raw;
|
|
5190
5327
|
try {
|
|
5191
|
-
raw =
|
|
5328
|
+
raw = readFileSync4(path, "utf-8");
|
|
5192
5329
|
} catch {
|
|
5193
5330
|
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
5194
5331
|
}
|
|
@@ -5291,7 +5428,7 @@ async function verifyScriptHash(name, scriptPath) {
|
|
|
5291
5428
|
return checkScriptHash(name, await sha256File(scriptPath));
|
|
5292
5429
|
}
|
|
5293
5430
|
function retrustHook(name, scriptPath, version, source) {
|
|
5294
|
-
const actual = sha256Of(
|
|
5431
|
+
const actual = sha256Of(readFileSync4(scriptPath));
|
|
5295
5432
|
const db = storeDb();
|
|
5296
5433
|
if (db) {
|
|
5297
5434
|
upsertHookRecord(db, {
|
|
@@ -5318,15 +5455,15 @@ var init_store = __esm(() => {
|
|
|
5318
5455
|
});
|
|
5319
5456
|
|
|
5320
5457
|
// src/lib/codex-settings.ts
|
|
5321
|
-
import { closeSync, existsSync as
|
|
5322
|
-
import { dirname as
|
|
5458
|
+
import { closeSync as closeSync2, existsSync as existsSync11, lstatSync as lstatSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
5459
|
+
import { dirname as dirname4 } from "path";
|
|
5323
5460
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5324
5461
|
function current(path) {
|
|
5325
5462
|
try {
|
|
5326
|
-
const stat =
|
|
5463
|
+
const stat = lstatSync2(path);
|
|
5327
5464
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
|
|
5328
5465
|
throw new Error("invalid_file");
|
|
5329
|
-
return
|
|
5466
|
+
return readFileSync5(path, "utf8");
|
|
5330
5467
|
} catch (error) {
|
|
5331
5468
|
if (error.code === "ENOENT")
|
|
5332
5469
|
return null;
|
|
@@ -5358,11 +5495,11 @@ function writeCodexSettings(path, settings) {
|
|
|
5358
5495
|
const snapshot = snapshots.get(settings);
|
|
5359
5496
|
if (!snapshot || snapshot.path !== path)
|
|
5360
5497
|
throw new Error("Codex settings require a complete current read before writing.");
|
|
5361
|
-
mkdirSync4(
|
|
5498
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
5362
5499
|
const lock = `${path}.hooks-write.lock`;
|
|
5363
5500
|
let fd;
|
|
5364
5501
|
try {
|
|
5365
|
-
fd =
|
|
5502
|
+
fd = openSync2(lock, "wx", 384);
|
|
5366
5503
|
} catch {
|
|
5367
5504
|
throw new Error("Codex settings are being edited; retry after the active writer finishes.");
|
|
5368
5505
|
}
|
|
@@ -5378,9 +5515,9 @@ function writeCodexSettings(path, settings) {
|
|
|
5378
5515
|
renameSync2(temp, path);
|
|
5379
5516
|
snapshots.set(settings, { path, raw: next });
|
|
5380
5517
|
} finally {
|
|
5381
|
-
if (
|
|
5518
|
+
if (existsSync11(temp))
|
|
5382
5519
|
unlinkSync(temp);
|
|
5383
|
-
|
|
5520
|
+
closeSync2(fd);
|
|
5384
5521
|
unlinkSync(lock);
|
|
5385
5522
|
}
|
|
5386
5523
|
}
|
|
@@ -5409,9 +5546,9 @@ __export(exports_installer, {
|
|
|
5409
5546
|
detectRewriteConflict: () => detectRewriteConflict,
|
|
5410
5547
|
buildCodewithTomlFragment: () => buildCodewithTomlFragment
|
|
5411
5548
|
});
|
|
5412
|
-
import { existsSync as
|
|
5413
|
-
import { join as
|
|
5414
|
-
import { homedir as
|
|
5549
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync6, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
5550
|
+
import { join as join11, dirname as dirname5 } from "path";
|
|
5551
|
+
import { homedir as homedir5 } from "os";
|
|
5415
5552
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5416
5553
|
function normalizeHookName(name) {
|
|
5417
5554
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
@@ -5424,8 +5561,7 @@ function removeHookEntriesByName(entries, hookName) {
|
|
|
5424
5561
|
if (!Array.isArray(entry.hooks))
|
|
5425
5562
|
return [entry];
|
|
5426
5563
|
const kept = entry.hooks.filter((h) => {
|
|
5427
|
-
|
|
5428
|
-
return !(match && match[1] === hookName);
|
|
5564
|
+
return registrationName(h.command) !== hookName;
|
|
5429
5565
|
});
|
|
5430
5566
|
return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
|
|
5431
5567
|
});
|
|
@@ -5459,29 +5595,29 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
5459
5595
|
return globalOverride;
|
|
5460
5596
|
const dir = getTargetSettingsDir(target);
|
|
5461
5597
|
if (target === "codex")
|
|
5462
|
-
return
|
|
5598
|
+
return join11(scope === "project" ? process.cwd() : homedir5(), dir, "hooks.json");
|
|
5463
5599
|
if (scope === "project") {
|
|
5464
|
-
return target === "codewith" ?
|
|
5600
|
+
return target === "codewith" ? join11(process.cwd(), dir, "config.toml") : join11(process.cwd(), dir, "settings.json");
|
|
5465
5601
|
}
|
|
5466
|
-
return target === "codewith" ?
|
|
5602
|
+
return target === "codewith" ? join11(homedir5(), dir, "config.toml") : join11(homedir5(), dir, "settings.json");
|
|
5467
5603
|
}
|
|
5468
5604
|
function getHookPath(name) {
|
|
5469
5605
|
const shortName = shortHookName(name);
|
|
5470
5606
|
const resolved = resolveHookDir(shortName);
|
|
5471
5607
|
if (resolved)
|
|
5472
5608
|
return resolved;
|
|
5473
|
-
return
|
|
5609
|
+
return join11(HOOKS_DIR, normalizeHookName(shortName));
|
|
5474
5610
|
}
|
|
5475
5611
|
function hookExists(name) {
|
|
5476
|
-
return
|
|
5612
|
+
return existsSync12(getHookPath(name));
|
|
5477
5613
|
}
|
|
5478
5614
|
function readSettings(scope = "global", target = "claude") {
|
|
5479
5615
|
const path = getSettingsPath(scope, target);
|
|
5480
5616
|
if (target === "codex")
|
|
5481
5617
|
return readCodexSettings(path);
|
|
5482
5618
|
try {
|
|
5483
|
-
if (
|
|
5484
|
-
return JSON.parse(
|
|
5619
|
+
if (existsSync12(path)) {
|
|
5620
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
5485
5621
|
}
|
|
5486
5622
|
} catch (error) {
|
|
5487
5623
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -5492,8 +5628,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
|
|
|
5492
5628
|
const path = getSettingsPath(scope, target);
|
|
5493
5629
|
if (target === "codex")
|
|
5494
5630
|
return writeCodexSettings(path, settings);
|
|
5495
|
-
const dir =
|
|
5496
|
-
if (!
|
|
5631
|
+
const dir = dirname5(path);
|
|
5632
|
+
if (!existsSync12(dir)) {
|
|
5497
5633
|
mkdirSync5(dir, { recursive: true });
|
|
5498
5634
|
}
|
|
5499
5635
|
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
@@ -5521,6 +5657,9 @@ function codewithTimeout(name) {
|
|
|
5521
5657
|
switch (name) {
|
|
5522
5658
|
case "mementos-context":
|
|
5523
5659
|
return 12;
|
|
5660
|
+
case "trash-guard":
|
|
5661
|
+
case "workspace-repos-guard":
|
|
5662
|
+
return 10;
|
|
5524
5663
|
case "knowledge-context":
|
|
5525
5664
|
return 6;
|
|
5526
5665
|
case "session-start":
|
|
@@ -5530,7 +5669,6 @@ function codewithTimeout(name) {
|
|
|
5530
5669
|
case "prompt-guard":
|
|
5531
5670
|
return 3;
|
|
5532
5671
|
case "worktree-guard":
|
|
5533
|
-
case "trash-guard":
|
|
5534
5672
|
case "stop-sync":
|
|
5535
5673
|
return 5;
|
|
5536
5674
|
default:
|
|
@@ -5559,11 +5697,13 @@ function codewithStatusMessage(name) {
|
|
|
5559
5697
|
}
|
|
5560
5698
|
function buildCodewithTomlFragment(name, profile, mementos) {
|
|
5561
5699
|
const shortName = shortHookName(name);
|
|
5562
|
-
|
|
5700
|
+
if (isNativeSafetyName(shortName) && profile)
|
|
5701
|
+
throw new Error("Bundled native safety guards do not select registry profiles");
|
|
5702
|
+
const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
|
|
5563
5703
|
if (!meta)
|
|
5564
5704
|
throw new Error(`Hook '${shortName}' not found`);
|
|
5565
5705
|
validateMementosRegistration(shortName, mementos);
|
|
5566
|
-
const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
|
|
5706
|
+
const command = isNativeSafetyName(shortName) ? installedNativeSafetyCommand(shortName) : (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
|
|
5567
5707
|
const matcher = codewithMatcher(meta.matcher);
|
|
5568
5708
|
const fragments = [];
|
|
5569
5709
|
for (const event of getHookEvents(meta)) {
|
|
@@ -5588,17 +5728,17 @@ function buildCodewithTomlFragment(name, profile, mementos) {
|
|
|
5588
5728
|
function readCodewithConfig(scope, configPath) {
|
|
5589
5729
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5590
5730
|
try {
|
|
5591
|
-
return
|
|
5731
|
+
return existsSync12(path) ? readFileSync6(path, "utf-8") : "";
|
|
5592
5732
|
} catch {
|
|
5593
5733
|
return "";
|
|
5594
5734
|
}
|
|
5595
5735
|
}
|
|
5596
5736
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
5597
5737
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5598
|
-
const dir =
|
|
5599
|
-
if (!
|
|
5738
|
+
const dir = dirname5(path);
|
|
5739
|
+
if (!existsSync12(dir))
|
|
5600
5740
|
mkdirSync5(dir, { recursive: true });
|
|
5601
|
-
const existing =
|
|
5741
|
+
const existing = existsSync12(path) ? readFileSync6(path, "utf-8") : "";
|
|
5602
5742
|
const sep2 = existing.trim() ? `
|
|
5603
5743
|
|
|
5604
5744
|
` : "";
|
|
@@ -5606,7 +5746,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
|
|
|
5606
5746
|
return path;
|
|
5607
5747
|
}
|
|
5608
5748
|
function detectConflict(name, scope, target) {
|
|
5609
|
-
const meta = resolveHookMeta(name);
|
|
5749
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
5610
5750
|
if (!meta || !meta.matcher)
|
|
5611
5751
|
return;
|
|
5612
5752
|
const events = new Set(getHookEvents(meta));
|
|
@@ -5626,7 +5766,7 @@ function detectConflict(name, scope, target) {
|
|
|
5626
5766
|
return;
|
|
5627
5767
|
}
|
|
5628
5768
|
function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
5629
|
-
const meta = resolveHookMeta(name);
|
|
5769
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
5630
5770
|
if (!meta?.matcher || !meta.rewritesInput)
|
|
5631
5771
|
return;
|
|
5632
5772
|
if (!getHookEvents(meta).includes("PreToolUse"))
|
|
@@ -5647,7 +5787,10 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
|
5647
5787
|
}
|
|
5648
5788
|
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
|
|
5649
5789
|
const shortName = shortHookName(name);
|
|
5650
|
-
if (
|
|
5790
|
+
if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
|
|
5791
|
+
return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
|
|
5792
|
+
}
|
|
5793
|
+
if (!isNativeSafetyName(shortName) && !hookExists(shortName)) {
|
|
5651
5794
|
return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
|
|
5652
5795
|
}
|
|
5653
5796
|
if (target === "codewith") {
|
|
@@ -5739,7 +5882,7 @@ function installHook(name, options = {}) {
|
|
|
5739
5882
|
}
|
|
5740
5883
|
if (target === "all") {
|
|
5741
5884
|
const shortName = shortHookName(name);
|
|
5742
|
-
const meta = resolveHookMeta(shortName);
|
|
5885
|
+
const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
|
|
5743
5886
|
if (meta) {
|
|
5744
5887
|
const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
|
|
5745
5888
|
if (unsupportedTargets.length > 0) {
|
|
@@ -5772,7 +5915,7 @@ function installHook(name, options = {}) {
|
|
|
5772
5915
|
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
|
|
5773
5916
|
}
|
|
5774
5917
|
function registerHook(name, scope = "global", target = "claude", profile, mementos) {
|
|
5775
|
-
const meta = resolveHookMeta(name);
|
|
5918
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
5776
5919
|
if (!meta)
|
|
5777
5920
|
return;
|
|
5778
5921
|
const eventKeys = getHookEvents(meta).map((event) => {
|
|
@@ -5789,8 +5932,17 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
5789
5932
|
const settings = readSettings(scope, target);
|
|
5790
5933
|
if (!settings.hooks)
|
|
5791
5934
|
settings.hooks = {};
|
|
5935
|
+
if (isNativeSafetyName(name)) {
|
|
5936
|
+
for (const groups of Object.values(settings.hooks))
|
|
5937
|
+
for (const group of groups)
|
|
5938
|
+
for (const hook of group.hooks ?? []) {
|
|
5939
|
+
if (typeof hook.command === "string" && /(?:^|\s)hasna-native-safety-v\d+(?:\s|$)/.test(hook.command) && !registrationName(hook.command)) {
|
|
5940
|
+
throw new Error("Unknown managed safety definition; reconcile its exact version before replacing it.");
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5792
5944
|
const retained = name === "mementos-context" && mementos === undefined ? retainedMementosRegistration(Object.values(settings.hooks).flatMap((groups) => Array.isArray(groups) ? groups.flatMap((group) => Array.isArray(group.hooks) ? group.hooks.map((hook) => hook.command).filter((command) => typeof command === "string") : []) : [])) : undefined;
|
|
5793
|
-
const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
5945
|
+
const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
5794
5946
|
if (target === "codex") {
|
|
5795
5947
|
const positions = [];
|
|
5796
5948
|
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
@@ -5798,9 +5950,11 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
5798
5950
|
for (let index = 0;index < entry.hooks.length; index++) {
|
|
5799
5951
|
const command = entry.hooks[index]?.command;
|
|
5800
5952
|
const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
|
|
5801
|
-
if (match && (match[1] ?? match[2]) === name) {
|
|
5953
|
+
if (match && (match[1] ?? match[2]) === name || registrationName(command) === name) {
|
|
5802
5954
|
if (registrationName(command) !== name)
|
|
5803
5955
|
throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
|
|
5956
|
+
if (isNativeSafetyName(name) && entry.hooks[index].async !== undefined && entry.hooks[index].async !== false)
|
|
5957
|
+
throw new Error("Asynchronous safety hooks cannot block; reconcile this registration before updating it.");
|
|
5804
5958
|
positions.push({ event, entry, index });
|
|
5805
5959
|
}
|
|
5806
5960
|
}
|
|
@@ -5864,12 +6018,20 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
|
|
|
5864
6018
|
if (target === "codewith") {
|
|
5865
6019
|
const config = readCodewithConfig(scope);
|
|
5866
6020
|
const registered2 = [];
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
6021
|
+
try {
|
|
6022
|
+
const parsed = Bun.TOML.parse(config);
|
|
6023
|
+
for (const groups of Object.values(parsed.hooks ?? {})) {
|
|
6024
|
+
if (!Array.isArray(groups))
|
|
6025
|
+
continue;
|
|
6026
|
+
for (const group of groups)
|
|
6027
|
+
for (const hook of group.hooks ?? []) {
|
|
6028
|
+
const name = registrationName(hook.command);
|
|
6029
|
+
if (name)
|
|
6030
|
+
registered2.push(name);
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
6033
|
+
} catch {
|
|
6034
|
+
return [];
|
|
5873
6035
|
}
|
|
5874
6036
|
return [...new Set(registered2)];
|
|
5875
6037
|
}
|
|
@@ -5914,9 +6076,9 @@ function removeHookForTarget(name, scope, target) {
|
|
|
5914
6076
|
}
|
|
5915
6077
|
function readSettingsAt(path) {
|
|
5916
6078
|
try {
|
|
5917
|
-
if (!
|
|
6079
|
+
if (!existsSync12(path))
|
|
5918
6080
|
return;
|
|
5919
|
-
const raw =
|
|
6081
|
+
const raw = readFileSync6(path, "utf-8");
|
|
5920
6082
|
return { raw, settings: JSON.parse(raw) };
|
|
5921
6083
|
} catch {
|
|
5922
6084
|
return;
|
|
@@ -5950,7 +6112,7 @@ function pruneStaleRegistrations(scope = "global") {
|
|
|
5950
6112
|
staleCommandsByEvent.set(finding.event, commands);
|
|
5951
6113
|
}
|
|
5952
6114
|
const settings = target === "codex" ? readCodexSettings(path) : parsed.settings;
|
|
5953
|
-
if (target === "codex" &&
|
|
6115
|
+
if (target === "codex" && readFileSync6(path, "utf8") !== parsed.raw)
|
|
5954
6116
|
throw new Error("Codex settings changed before pruning; retry.");
|
|
5955
6117
|
for (const [eventKey, commands] of staleCommandsByEvent) {
|
|
5956
6118
|
const entries = settings.hooks?.[eventKey];
|
|
@@ -6038,10 +6200,10 @@ function codewithHasHookEntryInText(configText, name) {
|
|
|
6038
6200
|
}
|
|
6039
6201
|
function codewithHasHookEntry(name, scope = "global") {
|
|
6040
6202
|
const path = getSettingsPath(scope, "codewith");
|
|
6041
|
-
if (!
|
|
6203
|
+
if (!existsSync12(path))
|
|
6042
6204
|
return false;
|
|
6043
6205
|
try {
|
|
6044
|
-
return codewithHasHookEntryInText(
|
|
6206
|
+
return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
|
|
6045
6207
|
} catch {
|
|
6046
6208
|
return false;
|
|
6047
6209
|
}
|
|
@@ -6050,12 +6212,12 @@ function removeStoreDirFailClosed(dir) {
|
|
|
6050
6212
|
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
6051
6213
|
for (const entry of entries) {
|
|
6052
6214
|
if (entry.isDirectory()) {
|
|
6053
|
-
removeStoreDirFailClosed(
|
|
6215
|
+
removeStoreDirFailClosed(join11(dir, entry.name));
|
|
6054
6216
|
}
|
|
6055
6217
|
}
|
|
6056
6218
|
for (const entry of entries) {
|
|
6057
6219
|
if (!entry.isDirectory()) {
|
|
6058
|
-
unlinkSync2(
|
|
6220
|
+
unlinkSync2(join11(dir, entry.name));
|
|
6059
6221
|
}
|
|
6060
6222
|
}
|
|
6061
6223
|
rmdirSync(dir);
|
|
@@ -6086,8 +6248,8 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
6086
6248
|
const registrationsRemaining = [];
|
|
6087
6249
|
if (target === "codewith" || target === "all") {
|
|
6088
6250
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
6089
|
-
if (
|
|
6090
|
-
const before =
|
|
6251
|
+
if (existsSync12(codewithPath)) {
|
|
6252
|
+
const before = readFileSync6(codewithPath, "utf-8");
|
|
6091
6253
|
const after = removeCodewithHookEntry(before, shortName);
|
|
6092
6254
|
if (after.removed) {
|
|
6093
6255
|
writeFileSync4(codewithPath, after.text, "utf-8");
|
|
@@ -6134,8 +6296,9 @@ var init_installer = __esm(() => {
|
|
|
6134
6296
|
init_registration();
|
|
6135
6297
|
init_store();
|
|
6136
6298
|
init_codex_settings();
|
|
6137
|
-
|
|
6138
|
-
|
|
6299
|
+
init_native_safety();
|
|
6300
|
+
__dirname2 = dirname5(fileURLToPath2(import.meta.url));
|
|
6301
|
+
HOOKS_DIR = existsSync12(join11(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join11(__dirname2, "..", "..", "hooks") : join11(__dirname2, "..", "hooks");
|
|
6139
6302
|
EVENT_MAP = {
|
|
6140
6303
|
claude: {
|
|
6141
6304
|
PreToolUse: "PreToolUse",
|
|
@@ -6248,12 +6411,12 @@ var init_redact = __esm(() => {
|
|
|
6248
6411
|
});
|
|
6249
6412
|
|
|
6250
6413
|
// src/lib/profiles.ts
|
|
6251
|
-
import { existsSync as
|
|
6252
|
-
import { join as
|
|
6414
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
|
|
6415
|
+
import { join as join12 } from "path";
|
|
6253
6416
|
function getProfilesDir() {
|
|
6254
|
-
const newDir =
|
|
6255
|
-
const oldDir =
|
|
6256
|
-
if (!
|
|
6417
|
+
const newDir = join12(getEffectiveDataRoot(), "profiles");
|
|
6418
|
+
const oldDir = join12(getHomeDir(), ".hooks", "profiles");
|
|
6419
|
+
if (!existsSync13(newDir) && existsSync13(oldDir)) {
|
|
6257
6420
|
mkdirSync6(newDir, { recursive: true });
|
|
6258
6421
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
6259
6422
|
}
|
|
@@ -6261,12 +6424,12 @@ function getProfilesDir() {
|
|
|
6261
6424
|
}
|
|
6262
6425
|
function ensureProfilesDir() {
|
|
6263
6426
|
const dir = getProfilesDir();
|
|
6264
|
-
if (!
|
|
6427
|
+
if (!existsSync13(dir)) {
|
|
6265
6428
|
mkdirSync6(dir, { recursive: true });
|
|
6266
6429
|
}
|
|
6267
6430
|
}
|
|
6268
6431
|
function profilePath(id) {
|
|
6269
|
-
return
|
|
6432
|
+
return join12(getProfilesDir(), `${id}.json`);
|
|
6270
6433
|
}
|
|
6271
6434
|
function shortUuid() {
|
|
6272
6435
|
return crypto.randomUUID().slice(0, 8);
|
|
@@ -6292,23 +6455,23 @@ function createProfile(input) {
|
|
|
6292
6455
|
function getProfile(id) {
|
|
6293
6456
|
const path = profilePath(id);
|
|
6294
6457
|
try {
|
|
6295
|
-
if (!
|
|
6458
|
+
if (!existsSync13(path))
|
|
6296
6459
|
return null;
|
|
6297
|
-
return JSON.parse(
|
|
6460
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
6298
6461
|
} catch {
|
|
6299
6462
|
return null;
|
|
6300
6463
|
}
|
|
6301
6464
|
}
|
|
6302
6465
|
function listProfiles() {
|
|
6303
6466
|
const dir = getProfilesDir();
|
|
6304
|
-
if (!
|
|
6467
|
+
if (!existsSync13(dir))
|
|
6305
6468
|
return [];
|
|
6306
6469
|
try {
|
|
6307
6470
|
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
6308
6471
|
const profiles = [];
|
|
6309
6472
|
for (const file of files) {
|
|
6310
6473
|
try {
|
|
6311
|
-
const content =
|
|
6474
|
+
const content = readFileSync7(join12(dir, file), "utf-8");
|
|
6312
6475
|
profiles.push(JSON.parse(content));
|
|
6313
6476
|
} catch {}
|
|
6314
6477
|
}
|
|
@@ -6338,7 +6501,7 @@ function importProfiles(profiles) {
|
|
|
6338
6501
|
continue;
|
|
6339
6502
|
}
|
|
6340
6503
|
const path = profilePath(profile.agent_id);
|
|
6341
|
-
if (
|
|
6504
|
+
if (existsSync13(path)) {
|
|
6342
6505
|
skipped++;
|
|
6343
6506
|
continue;
|
|
6344
6507
|
}
|
|
@@ -6352,14 +6515,19 @@ var init_profiles = __esm(() => {
|
|
|
6352
6515
|
init_app_home();
|
|
6353
6516
|
});
|
|
6354
6517
|
|
|
6355
|
-
//
|
|
6518
|
+
// ../contracts/dist/client/transport.js
|
|
6356
6519
|
import { isIP as isIP2 } from "net";
|
|
6357
6520
|
import { spawnSync } from "child_process";
|
|
6358
|
-
import { closeSync as
|
|
6359
|
-
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
6360
|
-
import { createRequire as createRequire2 } from "module";
|
|
6521
|
+
import { closeSync as closeSync22, fstatSync as fstatSync2, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
|
|
6522
|
+
import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
|
|
6361
6523
|
import { hostname as osHostname } from "os";
|
|
6362
|
-
import { isAbsolute as
|
|
6524
|
+
import { isAbsolute as isAbsolute3, join as join22 } from "path";
|
|
6525
|
+
import { createRequire as createRequire2 } from "module";
|
|
6526
|
+
import { fileURLToPath as fileURLToPath22 } from "url";
|
|
6527
|
+
import { closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3, readFileSync as readFileSync8, realpathSync as realpathSync3, statSync } from "fs";
|
|
6528
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
6529
|
+
import { basename as basename2, dirname as dirname6, join as join13, relative, sep as sep2 } from "path";
|
|
6530
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
|
|
6363
6531
|
function envToken(name) {
|
|
6364
6532
|
return name.toUpperCase().replace(/-/g, "_");
|
|
6365
6533
|
}
|
|
@@ -6376,27 +6544,133 @@ function credentialOverrideEnvKey(name) {
|
|
|
6376
6544
|
function credentialPointerEnvKey(name) {
|
|
6377
6545
|
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
6378
6546
|
}
|
|
6547
|
+
function e(e2, n, r) {
|
|
6548
|
+
throw new Error(r ? `No known conditions for "${n}" specifier in "${e2}" package` : `Missing "${n}" specifier in "${e2}" package`);
|
|
6549
|
+
}
|
|
6550
|
+
function n(n2, i, o, f) {
|
|
6551
|
+
let s, u, l = r(n2, o), c = function(e2) {
|
|
6552
|
+
let n3 = new Set(["default", ...e2.conditions || []]);
|
|
6553
|
+
return e2.unsafe || n3.add(e2.require ? "require" : "import"), e2.unsafe || n3.add(e2.browser ? "browser" : "node"), n3;
|
|
6554
|
+
}(f || {}), a = i[l];
|
|
6555
|
+
if (a === undefined) {
|
|
6556
|
+
let e2, n3, r, t;
|
|
6557
|
+
for (t in i)
|
|
6558
|
+
n3 && t.length < n3.length || (t[t.length - 1] === "/" && l.startsWith(t) ? (u = l.substring(t.length), n3 = t) : t.length > 1 && (r = t.indexOf("*", 1), ~r && (e2 = RegExp("^" + t.substring(0, r) + "(.*)" + t.substring(1 + r) + "$").exec(l), e2 && e2[1] && (u = e2[1], n3 = t))));
|
|
6559
|
+
a = i[n3];
|
|
6560
|
+
}
|
|
6561
|
+
return a || e(n2, l), s = t(a, c), s || e(n2, l, 1), u && function(e2, n3) {
|
|
6562
|
+
let r, t = 0, i2 = e2.length, o2 = /[*]/g, f2 = /[/]$/;
|
|
6563
|
+
for (;t < i2; t++)
|
|
6564
|
+
e2[t] = o2.test(r = e2[t]) ? r.replace(o2, n3) : f2.test(r) ? r + n3 : r;
|
|
6565
|
+
}(s, u), s;
|
|
6566
|
+
}
|
|
6567
|
+
function r(e2, n2, r2) {
|
|
6568
|
+
if (e2 === n2 || n2 === ".")
|
|
6569
|
+
return ".";
|
|
6570
|
+
let t = e2 + "/", i = t.length, o = n2.slice(0, i) === t, f = o ? n2.slice(i) : n2;
|
|
6571
|
+
return f[0] === "#" ? f : o || !r2 ? f.slice(0, 2) === "./" ? f : "./" + f : f;
|
|
6572
|
+
}
|
|
6573
|
+
function t(e2, n2, r2) {
|
|
6574
|
+
if (e2) {
|
|
6575
|
+
if (typeof e2 == "string")
|
|
6576
|
+
return r2 && r2.add(e2), [e2];
|
|
6577
|
+
let i, o;
|
|
6578
|
+
if (Array.isArray(e2)) {
|
|
6579
|
+
for (o = r2 || new Set, i = 0;i < e2.length; i++)
|
|
6580
|
+
t(e2[i], n2, o);
|
|
6581
|
+
if (!r2 && o.size)
|
|
6582
|
+
return [...o];
|
|
6583
|
+
} else
|
|
6584
|
+
for (i in e2)
|
|
6585
|
+
if (n2.has(i))
|
|
6586
|
+
return t(e2[i], n2, r2);
|
|
6587
|
+
}
|
|
6588
|
+
}
|
|
6589
|
+
function o(e2, r2, t2) {
|
|
6590
|
+
let i, o2 = e2.exports;
|
|
6591
|
+
if (o2) {
|
|
6592
|
+
if (typeof o2 == "string")
|
|
6593
|
+
o2 = { ".": o2 };
|
|
6594
|
+
else
|
|
6595
|
+
for (i in o2) {
|
|
6596
|
+
i[0] !== "." && (o2 = { ".": o2 });
|
|
6597
|
+
break;
|
|
6598
|
+
}
|
|
6599
|
+
return n(e2.name, o2, r2 || ".", t2);
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
function entryPoint(directory) {
|
|
6603
|
+
const root = realpathSync3(directory);
|
|
6604
|
+
const fd = openSync3(join13(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
6605
|
+
let pkg;
|
|
6606
|
+
try {
|
|
6607
|
+
const stat = fstatSync3(fd);
|
|
6608
|
+
if (!stat.isFile() || stat.size > MAX_PACKAGE_BYTES2)
|
|
6609
|
+
throw new Error("Invalid Secrets package metadata");
|
|
6610
|
+
pkg = JSON.parse(readFileSync8(fd, "utf8"));
|
|
6611
|
+
} finally {
|
|
6612
|
+
closeSync3(fd);
|
|
6613
|
+
}
|
|
6614
|
+
if (!pkg || typeof pkg !== "object" || !("name" in pkg) || pkg.name !== PACKAGE) {
|
|
6615
|
+
throw new Error("Invalid Secrets package identity");
|
|
6616
|
+
}
|
|
6617
|
+
const target = o(pkg, ".")?.[0];
|
|
6618
|
+
if (typeof target !== "string" || !target.startsWith("./"))
|
|
6619
|
+
throw new Error("Missing Secrets import export");
|
|
6620
|
+
const parts = target.slice(2).split("/");
|
|
6621
|
+
if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
|
|
6622
|
+
throw new Error("Invalid Secrets import export");
|
|
6623
|
+
}
|
|
6624
|
+
const file = realpathSync3(join13(root, ...parts));
|
|
6625
|
+
const within = relative(root, file);
|
|
6626
|
+
if (!within || within === ".." || within.startsWith(`..${sep2}`) || !statSync(file).isFile()) {
|
|
6627
|
+
throw new Error("Secrets import export escapes its package or is not a file");
|
|
6628
|
+
}
|
|
6629
|
+
return pathToFileURL(file).href;
|
|
6630
|
+
}
|
|
6631
|
+
function resolveInstalledSecrets(parentUrl) {
|
|
6632
|
+
let directory = dirname6(fileURLToPath3(parentUrl));
|
|
6633
|
+
for (;; ) {
|
|
6634
|
+
if (basename2(directory) !== "node_modules") {
|
|
6635
|
+
const candidate = join13(directory, "node_modules", PACKAGE);
|
|
6636
|
+
let absent = false;
|
|
6637
|
+
try {
|
|
6638
|
+
lstatSync3(candidate);
|
|
6639
|
+
} catch (error) {
|
|
6640
|
+
if (error.code !== "ENOENT")
|
|
6641
|
+
throw error;
|
|
6642
|
+
absent = true;
|
|
6643
|
+
}
|
|
6644
|
+
if (!absent)
|
|
6645
|
+
return entryPoint(candidate);
|
|
6646
|
+
}
|
|
6647
|
+
const parent = dirname6(directory);
|
|
6648
|
+
if (parent === directory)
|
|
6649
|
+
throw new Error("Secrets SDK is not installed for this consumer");
|
|
6650
|
+
directory = parent;
|
|
6651
|
+
}
|
|
6652
|
+
}
|
|
6379
6653
|
function homeDir(env) {
|
|
6380
6654
|
const home = env.HOME?.trim();
|
|
6381
6655
|
return home ? home : null;
|
|
6382
6656
|
}
|
|
6383
6657
|
function absoluteOverride(env, key) {
|
|
6384
6658
|
const value = env[key]?.trim();
|
|
6385
|
-
return value &&
|
|
6659
|
+
return value && isAbsolute3(value) ? value : null;
|
|
6386
6660
|
}
|
|
6387
6661
|
function hasnaHomeDir(env) {
|
|
6388
6662
|
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
6389
6663
|
if (override)
|
|
6390
6664
|
return override;
|
|
6391
6665
|
const home = homeDir(env);
|
|
6392
|
-
return home ?
|
|
6666
|
+
return home ? join22(home, HASNA_HOME_DIR) : null;
|
|
6393
6667
|
}
|
|
6394
6668
|
function appConfigDir(name, env) {
|
|
6395
6669
|
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
6396
6670
|
if (configRoot)
|
|
6397
|
-
return
|
|
6671
|
+
return join22(configRoot, name);
|
|
6398
6672
|
const root = hasnaHomeDir(env);
|
|
6399
|
-
return root ?
|
|
6673
|
+
return root ? join22(root, name, CONFIG_SUBDIR) : null;
|
|
6400
6674
|
}
|
|
6401
6675
|
function credentialDiskSourceList(name, env, profile = null) {
|
|
6402
6676
|
if (!SAFE_APP_SLUG.test(name))
|
|
@@ -6405,7 +6679,7 @@ function credentialDiskSourceList(name, env, profile = null) {
|
|
|
6405
6679
|
if (!directory)
|
|
6406
6680
|
return [];
|
|
6407
6681
|
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
6408
|
-
return [{ path:
|
|
6682
|
+
return [{ path: join22(directory, file), tier: "disk" }];
|
|
6409
6683
|
}
|
|
6410
6684
|
function credentialDiskSources(name, env) {
|
|
6411
6685
|
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
@@ -6428,9 +6702,9 @@ function parseEnvFile(text) {
|
|
|
6428
6702
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
6429
6703
|
continue;
|
|
6430
6704
|
let value = withoutExport.slice(equals + 1).trim();
|
|
6431
|
-
const
|
|
6432
|
-
if (
|
|
6433
|
-
if (value.length < 2 || !value.endsWith(
|
|
6705
|
+
const quote2 = value[0];
|
|
6706
|
+
if (quote2 === '"' || quote2 === "'") {
|
|
6707
|
+
if (value.length < 2 || !value.endsWith(quote2)) {
|
|
6434
6708
|
unusable.add(key);
|
|
6435
6709
|
continue;
|
|
6436
6710
|
}
|
|
@@ -6459,7 +6733,7 @@ function readAppConfigFile(path) {
|
|
|
6459
6733
|
};
|
|
6460
6734
|
let fd = -1;
|
|
6461
6735
|
try {
|
|
6462
|
-
fd =
|
|
6736
|
+
fd = openSync22(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
|
|
6463
6737
|
} catch (error) {
|
|
6464
6738
|
const code = error.code;
|
|
6465
6739
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -6469,7 +6743,7 @@ function readAppConfigFile(path) {
|
|
|
6469
6743
|
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
6470
6744
|
}
|
|
6471
6745
|
try {
|
|
6472
|
-
const before =
|
|
6746
|
+
const before = fstatSync2(fd);
|
|
6473
6747
|
if (!before.isFile())
|
|
6474
6748
|
unsafe("the path is not a regular file");
|
|
6475
6749
|
if (!configFileModeAllowed(before.mode)) {
|
|
@@ -6480,31 +6754,39 @@ function readAppConfigFile(path) {
|
|
|
6480
6754
|
unsafe("the file is not owned by the current user");
|
|
6481
6755
|
if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
|
|
6482
6756
|
unsafe("the file exceeds the size limit");
|
|
6483
|
-
const bytes =
|
|
6484
|
-
const after =
|
|
6757
|
+
const bytes = readFileSync22(fd);
|
|
6758
|
+
const after = fstatSync2(fd);
|
|
6485
6759
|
if (!configFileReadsCoherent(before, after)) {
|
|
6486
6760
|
unsafe("the file changed while being read");
|
|
6487
6761
|
}
|
|
6488
6762
|
return parseEnvFile(bytes.toString("utf8"));
|
|
6489
6763
|
} finally {
|
|
6490
6764
|
if (fd !== -1)
|
|
6491
|
-
|
|
6765
|
+
closeSync22(fd);
|
|
6492
6766
|
}
|
|
6493
6767
|
}
|
|
6494
|
-
function readCredentialFile(path, apiKeyKeys) {
|
|
6768
|
+
function readCredentialFile(path, apiKeyKeys, pointerKey) {
|
|
6495
6769
|
const parsed = readAppConfigFile(path);
|
|
6496
6770
|
if (!parsed)
|
|
6497
6771
|
return null;
|
|
6498
|
-
for (const key of apiKeyKeys) {
|
|
6772
|
+
for (const key of [...apiKeyKeys, pointerKey]) {
|
|
6499
6773
|
if (parsed.unusable.has(key)) {
|
|
6500
6774
|
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
6501
6775
|
}
|
|
6502
6776
|
}
|
|
6503
6777
|
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
6778
|
+
const pointer = parsed.values.get(pointerKey)?.trim();
|
|
6779
|
+
if (pointer !== undefined) {
|
|
6780
|
+
if (!VAULT_POINTER_SHAPE.test(pointer))
|
|
6781
|
+
throw new CredentialFileUnsafeError(path, `${pointerKey} must name a vault item`);
|
|
6782
|
+
if (values.length)
|
|
6783
|
+
throw new CredentialFileUnsafeError(path, "a credential file cannot select both a literal key and a vault reference");
|
|
6784
|
+
return { apiKey: "", pointerVaultKey: pointer };
|
|
6785
|
+
}
|
|
6504
6786
|
if (new Set(values).size > 1) {
|
|
6505
6787
|
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
6506
6788
|
}
|
|
6507
|
-
return values[0]
|
|
6789
|
+
return values[0] === undefined ? null : { apiKey: values[0] };
|
|
6508
6790
|
}
|
|
6509
6791
|
function appConfigDiskValue(name, env, keys) {
|
|
6510
6792
|
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
@@ -6576,6 +6858,18 @@ function sealCredential(fields) {
|
|
|
6576
6858
|
});
|
|
6577
6859
|
return Object.freeze(sealed);
|
|
6578
6860
|
}
|
|
6861
|
+
function explicitCredential(appName, apiKey) {
|
|
6862
|
+
const source = "explicit apiKey option";
|
|
6863
|
+
assertUsableCredential(appName, source, apiKey);
|
|
6864
|
+
return sealCredential({
|
|
6865
|
+
apiKey,
|
|
6866
|
+
tier: "argument",
|
|
6867
|
+
source,
|
|
6868
|
+
deliberate: true,
|
|
6869
|
+
diskCandidates: [],
|
|
6870
|
+
warning: null
|
|
6871
|
+
});
|
|
6872
|
+
}
|
|
6579
6873
|
function firstEnvValue(env, keys) {
|
|
6580
6874
|
for (const key of keys) {
|
|
6581
6875
|
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
@@ -6761,12 +7055,13 @@ function resolveCredential(name, env, options = {}) {
|
|
|
6761
7055
|
}
|
|
6762
7056
|
const paths = profileDiskSources(name, env, profile);
|
|
6763
7057
|
for (const path of paths) {
|
|
6764
|
-
const value = readCredentialFile(path, apiKeyKeys);
|
|
7058
|
+
const value = readCredentialFile(path, apiKeyKeys, pointerKeyName);
|
|
6765
7059
|
if (value) {
|
|
6766
|
-
|
|
7060
|
+
if (!value.pointerVaultKey)
|
|
7061
|
+
assertUsableCredential(name, path, value.apiKey);
|
|
6767
7062
|
return sealCredential({
|
|
6768
|
-
|
|
6769
|
-
tier: "profile",
|
|
7063
|
+
...value,
|
|
7064
|
+
tier: value.pointerVaultKey ? "pointer" : "profile",
|
|
6770
7065
|
source: path,
|
|
6771
7066
|
deliberate: true,
|
|
6772
7067
|
diskCandidates: paths,
|
|
@@ -6774,7 +7069,7 @@ function resolveCredential(name, env, options = {}) {
|
|
|
6774
7069
|
});
|
|
6775
7070
|
}
|
|
6776
7071
|
}
|
|
6777
|
-
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
|
|
7072
|
+
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} or ${pointerKeyName} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
|
|
6778
7073
|
}
|
|
6779
7074
|
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
6780
7075
|
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
@@ -6799,17 +7094,27 @@ function resolveCredential(name, env, options = {}) {
|
|
|
6799
7094
|
});
|
|
6800
7095
|
}
|
|
6801
7096
|
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
6802
|
-
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
7097
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys, pointerKeyName) })).filter((hit) => hit.value !== null);
|
|
6803
7098
|
if (diskHits.length > 0) {
|
|
6804
7099
|
const winner = diskHits[0];
|
|
6805
|
-
|
|
7100
|
+
if (winner.value.pointerVaultKey) {
|
|
7101
|
+
return sealCredential({
|
|
7102
|
+
...winner.value,
|
|
7103
|
+
tier: "pointer",
|
|
7104
|
+
source: winner.src.path,
|
|
7105
|
+
deliberate: false,
|
|
7106
|
+
diskCandidates: diskPaths,
|
|
7107
|
+
warning: null
|
|
7108
|
+
});
|
|
7109
|
+
}
|
|
7110
|
+
assertUsableCredential(name, winner.src.path, winner.value.apiKey);
|
|
6806
7111
|
const divergentSources = [
|
|
6807
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
6808
|
-
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
7112
|
+
...diskHits.slice(1).filter((hit) => hit.value.apiKey !== winner.value.apiKey || hit.value.pointerVaultKey !== winner.value.pointerVaultKey).map((hit) => hit.src.path),
|
|
7113
|
+
...envHit && envHit.value !== winner.value.apiKey ? [envHit.key] : []
|
|
6809
7114
|
];
|
|
6810
7115
|
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
|
|
6811
7116
|
return sealCredential({
|
|
6812
|
-
apiKey: winner.value,
|
|
7117
|
+
apiKey: winner.value.apiKey,
|
|
6813
7118
|
tier: winner.src.tier,
|
|
6814
7119
|
source: winner.src.path,
|
|
6815
7120
|
deliberate: false,
|
|
@@ -6830,6 +7135,53 @@ function resolveCredential(name, env, options = {}) {
|
|
|
6830
7135
|
}
|
|
6831
7136
|
return null;
|
|
6832
7137
|
}
|
|
7138
|
+
async function completePointerCredential(name, pointerResolution, env = process.env) {
|
|
7139
|
+
const secretsEnv = snapshotClientEnvironment("secrets", env);
|
|
7140
|
+
const vaultKey = pointerResolution.pointerVaultKey;
|
|
7141
|
+
const pointerEnvKey = pointerResolution.source;
|
|
7142
|
+
if (!vaultKey) {
|
|
7143
|
+
throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
|
|
7144
|
+
}
|
|
7145
|
+
if (name === "secrets") {
|
|
7146
|
+
throw new CredentialResolutionError(name, "The Secrets bootstrap credential cannot reference the same hosted vault; configure an independent bootstrap provider.", [pointerEnvKey]);
|
|
7147
|
+
}
|
|
7148
|
+
let secretsSdk;
|
|
7149
|
+
try {
|
|
7150
|
+
const sdkUrl = resolveInstalledSecrets(import.meta.url);
|
|
7151
|
+
secretsSdk = requireSecretsSdk2(fileURLToPath22(sdkUrl));
|
|
7152
|
+
} catch {
|
|
7153
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
|
|
7154
|
+
}
|
|
7155
|
+
let client;
|
|
7156
|
+
try {
|
|
7157
|
+
const bootstrap = resolveCredential("secrets", secretsEnv);
|
|
7158
|
+
if (!bootstrap || bootstrap.tier === "pointer") {
|
|
7159
|
+
throw new CredentialResolutionError("secrets", "The Secrets vault requires an independent, non-reference bootstrap credential.", bootstrap ? [bootstrap.source] : []);
|
|
7160
|
+
}
|
|
7161
|
+
client = secretsSdk.createSecretsClientFromEnv(secretsEnv);
|
|
7162
|
+
} catch {
|
|
7163
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
7164
|
+
}
|
|
7165
|
+
let secret;
|
|
7166
|
+
try {
|
|
7167
|
+
secret = await client.getSecret({ key: vaultKey });
|
|
7168
|
+
} catch {
|
|
7169
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
7170
|
+
}
|
|
7171
|
+
const value = secret.value;
|
|
7172
|
+
if (!value) {
|
|
7173
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
|
|
7174
|
+
}
|
|
7175
|
+
assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
|
|
7176
|
+
return sealCredential({
|
|
7177
|
+
apiKey: value,
|
|
7178
|
+
tier: "pointer",
|
|
7179
|
+
source: `${pointerEnvKey} -> vault:${vaultKey}`,
|
|
7180
|
+
deliberate: pointerResolution.deliberate,
|
|
7181
|
+
diskCandidates: pointerResolution.diskCandidates,
|
|
7182
|
+
warning: null
|
|
7183
|
+
});
|
|
7184
|
+
}
|
|
6833
7185
|
function defaultFleetGatewayBaseUrl(name) {
|
|
6834
7186
|
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
6835
7187
|
}
|
|
@@ -7032,8 +7384,9 @@ function credentialDiskSourcesForMessage(name, env) {
|
|
|
7032
7384
|
const paths = credentialDiskSources(name, env);
|
|
7033
7385
|
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
7034
7386
|
}
|
|
7035
|
-
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2,
|
|
7387
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", PACKAGE = "@hasna/secrets", MAX_PACKAGE_BYTES2, CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2;
|
|
7036
7388
|
var init_transport = __esm(() => {
|
|
7389
|
+
MAX_PACKAGE_BYTES2 = 1024 * 1024;
|
|
7037
7390
|
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
7038
7391
|
appName;
|
|
7039
7392
|
attempted;
|
|
@@ -7061,7 +7414,6 @@ var init_transport = __esm(() => {
|
|
|
7061
7414
|
INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
|
|
7062
7415
|
CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
|
|
7063
7416
|
AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
7064
|
-
SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
7065
7417
|
requireSecretsSdk2 = createRequire2(import.meta.url);
|
|
7066
7418
|
ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
7067
7419
|
DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -7150,7 +7502,9 @@ var exports_transport = {};
|
|
|
7150
7502
|
__export(exports_transport, {
|
|
7151
7503
|
rethrowHooksAuthorityFailure: () => rethrowHooksAuthorityFailure,
|
|
7152
7504
|
resolveHooksTransport: () => resolveHooksTransport,
|
|
7505
|
+
resolveHooksServePublishKeyAsync: () => resolveHooksServePublishKeyAsync,
|
|
7153
7506
|
resolveHooksServePublishKey: () => resolveHooksServePublishKey,
|
|
7507
|
+
resolveHooksRequestAuthority: () => resolveHooksRequestAuthority,
|
|
7154
7508
|
hooksRegistryOrigin: () => hooksRegistryOrigin,
|
|
7155
7509
|
announceHooksLocalMode: () => announceHooksLocalMode,
|
|
7156
7510
|
__resetHooksLocalNotice: () => __resetHooksLocalNotice
|
|
@@ -7189,6 +7543,40 @@ function rethrowHooksAuthorityFailure(error) {
|
|
|
7189
7543
|
}
|
|
7190
7544
|
throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
|
|
7191
7545
|
}
|
|
7546
|
+
function sameCredential(left, right) {
|
|
7547
|
+
return left === null || right === null ? left === right : left.apiKey === right.apiKey && left.pointerVaultKey === right.pointerVaultKey && left.source === right.source && left.tier === right.tier;
|
|
7548
|
+
}
|
|
7549
|
+
function changedBinding() {
|
|
7550
|
+
throw new Error("REMOTE_API_CREDENTIAL_INVALID: The configured Hooks authority or credential changed while preparing a request; no request was sent.");
|
|
7551
|
+
}
|
|
7552
|
+
async function resolveHooksRequestAuthority(authority) {
|
|
7553
|
+
if (authority.origin !== hooksRegistryOrigin(authority.v1BaseUrl))
|
|
7554
|
+
changedBinding();
|
|
7555
|
+
const binding = authorityBindings.get(authority);
|
|
7556
|
+
if (!binding)
|
|
7557
|
+
return { ...authority, apiKey: explicitCredential("hooks", authority.apiKey).apiKey };
|
|
7558
|
+
const snapshot = () => {
|
|
7559
|
+
const next = resolveHooksTransport(binding.env, binding.options);
|
|
7560
|
+
if (next.mode !== "remote" || !next.authority || next.authority.v1BaseUrl !== authority.v1BaseUrl)
|
|
7561
|
+
changedBinding();
|
|
7562
|
+
return { authority: next.authority, binding: authorityBindings.get(next.authority) };
|
|
7563
|
+
};
|
|
7564
|
+
const first = snapshot();
|
|
7565
|
+
const reviewed = snapshot();
|
|
7566
|
+
if (!sameCredential(first.binding.credential, reviewed.binding.credential))
|
|
7567
|
+
changedBinding();
|
|
7568
|
+
let credential = reviewed.binding.credential;
|
|
7569
|
+
try {
|
|
7570
|
+
if (credential.tier === "pointer")
|
|
7571
|
+
credential = await completePointerCredential("hooks", credential, binding.env);
|
|
7572
|
+
} catch (error) {
|
|
7573
|
+
rethrowHooksAuthorityFailure(error);
|
|
7574
|
+
}
|
|
7575
|
+
const final = snapshot();
|
|
7576
|
+
if (!sameCredential(reviewed.binding.credential, final.binding.credential))
|
|
7577
|
+
changedBinding();
|
|
7578
|
+
return { ...reviewed.authority, apiKey: credential.apiKey };
|
|
7579
|
+
}
|
|
7192
7580
|
function resolveHooksTransport(env = process.env, options = {}) {
|
|
7193
7581
|
if (selectsHooksLocalStore(env)) {
|
|
7194
7582
|
announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
|
|
@@ -7202,7 +7590,7 @@ function resolveHooksTransport(env = process.env, options = {}) {
|
|
|
7202
7590
|
try {
|
|
7203
7591
|
credential = resolveCredential("hooks", resolverEnv, credentialOptions);
|
|
7204
7592
|
resolution = resolveClientTransport("hooks", resolverEnv, {
|
|
7205
|
-
credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
7593
|
+
credentials: credential && credential.tier !== "pointer" ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
7206
7594
|
});
|
|
7207
7595
|
} catch (error) {
|
|
7208
7596
|
rethrowHooksAuthorityFailure(error);
|
|
@@ -7210,31 +7598,50 @@ function resolveHooksTransport(env = process.env, options = {}) {
|
|
|
7210
7598
|
if (!credential) {
|
|
7211
7599
|
throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
|
|
7212
7600
|
}
|
|
7601
|
+
const authority = {
|
|
7602
|
+
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
7603
|
+
apiKey: credential.apiKey,
|
|
7604
|
+
apiUrlSource: resolution.apiUrlSource,
|
|
7605
|
+
apiKeySource: credential.source,
|
|
7606
|
+
apiKeyTier: credential.tier,
|
|
7607
|
+
v1BaseUrl: resolution.baseUrl,
|
|
7608
|
+
warning: resolution.warning
|
|
7609
|
+
};
|
|
7610
|
+
authorityBindings.set(authority, { env, options, credential });
|
|
7213
7611
|
return {
|
|
7214
7612
|
mode: "remote",
|
|
7215
7613
|
source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
|
|
7216
|
-
authority
|
|
7217
|
-
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
7218
|
-
apiKey: credential.apiKey,
|
|
7219
|
-
apiUrlSource: resolution.apiUrlSource,
|
|
7220
|
-
apiKeySource: credential.source,
|
|
7221
|
-
apiKeyTier: credential.tier,
|
|
7222
|
-
v1BaseUrl: resolution.baseUrl,
|
|
7223
|
-
warning: resolution.warning
|
|
7224
|
-
}
|
|
7614
|
+
authority
|
|
7225
7615
|
};
|
|
7226
7616
|
}
|
|
7227
7617
|
function resolveHooksServePublishKey(env = process.env, options = {}) {
|
|
7228
7618
|
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
7229
7619
|
const tier1 = options.credentials?.apiKey;
|
|
7230
7620
|
const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
|
|
7231
|
-
|
|
7621
|
+
const credential = resolveCredential("hooks", resolverEnv, credentialOptions);
|
|
7622
|
+
if (credential?.tier === "pointer")
|
|
7623
|
+
throw new Error("Vault references require resolveHooksServePublishKeyAsync().");
|
|
7624
|
+
return credential?.apiKey;
|
|
7232
7625
|
}
|
|
7233
|
-
|
|
7626
|
+
async function resolveHooksServePublishKeyAsync(env = process.env, options = {}) {
|
|
7627
|
+
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
7628
|
+
const read = () => resolveCredential("hooks", resolverEnv, credentials);
|
|
7629
|
+
const selected = read();
|
|
7630
|
+
if (!selected || selected.tier !== "pointer")
|
|
7631
|
+
return selected?.apiKey;
|
|
7632
|
+
if (!sameCredential(selected, read()))
|
|
7633
|
+
changedBinding();
|
|
7634
|
+
const completed = await completePointerCredential("hooks", selected, env);
|
|
7635
|
+
if (!sameCredential(selected, read()))
|
|
7636
|
+
changedBinding();
|
|
7637
|
+
return completed.apiKey;
|
|
7638
|
+
}
|
|
7639
|
+
var localNoticePrinted = false, authorityBindings;
|
|
7234
7640
|
var init_transport2 = __esm(() => {
|
|
7235
7641
|
init_transport();
|
|
7236
7642
|
init_config();
|
|
7237
7643
|
init_local_opt_in();
|
|
7644
|
+
authorityBindings = new WeakMap;
|
|
7238
7645
|
});
|
|
7239
7646
|
|
|
7240
7647
|
// src/lib/db-writer.ts
|
|
@@ -7356,12 +7763,12 @@ var init_db_writer = __esm(() => {
|
|
|
7356
7763
|
});
|
|
7357
7764
|
|
|
7358
7765
|
// src/lib/hook-env.ts
|
|
7359
|
-
import { dirname as
|
|
7360
|
-
import { existsSync as
|
|
7766
|
+
import { dirname as dirname7, isAbsolute as isAbsolute4 } from "path";
|
|
7767
|
+
import { existsSync as existsSync14, realpathSync as realpathSync4, statSync as statSync2 } from "fs";
|
|
7361
7768
|
import { tmpdir } from "os";
|
|
7362
7769
|
function isDeniedEnvName(name) {
|
|
7363
7770
|
const upper = name.toUpperCase();
|
|
7364
|
-
if (DENY_PREFIXES.some((
|
|
7771
|
+
if (DENY_PREFIXES.some((prefix2) => upper.startsWith(prefix2)))
|
|
7365
7772
|
return true;
|
|
7366
7773
|
if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
|
|
7367
7774
|
return true;
|
|
@@ -7370,13 +7777,13 @@ function isDeniedEnvName(name) {
|
|
|
7370
7777
|
function isInterpreterInjectionEnvName(name) {
|
|
7371
7778
|
if (INTERPRETER_INJECTION.has(name))
|
|
7372
7779
|
return true;
|
|
7373
|
-
return INTERPRETER_INJECTION_PREFIXES.some((
|
|
7780
|
+
return INTERPRETER_INJECTION_PREFIXES.some((prefix2) => name.startsWith(prefix2));
|
|
7374
7781
|
}
|
|
7375
7782
|
function isBashFunctionEnvName(name) {
|
|
7376
7783
|
return name.startsWith("BASH_FUNC_");
|
|
7377
7784
|
}
|
|
7378
7785
|
function isUnsafePathEntry(entry, home) {
|
|
7379
|
-
if (entry === "" || !
|
|
7786
|
+
if (entry === "" || !isAbsolute4(entry))
|
|
7380
7787
|
return true;
|
|
7381
7788
|
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
7382
7789
|
return true;
|
|
@@ -7386,7 +7793,7 @@ function isUnsafePathEntry(entry, home) {
|
|
|
7386
7793
|
return true;
|
|
7387
7794
|
const canonical = (path) => {
|
|
7388
7795
|
try {
|
|
7389
|
-
return
|
|
7796
|
+
return realpathSync4(path);
|
|
7390
7797
|
} catch {
|
|
7391
7798
|
return path;
|
|
7392
7799
|
}
|
|
@@ -7398,16 +7805,16 @@ function isUnsafePathEntry(entry, home) {
|
|
|
7398
7805
|
return true;
|
|
7399
7806
|
}
|
|
7400
7807
|
try {
|
|
7401
|
-
if ((
|
|
7808
|
+
if ((statSync2(entry).mode & 2) !== 0)
|
|
7402
7809
|
return true;
|
|
7403
7810
|
} catch {}
|
|
7404
7811
|
return false;
|
|
7405
7812
|
}
|
|
7406
7813
|
function sanitizeHookPath(pathValue, home) {
|
|
7407
7814
|
const baseline = PATH_BASELINE.slice();
|
|
7408
|
-
if (process.platform === "darwin" &&
|
|
7815
|
+
if (process.platform === "darwin" && existsSync14("/opt/homebrew/bin"))
|
|
7409
7816
|
baseline.push("/opt/homebrew/bin");
|
|
7410
|
-
const bunDir =
|
|
7817
|
+
const bunDir = dirname7(process.execPath);
|
|
7411
7818
|
const seen = new Set;
|
|
7412
7819
|
const out = [];
|
|
7413
7820
|
for (const dir of baseline) {
|
|
@@ -7532,9 +7939,9 @@ __export(exports_run, {
|
|
|
7532
7939
|
});
|
|
7533
7940
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
7534
7941
|
import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
7535
|
-
import { basename as
|
|
7942
|
+
import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute5, join as join14 } from "path";
|
|
7536
7943
|
function extensionOf(scriptPath) {
|
|
7537
|
-
const base =
|
|
7944
|
+
const base = basename3(scriptPath);
|
|
7538
7945
|
const dot = base.lastIndexOf(".");
|
|
7539
7946
|
return dot > 0 ? base.slice(dot + 1) : "ts";
|
|
7540
7947
|
}
|
|
@@ -7546,9 +7953,9 @@ function shebangInterpreter(firstLine) {
|
|
|
7546
7953
|
return null;
|
|
7547
7954
|
const parts = rest.split(/\s+/);
|
|
7548
7955
|
if (parts[0] === "env" || parts[0] === "/usr/bin/env") {
|
|
7549
|
-
return parts[1] && parts[1] !== "-S" ?
|
|
7956
|
+
return parts[1] && parts[1] !== "-S" ? basename3(parts[1]) : parts[2] ? basename3(parts[2]) : null;
|
|
7550
7957
|
}
|
|
7551
|
-
return
|
|
7958
|
+
return basename3(parts[0]);
|
|
7552
7959
|
}
|
|
7553
7960
|
function interpreterFor(name, scriptPath, content) {
|
|
7554
7961
|
const ext = extensionOf(scriptPath);
|
|
@@ -7582,8 +7989,8 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
7582
7989
|
result = await Promise.race([
|
|
7583
7990
|
reader.read(),
|
|
7584
7991
|
new Promise((_, reject) => {
|
|
7585
|
-
const
|
|
7586
|
-
|
|
7992
|
+
const t2 = setTimeout(() => reject(new Error("pipe-drain-deadline")), remaining);
|
|
7993
|
+
t2.unref?.();
|
|
7587
7994
|
})
|
|
7588
7995
|
]);
|
|
7589
7996
|
} catch {
|
|
@@ -7601,9 +8008,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
7601
8008
|
return chunks.join("");
|
|
7602
8009
|
}
|
|
7603
8010
|
async function executeVerifiedScript(options) {
|
|
7604
|
-
const scriptDir =
|
|
8011
|
+
const scriptDir = dirname8(options.scriptPath);
|
|
7605
8012
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
7606
|
-
const tempPath =
|
|
8013
|
+
const tempPath = join14(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
7607
8014
|
let proc = null;
|
|
7608
8015
|
let timedOut = false;
|
|
7609
8016
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -7612,7 +8019,7 @@ async function executeVerifiedScript(options) {
|
|
|
7612
8019
|
}, options.timeout) : null;
|
|
7613
8020
|
try {
|
|
7614
8021
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
7615
|
-
proc = Bun.spawn([...interpreter.command,
|
|
8022
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute5(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
7616
8023
|
stdin: new Response(options.stdin),
|
|
7617
8024
|
stdout: "pipe",
|
|
7618
8025
|
stderr: "pipe",
|
|
@@ -7694,15 +8101,15 @@ __export(exports_custom_install, {
|
|
|
7694
8101
|
installCustomSource: () => installCustomSource,
|
|
7695
8102
|
ensureCustomDir: () => ensureCustomDir
|
|
7696
8103
|
});
|
|
7697
|
-
import { existsSync as
|
|
7698
|
-
import { join as
|
|
8104
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync9, rmSync as rmSync3 } from "fs";
|
|
8105
|
+
import { join as join15 } from "path";
|
|
7699
8106
|
import { tmpdir as tmpdir2 } from "os";
|
|
7700
8107
|
function manifestAt(dir) {
|
|
7701
|
-
const path =
|
|
7702
|
-
if (!
|
|
8108
|
+
const path = join15(dir, "manifest.json");
|
|
8109
|
+
if (!existsSync15(path))
|
|
7703
8110
|
return;
|
|
7704
8111
|
try {
|
|
7705
|
-
return parseManifest(
|
|
8112
|
+
return parseManifest(readFileSync9(path, "utf-8"));
|
|
7706
8113
|
} catch {
|
|
7707
8114
|
return;
|
|
7708
8115
|
}
|
|
@@ -7721,8 +8128,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
7721
8128
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
7722
8129
|
}
|
|
7723
8130
|
async function cloneToTemp(gitUrl) {
|
|
7724
|
-
const tmp = mkdtempSync(
|
|
7725
|
-
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl,
|
|
8131
|
+
const tmp = mkdtempSync(join15(tmpdir2(), "hooks-install-"));
|
|
8132
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join15(tmp, "repo")], {
|
|
7726
8133
|
stdout: "pipe",
|
|
7727
8134
|
stderr: "pipe"
|
|
7728
8135
|
});
|
|
@@ -7735,7 +8142,7 @@ async function cloneToTemp(gitUrl) {
|
|
|
7735
8142
|
rmSync3(tmp, { recursive: true, force: true });
|
|
7736
8143
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
7737
8144
|
}
|
|
7738
|
-
return
|
|
8145
|
+
return join15(tmp, "repo");
|
|
7739
8146
|
}
|
|
7740
8147
|
function isGitUrl(value) {
|
|
7741
8148
|
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
@@ -7745,7 +8152,7 @@ function isCustomSource(value) {
|
|
|
7745
8152
|
return true;
|
|
7746
8153
|
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
7747
8154
|
return true;
|
|
7748
|
-
return
|
|
8155
|
+
return existsSync15(value);
|
|
7749
8156
|
}
|
|
7750
8157
|
async function installCustomSource(source) {
|
|
7751
8158
|
if (isGitUrl(source)) {
|
|
@@ -7759,11 +8166,11 @@ async function installCustomSource(source) {
|
|
|
7759
8166
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
7760
8167
|
return installManifestUrl(source);
|
|
7761
8168
|
}
|
|
7762
|
-
if (!
|
|
8169
|
+
if (!existsSync15(source)) {
|
|
7763
8170
|
throw new Error(`No such local path: ${source}`);
|
|
7764
8171
|
}
|
|
7765
8172
|
if (source.endsWith("manifest.json")) {
|
|
7766
|
-
const manifest = parseManifest(
|
|
8173
|
+
const manifest = parseManifest(readFileSync9(source, "utf-8"));
|
|
7767
8174
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
7768
8175
|
}
|
|
7769
8176
|
return copyManifestDir(source, source, "local");
|
|
@@ -7812,13 +8219,13 @@ __export(exports_sync, {
|
|
|
7812
8219
|
fetchPinnedHook: () => fetchPinnedHook,
|
|
7813
8220
|
commitSyncArtifacts: () => commitSyncArtifacts
|
|
7814
8221
|
});
|
|
7815
|
-
import { createHash as
|
|
7816
|
-
import { readFileSync as
|
|
8222
|
+
import { createHash as createHash3 } from "crypto";
|
|
8223
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
7817
8224
|
function sha256OfText(text) {
|
|
7818
|
-
return
|
|
8225
|
+
return createHash3("sha256").update(text).digest("hex");
|
|
7819
8226
|
}
|
|
7820
8227
|
function sha256OfPath(path) {
|
|
7821
|
-
return
|
|
8228
|
+
return createHash3("sha256").update(readFileSync10(path)).digest("hex");
|
|
7822
8229
|
}
|
|
7823
8230
|
function collectBundledCatalog() {
|
|
7824
8231
|
const out = [];
|
|
@@ -7836,7 +8243,14 @@ function collectBundledCatalog() {
|
|
|
7836
8243
|
}
|
|
7837
8244
|
async function fetchJson(base, path, apiKey, opts) {
|
|
7838
8245
|
const headers = { accept: "application/json" };
|
|
7839
|
-
if (apiKey)
|
|
8246
|
+
if (typeof apiKey === "object") {
|
|
8247
|
+
if (apiKey.origin !== base)
|
|
8248
|
+
throw new Error("Hooks registry authority does not match the request target");
|
|
8249
|
+
const resolved = await resolveHooksRequestAuthority(apiKey);
|
|
8250
|
+
if (resolved.origin !== base)
|
|
8251
|
+
throw new Error("Hooks registry authority changed before dispatch");
|
|
8252
|
+
headers["x-api-key"] = resolved.apiKey;
|
|
8253
|
+
} else if (apiKey)
|
|
7840
8254
|
headers["x-api-key"] = apiKey;
|
|
7841
8255
|
const res = await fetch(`${base}${path}`, {
|
|
7842
8256
|
headers,
|
|
@@ -7904,8 +8318,8 @@ async function planSync(options = {}) {
|
|
|
7904
8318
|
credentials: options.credentials
|
|
7905
8319
|
});
|
|
7906
8320
|
if (transport.mode === "remote" && transport.authority) {
|
|
7907
|
-
const { origin
|
|
7908
|
-
const { catalog, remoteLock } = await fetchRemoteState(origin,
|
|
8321
|
+
const { origin } = transport.authority;
|
|
8322
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, transport.authority);
|
|
7909
8323
|
return { apiUrl: origin, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
|
|
7910
8324
|
}
|
|
7911
8325
|
const diff = computeDiff(collectBundledCatalog(), null);
|
|
@@ -7933,10 +8347,10 @@ async function syncHooks(options = {}) {
|
|
|
7933
8347
|
}
|
|
7934
8348
|
return plan;
|
|
7935
8349
|
}
|
|
7936
|
-
const { origin
|
|
7937
|
-
const { catalog, remoteLock } = await fetchRemoteState(origin,
|
|
8350
|
+
const { origin } = transport.authority;
|
|
8351
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, transport.authority);
|
|
7938
8352
|
const diff = computeDiff(catalog, remoteLock);
|
|
7939
|
-
const staged = await stageSyncArtifacts(origin,
|
|
8353
|
+
const staged = await stageSyncArtifacts(origin, transport.authority, [...diff.added, ...diff.updated], remoteLock);
|
|
7940
8354
|
await commitSyncArtifacts(staged, origin, remoteLock);
|
|
7941
8355
|
return { apiUrl: origin, dryRun: false, diff };
|
|
7942
8356
|
}
|
|
@@ -8597,21 +9011,20 @@ function secureEqual(a, b) {
|
|
|
8597
9011
|
const ab = enc.encode(a);
|
|
8598
9012
|
const bb = enc.encode(b);
|
|
8599
9013
|
let diff = ab.length ^ bb.length;
|
|
8600
|
-
const
|
|
8601
|
-
for (let i = 0;i <
|
|
9014
|
+
const n2 = Math.max(ab.length, bb.length);
|
|
9015
|
+
for (let i = 0;i < n2; i++) {
|
|
8602
9016
|
diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
|
|
8603
9017
|
}
|
|
8604
9018
|
return diff === 0;
|
|
8605
9019
|
}
|
|
8606
9020
|
|
|
8607
9021
|
// src/openapi.ts
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
openApiDocument = {
|
|
9022
|
+
function buildOpenApiDocument(version) {
|
|
9023
|
+
return {
|
|
8611
9024
|
openapi: "3.1.0",
|
|
8612
9025
|
info: {
|
|
8613
9026
|
title: "Hooks Registry API",
|
|
8614
|
-
version
|
|
9027
|
+
version,
|
|
8615
9028
|
description: "Local registry HTTP API for @hasna/hooks: catalog, lock, and artifact reads are open; publishing (PUT /api/v1/hooks) requires the API key (authorization: Bearer or x-api-key)."
|
|
8616
9029
|
},
|
|
8617
9030
|
servers: [{ url: "/" }],
|
|
@@ -8625,7 +9038,7 @@ var init_openapi = __esm(() => {
|
|
|
8625
9038
|
description: "Service is alive",
|
|
8626
9039
|
content: {
|
|
8627
9040
|
"application/json": {
|
|
8628
|
-
schema: {
|
|
9041
|
+
schema: { $ref: "#/components/schemas/HooksHealth" }
|
|
8629
9042
|
}
|
|
8630
9043
|
}
|
|
8631
9044
|
}
|
|
@@ -8637,8 +9050,22 @@ var init_openapi = __esm(() => {
|
|
|
8637
9050
|
summary: "Readiness probe \u2014 the hooks.lock store is readable",
|
|
8638
9051
|
operationId: "getReady",
|
|
8639
9052
|
responses: {
|
|
8640
|
-
"200": {
|
|
8641
|
-
|
|
9053
|
+
"200": {
|
|
9054
|
+
description: "Lock store is readable",
|
|
9055
|
+
content: {
|
|
9056
|
+
"application/json": {
|
|
9057
|
+
schema: { $ref: "#/components/schemas/HooksHealth" }
|
|
9058
|
+
}
|
|
9059
|
+
}
|
|
9060
|
+
},
|
|
9061
|
+
"503": {
|
|
9062
|
+
description: "Lock store is not readable",
|
|
9063
|
+
content: {
|
|
9064
|
+
"application/json": {
|
|
9065
|
+
schema: { $ref: "#/components/schemas/HooksHealth" }
|
|
9066
|
+
}
|
|
9067
|
+
}
|
|
9068
|
+
}
|
|
8642
9069
|
}
|
|
8643
9070
|
}
|
|
8644
9071
|
},
|
|
@@ -8651,7 +9078,7 @@ var init_openapi = __esm(() => {
|
|
|
8651
9078
|
description: "The installed @hasna/hooks version",
|
|
8652
9079
|
content: {
|
|
8653
9080
|
"application/json": {
|
|
8654
|
-
schema: {
|
|
9081
|
+
schema: { $ref: "#/components/schemas/HooksVersion" }
|
|
8655
9082
|
}
|
|
8656
9083
|
}
|
|
8657
9084
|
}
|
|
@@ -8662,7 +9089,16 @@ var init_openapi = __esm(() => {
|
|
|
8662
9089
|
get: {
|
|
8663
9090
|
summary: "This OpenAPI document",
|
|
8664
9091
|
operationId: "getOpenApi",
|
|
8665
|
-
responses: {
|
|
9092
|
+
responses: {
|
|
9093
|
+
"200": {
|
|
9094
|
+
description: "OpenAPI 3.1 document",
|
|
9095
|
+
content: {
|
|
9096
|
+
"application/json": {
|
|
9097
|
+
schema: { type: "object", additionalProperties: true }
|
|
9098
|
+
}
|
|
9099
|
+
}
|
|
9100
|
+
}
|
|
9101
|
+
}
|
|
8666
9102
|
}
|
|
8667
9103
|
},
|
|
8668
9104
|
"/api/v1/catalog": {
|
|
@@ -8674,23 +9110,7 @@ var init_openapi = __esm(() => {
|
|
|
8674
9110
|
description: "Catalog entries",
|
|
8675
9111
|
content: {
|
|
8676
9112
|
"application/json": {
|
|
8677
|
-
schema: {
|
|
8678
|
-
type: "object",
|
|
8679
|
-
properties: {
|
|
8680
|
-
hooks: {
|
|
8681
|
-
type: "array",
|
|
8682
|
-
items: {
|
|
8683
|
-
type: "object",
|
|
8684
|
-
properties: {
|
|
8685
|
-
name: { type: "string" },
|
|
8686
|
-
version: { type: "string" },
|
|
8687
|
-
sha256: { type: "string" },
|
|
8688
|
-
events: { type: "array", items: { type: "string" } }
|
|
8689
|
-
}
|
|
8690
|
-
}
|
|
8691
|
-
}
|
|
8692
|
-
}
|
|
8693
|
-
}
|
|
9113
|
+
schema: { $ref: "#/components/schemas/HooksCatalog" }
|
|
8694
9114
|
}
|
|
8695
9115
|
}
|
|
8696
9116
|
}
|
|
@@ -8701,7 +9121,16 @@ var init_openapi = __esm(() => {
|
|
|
8701
9121
|
get: {
|
|
8702
9122
|
summary: "Read the published hooks.lock with per-hook versions",
|
|
8703
9123
|
operationId: "getLock",
|
|
8704
|
-
responses: {
|
|
9124
|
+
responses: {
|
|
9125
|
+
"200": {
|
|
9126
|
+
description: "The lock file",
|
|
9127
|
+
content: {
|
|
9128
|
+
"application/json": {
|
|
9129
|
+
schema: { $ref: "#/components/schemas/HooksLock" }
|
|
9130
|
+
}
|
|
9131
|
+
}
|
|
9132
|
+
}
|
|
9133
|
+
}
|
|
8705
9134
|
}
|
|
8706
9135
|
},
|
|
8707
9136
|
"/api/v1/hooks/{name}/{version}": {
|
|
@@ -8714,10 +9143,35 @@ var init_openapi = __esm(() => {
|
|
|
8714
9143
|
],
|
|
8715
9144
|
responses: {
|
|
8716
9145
|
"200": {
|
|
8717
|
-
description: "Hook artifact; x-hook-sha256 header carries the script hash"
|
|
9146
|
+
description: "Hook artifact; x-hook-sha256 header carries the script hash",
|
|
9147
|
+
headers: {
|
|
9148
|
+
"x-hook-sha256": {
|
|
9149
|
+
description: "sha256 of the served script, the same value the lock pins",
|
|
9150
|
+
schema: { type: "string" }
|
|
9151
|
+
}
|
|
9152
|
+
},
|
|
9153
|
+
content: {
|
|
9154
|
+
"application/json": {
|
|
9155
|
+
schema: { $ref: "#/components/schemas/HooksArtifact" }
|
|
9156
|
+
}
|
|
9157
|
+
}
|
|
9158
|
+
},
|
|
9159
|
+
"400": {
|
|
9160
|
+
description: "Invalid URL encoding or semver version",
|
|
9161
|
+
content: {
|
|
9162
|
+
"application/json": {
|
|
9163
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9164
|
+
}
|
|
9165
|
+
}
|
|
8718
9166
|
},
|
|
8719
|
-
"
|
|
8720
|
-
|
|
9167
|
+
"404": {
|
|
9168
|
+
description: "Hook not found locally",
|
|
9169
|
+
content: {
|
|
9170
|
+
"application/json": {
|
|
9171
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9172
|
+
}
|
|
9173
|
+
}
|
|
9174
|
+
}
|
|
8721
9175
|
}
|
|
8722
9176
|
}
|
|
8723
9177
|
},
|
|
@@ -8729,18 +9183,51 @@ var init_openapi = __esm(() => {
|
|
|
8729
9183
|
required: true,
|
|
8730
9184
|
content: {
|
|
8731
9185
|
"application/json": {
|
|
8732
|
-
schema: {
|
|
8733
|
-
type: "object",
|
|
8734
|
-
properties: { name: { type: "string" }, version: { type: "string" } }
|
|
8735
|
-
}
|
|
9186
|
+
schema: { $ref: "#/components/schemas/HooksPublishRequest" }
|
|
8736
9187
|
}
|
|
8737
9188
|
}
|
|
8738
9189
|
},
|
|
8739
9190
|
responses: {
|
|
8740
|
-
"200": {
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
9191
|
+
"200": {
|
|
9192
|
+
description: "Hook retrusted and pin updated",
|
|
9193
|
+
content: {
|
|
9194
|
+
"application/json": {
|
|
9195
|
+
schema: { $ref: "#/components/schemas/HooksPublishResponse" }
|
|
9196
|
+
}
|
|
9197
|
+
}
|
|
9198
|
+
},
|
|
9199
|
+
"400": {
|
|
9200
|
+
description: "Invalid JSON body",
|
|
9201
|
+
content: {
|
|
9202
|
+
"application/json": {
|
|
9203
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9204
|
+
}
|
|
9205
|
+
}
|
|
9206
|
+
},
|
|
9207
|
+
"401": {
|
|
9208
|
+
description: "Missing or invalid API key",
|
|
9209
|
+
content: {
|
|
9210
|
+
"application/json": {
|
|
9211
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9212
|
+
}
|
|
9213
|
+
}
|
|
9214
|
+
},
|
|
9215
|
+
"404": {
|
|
9216
|
+
description: "Hook not found in local store",
|
|
9217
|
+
content: {
|
|
9218
|
+
"application/json": {
|
|
9219
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9220
|
+
}
|
|
9221
|
+
}
|
|
9222
|
+
},
|
|
9223
|
+
"409": {
|
|
9224
|
+
description: "Version mismatch with the local store",
|
|
9225
|
+
content: {
|
|
9226
|
+
"application/json": {
|
|
9227
|
+
schema: { $ref: "#/components/schemas/HooksError" }
|
|
9228
|
+
}
|
|
9229
|
+
}
|
|
9230
|
+
}
|
|
8744
9231
|
},
|
|
8745
9232
|
security: [{ apiKey: [] }]
|
|
8746
9233
|
}
|
|
@@ -8753,10 +9240,135 @@ var init_openapi = __esm(() => {
|
|
|
8753
9240
|
scheme: "bearer",
|
|
8754
9241
|
description: "The registry API key \u2014 resolved by the client through the @hasna/contracts chain (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials); the server compares the inbound bearer/x-api-key value."
|
|
8755
9242
|
}
|
|
9243
|
+
},
|
|
9244
|
+
schemas: {
|
|
9245
|
+
HooksHealth: {
|
|
9246
|
+
type: "object",
|
|
9247
|
+
additionalProperties: false,
|
|
9248
|
+
required: ["status", "name"],
|
|
9249
|
+
properties: {
|
|
9250
|
+
status: { type: "string" },
|
|
9251
|
+
name: { type: "string" }
|
|
9252
|
+
}
|
|
9253
|
+
},
|
|
9254
|
+
HooksVersion: {
|
|
9255
|
+
type: "object",
|
|
9256
|
+
additionalProperties: false,
|
|
9257
|
+
required: ["version"],
|
|
9258
|
+
properties: { version: { type: "string" } }
|
|
9259
|
+
},
|
|
9260
|
+
HooksCatalogEntry: {
|
|
9261
|
+
type: "object",
|
|
9262
|
+
additionalProperties: false,
|
|
9263
|
+
required: ["name", "version", "sha256", "events", "description", "source", "versions"],
|
|
9264
|
+
properties: {
|
|
9265
|
+
name: { type: "string" },
|
|
9266
|
+
version: { type: "string" },
|
|
9267
|
+
sha256: { type: "string" },
|
|
9268
|
+
events: { type: "array", items: { type: "string" } },
|
|
9269
|
+
description: { type: "string" },
|
|
9270
|
+
source: {
|
|
9271
|
+
type: "string",
|
|
9272
|
+
description: "bundled | custom | custom-overrides-bundled"
|
|
9273
|
+
},
|
|
9274
|
+
versions: { type: "array", items: { type: "string" } }
|
|
9275
|
+
}
|
|
9276
|
+
},
|
|
9277
|
+
HooksCatalog: {
|
|
9278
|
+
type: "object",
|
|
9279
|
+
additionalProperties: false,
|
|
9280
|
+
required: ["hooks"],
|
|
9281
|
+
properties: {
|
|
9282
|
+
hooks: { type: "array", items: { $ref: "#/components/schemas/HooksCatalogEntry" } }
|
|
9283
|
+
}
|
|
9284
|
+
},
|
|
9285
|
+
HooksLockEntry: {
|
|
9286
|
+
type: "object",
|
|
9287
|
+
additionalProperties: false,
|
|
9288
|
+
required: ["version", "sha256", "source"],
|
|
9289
|
+
properties: {
|
|
9290
|
+
version: { type: "string" },
|
|
9291
|
+
sha256: { type: "string" },
|
|
9292
|
+
source: { type: "string" },
|
|
9293
|
+
pinned: {
|
|
9294
|
+
type: "boolean",
|
|
9295
|
+
description: "True for an explicit install/update pin, absent for sync-maintained pins."
|
|
9296
|
+
},
|
|
9297
|
+
versions: { type: "array", items: { type: "string" } }
|
|
9298
|
+
}
|
|
9299
|
+
},
|
|
9300
|
+
HooksLock: {
|
|
9301
|
+
type: "object",
|
|
9302
|
+
additionalProperties: false,
|
|
9303
|
+
required: ["hooks"],
|
|
9304
|
+
properties: {
|
|
9305
|
+
hooks: {
|
|
9306
|
+
type: "object",
|
|
9307
|
+
additionalProperties: { $ref: "#/components/schemas/HooksLockEntry" }
|
|
9308
|
+
}
|
|
9309
|
+
}
|
|
9310
|
+
},
|
|
9311
|
+
HooksManifest: {
|
|
9312
|
+
type: "object",
|
|
9313
|
+
additionalProperties: false,
|
|
9314
|
+
required: ["name", "version", "events", "script"],
|
|
9315
|
+
properties: {
|
|
9316
|
+
name: { type: "string" },
|
|
9317
|
+
version: { type: "string" },
|
|
9318
|
+
description: { type: "string" },
|
|
9319
|
+
events: { type: "array", items: { type: "string" } },
|
|
9320
|
+
script: { type: "string" },
|
|
9321
|
+
script_kind: { type: "string", enum: ["inline", "file"] },
|
|
9322
|
+
args: { type: "array", items: { type: "string" } },
|
|
9323
|
+
timeout_ms: { type: "number" }
|
|
9324
|
+
}
|
|
9325
|
+
},
|
|
9326
|
+
HooksArtifact: {
|
|
9327
|
+
type: "object",
|
|
9328
|
+
additionalProperties: false,
|
|
9329
|
+
required: ["manifest", "script"],
|
|
9330
|
+
properties: {
|
|
9331
|
+
manifest: { $ref: "#/components/schemas/HooksManifest" },
|
|
9332
|
+
script: { type: "string" }
|
|
9333
|
+
}
|
|
9334
|
+
},
|
|
9335
|
+
HooksError: {
|
|
9336
|
+
type: "object",
|
|
9337
|
+
additionalProperties: false,
|
|
9338
|
+
required: ["error"],
|
|
9339
|
+
properties: { error: { type: "string" } }
|
|
9340
|
+
},
|
|
9341
|
+
HooksPublishRequest: {
|
|
9342
|
+
type: "object",
|
|
9343
|
+
additionalProperties: false,
|
|
9344
|
+
required: ["name"],
|
|
9345
|
+
properties: {
|
|
9346
|
+
name: { type: "string" },
|
|
9347
|
+
version: { type: "string" }
|
|
9348
|
+
}
|
|
9349
|
+
},
|
|
9350
|
+
HooksPublishResponse: {
|
|
9351
|
+
type: "object",
|
|
9352
|
+
additionalProperties: false,
|
|
9353
|
+
required: ["ok", "hook"],
|
|
9354
|
+
properties: {
|
|
9355
|
+
ok: { type: "boolean" },
|
|
9356
|
+
hook: {
|
|
9357
|
+
type: "object",
|
|
9358
|
+
additionalProperties: false,
|
|
9359
|
+
required: ["name", "version", "sha256"],
|
|
9360
|
+
properties: {
|
|
9361
|
+
name: { type: "string" },
|
|
9362
|
+
version: { type: "string" },
|
|
9363
|
+
sha256: { type: "string" }
|
|
9364
|
+
}
|
|
9365
|
+
}
|
|
9366
|
+
}
|
|
9367
|
+
}
|
|
8756
9368
|
}
|
|
8757
9369
|
}
|
|
8758
9370
|
};
|
|
8759
|
-
}
|
|
9371
|
+
}
|
|
8760
9372
|
|
|
8761
9373
|
// src/serve.ts
|
|
8762
9374
|
var exports_serve = {};
|
|
@@ -8768,13 +9380,13 @@ __export(exports_serve, {
|
|
|
8768
9380
|
SERVE_HOST: () => SERVE_HOST,
|
|
8769
9381
|
DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
|
|
8770
9382
|
});
|
|
8771
|
-
import { readFileSync as
|
|
8772
|
-
import { dirname as
|
|
8773
|
-
import { fileURLToPath as
|
|
9383
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
9384
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
9385
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8774
9386
|
function packageVersion() {
|
|
8775
9387
|
try {
|
|
8776
|
-
const pkgPath =
|
|
8777
|
-
return JSON.parse(
|
|
9388
|
+
const pkgPath = join16(dirname9(fileURLToPath4(import.meta.url)), "..", "package.json");
|
|
9389
|
+
return JSON.parse(readFileSync11(pkgPath, "utf-8")).version || "0.0.0";
|
|
8778
9390
|
} catch {
|
|
8779
9391
|
return "0.0.0";
|
|
8780
9392
|
}
|
|
@@ -8863,7 +9475,7 @@ function handleServeRequest(req, apiKey) {
|
|
|
8863
9475
|
return Promise.resolve(json({ version: packageVersion() }));
|
|
8864
9476
|
}
|
|
8865
9477
|
if (url.pathname === "/openapi.json" && req.method === "GET") {
|
|
8866
|
-
return Promise.resolve(json(
|
|
9478
|
+
return Promise.resolve(json(buildOpenApiDocument(packageVersion())));
|
|
8867
9479
|
}
|
|
8868
9480
|
if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
|
|
8869
9481
|
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
@@ -8935,7 +9547,7 @@ function startServeServer(options) {
|
|
|
8935
9547
|
hostname: host,
|
|
8936
9548
|
port,
|
|
8937
9549
|
async fetch(req) {
|
|
8938
|
-
return handleServeRequest(req,
|
|
9550
|
+
return handleServeRequest(req, await resolveHooksServePublishKeyAsync());
|
|
8939
9551
|
}
|
|
8940
9552
|
});
|
|
8941
9553
|
console.error(`hooks registry listening on http://${host}:${server.port} (publish requires an API key)`);
|
|
@@ -8948,7 +9560,6 @@ var init_serve = __esm(() => {
|
|
|
8948
9560
|
init_store();
|
|
8949
9561
|
init_resolve();
|
|
8950
9562
|
init_transport2();
|
|
8951
|
-
init_openapi();
|
|
8952
9563
|
init_semver();
|
|
8953
9564
|
if (false) {}
|
|
8954
9565
|
});
|
|
@@ -9075,17 +9686,17 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
9075
9686
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9076
9687
|
import { z as z2 } from "zod";
|
|
9077
9688
|
import { createServer } from "http";
|
|
9078
|
-
import { existsSync as
|
|
9079
|
-
import { join as
|
|
9080
|
-
import { fileURLToPath as
|
|
9689
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
|
|
9690
|
+
import { join as join17, dirname as dirname10 } from "path";
|
|
9691
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
9081
9692
|
async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
9082
9693
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
9083
9694
|
const resolved = resolveHook2(name);
|
|
9084
9695
|
const hookScript = resolveScriptPath2(name);
|
|
9085
|
-
if (!resolved || !hookScript || !
|
|
9696
|
+
if (!resolved || !hookScript || !existsSync16(hookScript)) {
|
|
9086
9697
|
throw new Error(`Hook '${name}' not found`);
|
|
9087
9698
|
}
|
|
9088
|
-
const content =
|
|
9699
|
+
const content = readFileSync12(hookScript);
|
|
9089
9700
|
const sha = sha256Of(content);
|
|
9090
9701
|
const check = checkScriptHash(name, sha);
|
|
9091
9702
|
if (!check.ok) {
|
|
@@ -9151,8 +9762,8 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
|
9151
9762
|
return ran;
|
|
9152
9763
|
}
|
|
9153
9764
|
function formatInstallResults(results, extra) {
|
|
9154
|
-
const installed = results.filter((
|
|
9155
|
-
const failed = results.filter((
|
|
9765
|
+
const installed = results.filter((r2) => r2.success).map((r2) => r2.hook);
|
|
9766
|
+
const failed = results.filter((r2) => !r2.success).map((r2) => ({ hook: r2.hook, error: r2.error }));
|
|
9156
9767
|
return {
|
|
9157
9768
|
content: [{
|
|
9158
9769
|
type: "text",
|
|
@@ -9290,7 +9901,7 @@ function createHooksServer() {
|
|
|
9290
9901
|
const settingsPath = getSettingsPath(scope);
|
|
9291
9902
|
const issues = [];
|
|
9292
9903
|
const healthy = [];
|
|
9293
|
-
const settingsExist =
|
|
9904
|
+
const settingsExist = existsSync16(settingsPath);
|
|
9294
9905
|
if (!settingsExist) {
|
|
9295
9906
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
9296
9907
|
}
|
|
@@ -9300,7 +9911,7 @@ function createHooksServer() {
|
|
|
9300
9911
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
9301
9912
|
let hookHealthy = true;
|
|
9302
9913
|
if (custom) {
|
|
9303
|
-
if (!
|
|
9914
|
+
if (!existsSync16(custom.scriptPath)) {
|
|
9304
9915
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
9305
9916
|
hookHealthy = false;
|
|
9306
9917
|
}
|
|
@@ -9310,14 +9921,14 @@ function createHooksServer() {
|
|
|
9310
9921
|
continue;
|
|
9311
9922
|
}
|
|
9312
9923
|
const hookDir = getHookPath(name);
|
|
9313
|
-
if (!
|
|
9924
|
+
if (!existsSync16(join17(hookDir, "src", "hook.ts"))) {
|
|
9314
9925
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
9315
9926
|
hookHealthy = false;
|
|
9316
9927
|
}
|
|
9317
9928
|
}
|
|
9318
9929
|
if (meta && settingsExist) {
|
|
9319
9930
|
try {
|
|
9320
|
-
const settings = JSON.parse(
|
|
9931
|
+
const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
|
|
9321
9932
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
9322
9933
|
const eventName = meta.event.split(":")[0];
|
|
9323
9934
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -9354,10 +9965,10 @@ function createHooksServer() {
|
|
|
9354
9965
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
9355
9966
|
}
|
|
9356
9967
|
const hookPath = getHookPath(name);
|
|
9357
|
-
const readmePath =
|
|
9968
|
+
const readmePath = join17(hookPath, "README.md");
|
|
9358
9969
|
let readme = "";
|
|
9359
|
-
if (
|
|
9360
|
-
readme =
|
|
9970
|
+
if (existsSync16(readmePath)) {
|
|
9971
|
+
readme = readFileSync12(readmePath, "utf-8");
|
|
9361
9972
|
}
|
|
9362
9973
|
if (verbose) {
|
|
9363
9974
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -9515,8 +10126,8 @@ function createHooksServer() {
|
|
|
9515
10126
|
}
|
|
9516
10127
|
return installHook(name, { scope, overwrite: true });
|
|
9517
10128
|
});
|
|
9518
|
-
const updated = results.filter((
|
|
9519
|
-
const failed = results.filter((
|
|
10129
|
+
const updated = results.filter((r2) => r2.success).map((r2) => r2.hook);
|
|
10130
|
+
const failed = results.filter((r2) => !r2.success).map((r2) => ({ hook: r2.hook, error: r2.error }));
|
|
9520
10131
|
return { content: [{ type: "text", text: JSON.stringify({ updated, failed, total: results.length }) }] };
|
|
9521
10132
|
});
|
|
9522
10133
|
defineTool("hooks_context", "Get compact agent context in one call: installed hooks, active profile summary, settings path, and doctor status.", {
|
|
@@ -9540,7 +10151,7 @@ function createHooksServer() {
|
|
|
9540
10151
|
const ctx = {
|
|
9541
10152
|
scope,
|
|
9542
10153
|
settings_path: settingsPath,
|
|
9543
|
-
settings_exists:
|
|
10154
|
+
settings_exists: existsSync16(settingsPath),
|
|
9544
10155
|
registered_hooks: hooks,
|
|
9545
10156
|
hook_count: hooks.length,
|
|
9546
10157
|
healthy,
|
|
@@ -9603,7 +10214,7 @@ function createHooksServer() {
|
|
|
9603
10214
|
return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
|
|
9604
10215
|
}
|
|
9605
10216
|
}));
|
|
9606
|
-
const blocked = results.find((
|
|
10217
|
+
const blocked = results.find((r2) => r2.decision === "block");
|
|
9607
10218
|
return {
|
|
9608
10219
|
content: [{
|
|
9609
10220
|
type: "text",
|
|
@@ -9637,8 +10248,8 @@ function createHooksServer() {
|
|
|
9637
10248
|
const target = agent_type === "gemini" ? "gemini" : "claude";
|
|
9638
10249
|
const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
|
|
9639
10250
|
const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id, target }));
|
|
9640
|
-
const installed = results.filter((
|
|
9641
|
-
const failed = results.filter((
|
|
10251
|
+
const installed = results.filter((r2) => r2.success).map((r2) => r2.hook);
|
|
10252
|
+
const failed = results.filter((r2) => !r2.success).map((r2) => ({ hook: r2.hook, error: r2.error }));
|
|
9642
10253
|
return {
|
|
9643
10254
|
content: [{
|
|
9644
10255
|
type: "text",
|
|
@@ -9686,8 +10297,8 @@ function createHooksServer() {
|
|
|
9686
10297
|
const settingsPath = getSettingsPath(scope);
|
|
9687
10298
|
let settings = {};
|
|
9688
10299
|
try {
|
|
9689
|
-
if (
|
|
9690
|
-
settings = JSON.parse(
|
|
10300
|
+
if (existsSync16(settingsPath))
|
|
10301
|
+
settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
|
|
9691
10302
|
} catch {}
|
|
9692
10303
|
if (!settings.hooks)
|
|
9693
10304
|
settings.hooks = {};
|
|
@@ -9696,8 +10307,8 @@ function createHooksServer() {
|
|
|
9696
10307
|
disabled.push(name);
|
|
9697
10308
|
settings.hooks.__disabled = disabled;
|
|
9698
10309
|
const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
|
|
9699
|
-
const { dirname:
|
|
9700
|
-
mkdirSync8(
|
|
10310
|
+
const { dirname: dirname11 } = await import("path");
|
|
10311
|
+
mkdirSync8(dirname11(settingsPath), { recursive: true });
|
|
9701
10312
|
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
9702
10313
|
`);
|
|
9703
10314
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
@@ -9709,11 +10320,11 @@ function createHooksServer() {
|
|
|
9709
10320
|
const settingsPath = getSettingsPath(scope);
|
|
9710
10321
|
let settings = {};
|
|
9711
10322
|
try {
|
|
9712
|
-
if (
|
|
9713
|
-
settings = JSON.parse(
|
|
10323
|
+
if (existsSync16(settingsPath))
|
|
10324
|
+
settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
|
|
9714
10325
|
} catch {}
|
|
9715
10326
|
if (settings.hooks?.__disabled) {
|
|
9716
|
-
settings.hooks.__disabled = settings.hooks.__disabled.filter((
|
|
10327
|
+
settings.hooks.__disabled = settings.hooks.__disabled.filter((n2) => n2 !== name);
|
|
9717
10328
|
if (settings.hooks.__disabled.length === 0)
|
|
9718
10329
|
delete settings.hooks.__disabled;
|
|
9719
10330
|
const { writeFileSync: writeFileSync7 } = await import("fs");
|
|
@@ -9770,9 +10381,9 @@ function createHooksServer() {
|
|
|
9770
10381
|
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
9771
10382
|
if (!m)
|
|
9772
10383
|
return null;
|
|
9773
|
-
const
|
|
10384
|
+
const n2 = parseInt(m[1]);
|
|
9774
10385
|
const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
|
|
9775
|
-
return new Date(Date.now() -
|
|
10386
|
+
return new Date(Date.now() - n2 * ms).toISOString();
|
|
9776
10387
|
}
|
|
9777
10388
|
let sql = "SELECT * FROM hook_events WHERE 1=1";
|
|
9778
10389
|
const params = [];
|
|
@@ -9810,10 +10421,10 @@ function createHooksServer() {
|
|
|
9810
10421
|
defineTool("hooks_log_tail", "Show recent hook events from SQLite. Compact summaries by default.", {
|
|
9811
10422
|
n: z2.number().default(20).describe("Number of most recent events to return"),
|
|
9812
10423
|
compact: z2.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
|
|
9813
|
-
}, async ({ n, compact }) => {
|
|
10424
|
+
}, async ({ n: n2, compact }) => {
|
|
9814
10425
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
9815
10426
|
const db = getDb2();
|
|
9816
|
-
const maxRows = boundedLimit(
|
|
10427
|
+
const maxRows = boundedLimit(n2, 20, compact ? 100 : 500);
|
|
9817
10428
|
const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(maxRows);
|
|
9818
10429
|
const projected = projectLogRows(rows);
|
|
9819
10430
|
return {
|
|
@@ -9840,9 +10451,9 @@ function createHooksServer() {
|
|
|
9840
10451
|
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
9841
10452
|
if (!m)
|
|
9842
10453
|
return s;
|
|
9843
|
-
const
|
|
10454
|
+
const n2 = parseInt(m[1]);
|
|
9844
10455
|
const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
|
|
9845
|
-
return new Date(Date.now() -
|
|
10456
|
+
return new Date(Date.now() - n2 * ms).toISOString();
|
|
9846
10457
|
}
|
|
9847
10458
|
const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
|
|
9848
10459
|
const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(ts, maxRows);
|
|
@@ -9868,20 +10479,20 @@ function createHooksServer() {
|
|
|
9868
10479
|
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
9869
10480
|
if (!m)
|
|
9870
10481
|
return s;
|
|
9871
|
-
const
|
|
10482
|
+
const n2 = parseInt(m[1]);
|
|
9872
10483
|
const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
|
|
9873
|
-
return new Date(Date.now() -
|
|
10484
|
+
return new Date(Date.now() - n2 * ms).toISOString();
|
|
9874
10485
|
}
|
|
9875
10486
|
const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
|
|
9876
10487
|
const totals = db.query("SELECT hook_name, COUNT(*) as total, SUM(CASE WHEN error IS NOT NULL THEN 1 ELSE 0 END) as errors FROM hook_events WHERE timestamp >= ? GROUP BY hook_name ORDER BY total DESC").all(ts);
|
|
9877
|
-
const summary = totals.map((
|
|
9878
|
-
hook_name:
|
|
9879
|
-
total:
|
|
9880
|
-
errors:
|
|
9881
|
-
error_rate:
|
|
10488
|
+
const summary = totals.map((r2) => ({
|
|
10489
|
+
hook_name: r2.hook_name,
|
|
10490
|
+
total: r2.total,
|
|
10491
|
+
errors: r2.errors,
|
|
10492
|
+
error_rate: r2.total > 0 ? (r2.errors / r2.total * 100).toFixed(1) + "%" : "0%"
|
|
9882
10493
|
}));
|
|
9883
|
-
const grandTotal = totals.reduce((s,
|
|
9884
|
-
const grandErrors = totals.reduce((s,
|
|
10494
|
+
const grandTotal = totals.reduce((s, r2) => s + r2.total, 0);
|
|
10495
|
+
const grandErrors = totals.reduce((s, r2) => s + r2.errors, 0);
|
|
9885
10496
|
return {
|
|
9886
10497
|
content: [{
|
|
9887
10498
|
type: "text",
|
|
@@ -9912,8 +10523,8 @@ function createHooksServer() {
|
|
|
9912
10523
|
pkg.version
|
|
9913
10524
|
]);
|
|
9914
10525
|
return { content: [{ type: "text", text: "Feedback saved. Thank you!" }] };
|
|
9915
|
-
} catch (
|
|
9916
|
-
return { content: [{ type: "text", text: String(
|
|
10526
|
+
} catch (e2) {
|
|
10527
|
+
return { content: [{ type: "text", text: String(e2) }], isError: true };
|
|
9917
10528
|
}
|
|
9918
10529
|
});
|
|
9919
10530
|
defineTool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
|
|
@@ -10080,13 +10691,13 @@ var init_server = __esm(() => {
|
|
|
10080
10691
|
init_redact();
|
|
10081
10692
|
init_authority();
|
|
10082
10693
|
init_storage();
|
|
10083
|
-
__dirname3 =
|
|
10694
|
+
__dirname3 = dirname10(fileURLToPath5(import.meta.url));
|
|
10084
10695
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
10085
10696
|
try {
|
|
10086
10697
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
10087
|
-
const p =
|
|
10088
|
-
if (
|
|
10089
|
-
pkg = JSON.parse(
|
|
10698
|
+
const p = join17(__dirname3, rel);
|
|
10699
|
+
if (existsSync16(p)) {
|
|
10700
|
+
pkg = JSON.parse(readFileSync12(p, "utf-8"));
|
|
10090
10701
|
break;
|
|
10091
10702
|
}
|
|
10092
10703
|
}
|
|
@@ -11828,9 +12439,9 @@ var {
|
|
|
11828
12439
|
|
|
11829
12440
|
// src/cli/index.tsx
|
|
11830
12441
|
import chalk2 from "chalk";
|
|
11831
|
-
import { existsSync as
|
|
11832
|
-
import { join as
|
|
11833
|
-
import { fileURLToPath as
|
|
12442
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13, rmSync as rmSync4 } from "fs";
|
|
12443
|
+
import { join as join18, dirname as dirname11 } from "path";
|
|
12444
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
11834
12445
|
|
|
11835
12446
|
// src/cli/components/App.tsx
|
|
11836
12447
|
import { useState as useState7 } from "react";
|
|
@@ -13002,6 +13613,7 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
13002
13613
|
}
|
|
13003
13614
|
|
|
13004
13615
|
// src/cli/index.tsx
|
|
13616
|
+
init_native_safety();
|
|
13005
13617
|
init_registry();
|
|
13006
13618
|
init_installer();
|
|
13007
13619
|
init_registration();
|
|
@@ -13017,10 +13629,10 @@ init_local_opt_in();
|
|
|
13017
13629
|
init_transport2();
|
|
13018
13630
|
init_db();
|
|
13019
13631
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
13020
|
-
var __dirname4 =
|
|
13021
|
-
var pkgPath =
|
|
13022
|
-
var pkg2 = JSON.parse(
|
|
13023
|
-
var
|
|
13632
|
+
var __dirname4 = dirname11(fileURLToPath6(import.meta.url));
|
|
13633
|
+
var pkgPath = existsSync17(join18(__dirname4, "..", "package.json")) ? join18(__dirname4, "..", "package.json") : join18(__dirname4, "..", "..", "package.json");
|
|
13634
|
+
var pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
13635
|
+
var program3 = new Command;
|
|
13024
13636
|
function resolveScope(options) {
|
|
13025
13637
|
if (options.project)
|
|
13026
13638
|
return "project";
|
|
@@ -13099,7 +13711,7 @@ async function openLocalStoreOrExit(json2) {
|
|
|
13099
13711
|
function readToken(tokenFile) {
|
|
13100
13712
|
if (tokenFile) {
|
|
13101
13713
|
try {
|
|
13102
|
-
return
|
|
13714
|
+
return readFileSync13(tokenFile, "utf-8").trim();
|
|
13103
13715
|
} catch {
|
|
13104
13716
|
return;
|
|
13105
13717
|
}
|
|
@@ -13144,22 +13756,22 @@ function printDisclosureHint(hidden, detailCommand, options = {}) {
|
|
|
13144
13756
|
}
|
|
13145
13757
|
}
|
|
13146
13758
|
function editDistance(a, b) {
|
|
13147
|
-
const m = a.length,
|
|
13148
|
-
const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(
|
|
13149
|
-
for (let j = 0;j <=
|
|
13759
|
+
const m = a.length, n2 = b.length;
|
|
13760
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n2).fill(0)]);
|
|
13761
|
+
for (let j = 0;j <= n2; j++)
|
|
13150
13762
|
dp[0][j] = j;
|
|
13151
13763
|
for (let i = 1;i <= m; i++) {
|
|
13152
|
-
for (let j = 1;j <=
|
|
13764
|
+
for (let j = 1;j <= n2; j++) {
|
|
13153
13765
|
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
13154
13766
|
}
|
|
13155
13767
|
}
|
|
13156
|
-
return dp[m][
|
|
13768
|
+
return dp[m][n2];
|
|
13157
13769
|
}
|
|
13158
13770
|
function suggestHooks(name, max = 3) {
|
|
13159
|
-
return HOOKS.map((h) => ({ name: h.name, dist: editDistance(name.toLowerCase(), h.name.toLowerCase()) })).filter(({ dist }) => dist <= 4).sort((a, b) => a.dist - b.dist).slice(0, max).map(({ name:
|
|
13771
|
+
return HOOKS.map((h) => ({ name: h.name, dist: editDistance(name.toLowerCase(), h.name.toLowerCase()) })).filter(({ dist }) => dist <= 4).sort((a, b) => a.dist - b.dist).slice(0, max).map(({ name: n2 }) => n2);
|
|
13160
13772
|
}
|
|
13161
|
-
|
|
13162
|
-
|
|
13773
|
+
program3.name("hooks").description("Install hooks for AI coding agents").version(pkg2.version);
|
|
13774
|
+
program3.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
|
|
13163
13775
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
13164
13776
|
console.error(chalk2.red("The interactive hook browser requires a TTY terminal.") + `
|
|
13165
13777
|
` + chalk2.dim("Use `hooks search <query>`, `hooks list`, or `hooks categories` for non-interactive use."));
|
|
@@ -13167,7 +13779,7 @@ program2.command("interactive", { isDefault: true }).alias("i").description("Int
|
|
|
13167
13779
|
}
|
|
13168
13780
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
13169
13781
|
});
|
|
13170
|
-
|
|
13782
|
+
program3.command("init").description("Register a new agent profile with a unique ID").option("-a, --agent <type>", "Agent type: claude, gemini, custom", "claude").option("-n, --name <name>", "Optional display name for the agent").option("-j, --json", "Output as JSON", false).option("--cloudflare", "Configure a remote registry (Cloudflare worker)", false).option("--api-url <url>", "Remote registry API URL (with --cloudflare)").option("--api-key <ref>", "Vault key NAME for the registry API key (never the value; with --cloudflare)").action(async (options) => {
|
|
13171
13783
|
if (options.cloudflare) {
|
|
13172
13784
|
if (!options.apiUrl) {
|
|
13173
13785
|
const message = "--cloudflare requires --api-url <url>";
|
|
@@ -13237,13 +13849,13 @@ program2.command("init").description("Register a new agent profile with a unique
|
|
|
13237
13849
|
if (profile.name) {
|
|
13238
13850
|
console.log(` ${chalk2.dim("Name:")} ${profile.name}`);
|
|
13239
13851
|
}
|
|
13240
|
-
console.log(` ${chalk2.dim("Profile:")} ${
|
|
13852
|
+
console.log(` ${chalk2.dim("Profile:")} ${join18(getProfilesDir(), `${profile.agent_id}.json`)}`);
|
|
13241
13853
|
console.log();
|
|
13242
13854
|
console.log(chalk2.dim(" Install hooks with this profile:"));
|
|
13243
13855
|
console.log(` hooks install gitguard --profile ${profile.agent_id}`);
|
|
13244
13856
|
console.log();
|
|
13245
13857
|
});
|
|
13246
|
-
|
|
13858
|
+
program3.command("run").argument("<hook>", "Hook to run").option("--profile <id>", "Agent profile ID").option("--enable-mementos", "Enable mementos-context for this invocation (provider assistance remains separately opt-in)").option("--disable-mementos", "Disable mementos-context for this invocation").option("--openrouter-secret-ref <reference>", "Secrets reference for the enabled Mementos hook; never a key value").description("Execute a hook (called by AI coding agents)").action(async (hook, options) => {
|
|
13247
13859
|
let mementos;
|
|
13248
13860
|
try {
|
|
13249
13861
|
mementos = mementosRegistrationFromFlags(hook, options);
|
|
@@ -13261,11 +13873,11 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
13261
13873
|
process.exit(1);
|
|
13262
13874
|
}
|
|
13263
13875
|
const hookScript = resolveScriptPath2(hook);
|
|
13264
|
-
if (!hookScript || !
|
|
13876
|
+
if (!hookScript || !existsSync17(hookScript)) {
|
|
13265
13877
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
|
|
13266
13878
|
process.exit(1);
|
|
13267
13879
|
}
|
|
13268
|
-
const content =
|
|
13880
|
+
const content = readFileSync13(hookScript);
|
|
13269
13881
|
const sha = sha256Of2(content);
|
|
13270
13882
|
const check = checkScriptHash2(hook, sha);
|
|
13271
13883
|
if (!check.ok) {
|
|
@@ -13371,7 +13983,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
13371
13983
|
process.stderr.write(stderr);
|
|
13372
13984
|
process.exit(exitCode);
|
|
13373
13985
|
});
|
|
13374
|
-
|
|
13986
|
+
program3.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--enable-mementos", "Persistently enable mementos-context (provider assistance remains separately opt-in)").option("--disable-mementos", "Persistently disable mementos-context").option("--openrouter-secret-ref <reference>", "Secrets reference for the enabled Mementos hook; never a key value").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks (registry names, local paths, git URLs, manifest URLs, or <name>@<version> pinned installs)").action(async (hooks, options) => {
|
|
13375
13987
|
const scope = resolveScope(options);
|
|
13376
13988
|
const target = resolveTarget(options);
|
|
13377
13989
|
let toInstall = hooks;
|
|
@@ -13437,10 +14049,10 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
|
|
|
13437
14049
|
return { name, version };
|
|
13438
14050
|
}
|
|
13439
14051
|
const pinnedRequests = toInstall.map((arg) => ({ arg, pinned: parseNameVersion(arg) })).filter((x) => x.pinned !== null);
|
|
13440
|
-
const customSources = toInstall.filter((
|
|
14052
|
+
const customSources = toInstall.filter((n2) => !getHook(n2) && isCustomSource2(n2));
|
|
13441
14053
|
if (options.dryRun) {
|
|
13442
|
-
const known = toInstall.filter((
|
|
13443
|
-
const unknown = toInstall.filter((
|
|
14054
|
+
const known = toInstall.filter((n2) => getHook(n2));
|
|
14055
|
+
const unknown = toInstall.filter((n2) => !getHook(n2) && !isCustomSource2(n2) && !parseNameVersion(n2) && !readCustomManifest2(n2));
|
|
13444
14056
|
if (options.json) {
|
|
13445
14057
|
console.log(JSON.stringify({
|
|
13446
14058
|
dryRun: true,
|
|
@@ -13514,7 +14126,7 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
13514
14126
|
for (const { arg, pinned } of pinnedRequests) {
|
|
13515
14127
|
try {
|
|
13516
14128
|
const transport = resolveHooksTransport2();
|
|
13517
|
-
const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority
|
|
14129
|
+
const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority) : await installPinnedFromBundled2(pinned.name, pinned.version);
|
|
13518
14130
|
installedPinned.push(pinnedInstall.name);
|
|
13519
14131
|
if (options.json) {
|
|
13520
14132
|
console.log(JSON.stringify({ pinned: { request: arg, name: pinnedInstall.name, version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source } }));
|
|
@@ -13533,7 +14145,7 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
13533
14145
|
}
|
|
13534
14146
|
}
|
|
13535
14147
|
const namesToRegister = [...new Set([
|
|
13536
|
-
...toInstall.filter((
|
|
14148
|
+
...toInstall.filter((n2) => !isCustomSource2(n2) && !parseNameVersion(n2)),
|
|
13537
14149
|
...installedCustom,
|
|
13538
14150
|
...installedPinned
|
|
13539
14151
|
])];
|
|
@@ -13557,22 +14169,22 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
13557
14169
|
}
|
|
13558
14170
|
if (options.json) {
|
|
13559
14171
|
console.log(JSON.stringify({
|
|
13560
|
-
installed: results.filter((
|
|
13561
|
-
failed: results.filter((
|
|
13562
|
-
fragments: results.filter((
|
|
14172
|
+
installed: results.filter((r2) => r2.success).map((r2) => r2.hook),
|
|
14173
|
+
failed: results.filter((r2) => !r2.success).map((r2) => ({ hook: r2.hook, error: r2.error })),
|
|
14174
|
+
fragments: results.filter((r2) => r2.success && r2.fragment).map((r2) => ({ hook: r2.hook, fragment: r2.fragment, applied: r2.applied, configPath: r2.configPath, note: r2.note })),
|
|
13563
14175
|
total: results.length,
|
|
13564
|
-
success: results.filter((
|
|
14176
|
+
success: results.filter((r2) => r2.success).length,
|
|
13565
14177
|
scope,
|
|
13566
14178
|
target,
|
|
13567
|
-
applied: results.some((
|
|
13568
|
-
...target === "codex" ? { notes: results.filter((
|
|
14179
|
+
applied: results.some((r2) => r2.applied),
|
|
14180
|
+
...target === "codex" ? { notes: results.filter((r2) => r2.success && r2.note).map((r2) => r2.note) } : {}
|
|
13569
14181
|
}));
|
|
13570
|
-
if (results.some((
|
|
14182
|
+
if (results.some((r2) => !r2.success)) {
|
|
13571
14183
|
process.exitCode = 1;
|
|
13572
14184
|
}
|
|
13573
14185
|
return;
|
|
13574
14186
|
}
|
|
13575
|
-
const successCount = results.filter((
|
|
14187
|
+
const successCount = results.filter((r2) => r2.success).length;
|
|
13576
14188
|
const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (configs should apply)" : formatSettingsPath(scope, target);
|
|
13577
14189
|
console.log(chalk2.bold(`
|
|
13578
14190
|
Installing hooks (${scope}, ${target})...
|
|
@@ -13615,7 +14227,7 @@ Installing hooks (${scope}, ${target})...
|
|
|
13615
14227
|
console.log(chalk2.dim(`
|
|
13616
14228
|
Registered in ${settingsFile}`));
|
|
13617
14229
|
});
|
|
13618
|
-
|
|
14230
|
+
program3.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex (default: claude)", "claude").option("-n, --limit <n>", "Max rows to show in compact output", "20").option("--verbose", "Show descriptions and full detail columns", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks (bundled + custom/registry)").action((options) => {
|
|
13619
14231
|
const scope = resolveScope(options);
|
|
13620
14232
|
const limit = options.all ? Number.MAX_SAFE_INTEGER : parseLimit(options.limit, 20, 200);
|
|
13621
14233
|
const storeHooks = listStoreHooks();
|
|
@@ -13724,7 +14336,7 @@ Custom / Registry hooks (${storeHooks.length}, showing ${storeVisible.length}):
|
|
|
13724
14336
|
printDisclosureHint(storeHooks.length - storeVisible.length, "hooks info <name>", { includeAll: true });
|
|
13725
14337
|
}
|
|
13726
14338
|
});
|
|
13727
|
-
|
|
14339
|
+
program3.command("search").argument("<query>", "Search term").option("-n, --limit <n>", "Max rows to show in compact output", "10").option("--verbose", "Show descriptions for search results", false).option("-j, --json", "Output as JSON", false).description("Search for hooks").action((query, options) => {
|
|
13728
14340
|
const results = searchHooks(query);
|
|
13729
14341
|
if (options.json) {
|
|
13730
14342
|
console.log(JSON.stringify(results));
|
|
@@ -13743,7 +14355,7 @@ Found ${results.length} hook(s), showing ${visible.length}:
|
|
|
13743
14355
|
console.log(hookSummaryLine(h, { verbose: options.verbose }));
|
|
13744
14356
|
printDisclosureHint(results.length - visible.length, "hooks info <name>");
|
|
13745
14357
|
});
|
|
13746
|
-
|
|
14358
|
+
program3.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex, all (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("Remove an installed hook (settings registration + store + lock + DB)").action(async (hook, options) => {
|
|
13747
14359
|
const scope = resolveScope(options);
|
|
13748
14360
|
const target = resolveTarget(options);
|
|
13749
14361
|
const { uninstallHook: uninstallHook2 } = await Promise.resolve().then(() => (init_installer(), exports_installer));
|
|
@@ -13779,7 +14391,7 @@ program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").opti
|
|
|
13779
14391
|
console.log(chalk2.red(`\u2717 Hook '${hook}' not found${hint}`));
|
|
13780
14392
|
}
|
|
13781
14393
|
});
|
|
13782
|
-
|
|
14394
|
+
program3.command("categories").option("-j, --json", "Output as JSON", false).description("List all categories").action((options) => {
|
|
13783
14395
|
if (options.json) {
|
|
13784
14396
|
const result = CATEGORIES.map((cat) => ({
|
|
13785
14397
|
name: cat,
|
|
@@ -13796,7 +14408,7 @@ Categories:
|
|
|
13796
14408
|
console.log(` ${category} (${count})`);
|
|
13797
14409
|
}
|
|
13798
14410
|
});
|
|
13799
|
-
|
|
14411
|
+
program3.command("info").argument("<hook>", "Hook name").option("-j, --json", "Output as JSON", false).description("Show detailed info about a hook").action(async (hook, options) => {
|
|
13800
14412
|
const { resolveHookMeta: resolveHookMeta2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
13801
14413
|
const meta = resolveHookMeta2(hook) ?? getHook(hook);
|
|
13802
14414
|
if (!meta) {
|
|
@@ -13844,7 +14456,7 @@ ${meta.displayName}
|
|
|
13844
14456
|
console.log(chalk2.dim(" \u25CB Not installed in project"));
|
|
13845
14457
|
}
|
|
13846
14458
|
});
|
|
13847
|
-
|
|
14459
|
+
program3.command("doctor").option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-f, --fix", "Remove stale registrations (a .bak is written beside each settings file)", false).option("-j, --json", "Output as JSON", false).description("Check health of installed hooks; --fix removes registrations whose hook cannot resolve").action((options) => {
|
|
13848
14460
|
const scope = resolveScope(options);
|
|
13849
14461
|
const settingsPath = getSettingsPath(scope);
|
|
13850
14462
|
const issues = [];
|
|
@@ -13859,7 +14471,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13859
14471
|
});
|
|
13860
14472
|
}
|
|
13861
14473
|
const staleNames = new Set(stale.map((finding) => finding.hook));
|
|
13862
|
-
const settingsExist =
|
|
14474
|
+
const settingsExist = existsSync17(settingsPath);
|
|
13863
14475
|
if (!settingsExist) {
|
|
13864
14476
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
13865
14477
|
}
|
|
@@ -13867,7 +14479,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13867
14479
|
let wiringCount = 0;
|
|
13868
14480
|
if (settingsExist) {
|
|
13869
14481
|
try {
|
|
13870
|
-
const settings = JSON.parse(
|
|
14482
|
+
const settings = JSON.parse(readFileSync13(settingsPath, "utf-8"));
|
|
13871
14483
|
wiringCount = countSettingsWiring(settings);
|
|
13872
14484
|
} catch {}
|
|
13873
14485
|
}
|
|
@@ -13878,7 +14490,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13878
14490
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
13879
14491
|
let hookHealthy = true;
|
|
13880
14492
|
if (custom) {
|
|
13881
|
-
if (!
|
|
14493
|
+
if (!existsSync17(custom.scriptPath)) {
|
|
13882
14494
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
13883
14495
|
hookHealthy = false;
|
|
13884
14496
|
}
|
|
@@ -13889,15 +14501,15 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
13889
14501
|
continue;
|
|
13890
14502
|
}
|
|
13891
14503
|
const hookDir = getHookPath(name);
|
|
13892
|
-
const hookScript =
|
|
13893
|
-
if (!
|
|
14504
|
+
const hookScript = join18(hookDir, "src", "hook.ts");
|
|
14505
|
+
if (!existsSync17(hookScript)) {
|
|
13894
14506
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
13895
14507
|
hookHealthy = false;
|
|
13896
14508
|
}
|
|
13897
14509
|
}
|
|
13898
14510
|
if (meta && settingsExist) {
|
|
13899
14511
|
try {
|
|
13900
|
-
const settings = JSON.parse(
|
|
14512
|
+
const settings = JSON.parse(readFileSync13(settingsPath, "utf-8"));
|
|
13901
14513
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
13902
14514
|
const eventName = meta.event.split(":")[0];
|
|
13903
14515
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -13984,7 +14596,7 @@ Hook Health Check (${scope})
|
|
|
13984
14596
|
}
|
|
13985
14597
|
console.log();
|
|
13986
14598
|
});
|
|
13987
|
-
|
|
14599
|
+
program3.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version), refresh lock pins, or install a pinned registry version (<name>@<version>)").action(async (hooks, options) => {
|
|
13988
14600
|
const scope = resolveScope(options);
|
|
13989
14601
|
const installed = getInstalledHooks(scope);
|
|
13990
14602
|
const toUpdate = hooks.length > 0 ? hooks : installed;
|
|
@@ -14024,7 +14636,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
14024
14636
|
if (pinned) {
|
|
14025
14637
|
try {
|
|
14026
14638
|
const transport = resolveHooksTransport2();
|
|
14027
|
-
const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority
|
|
14639
|
+
const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority) : await installPinnedFromBundled2(pinned.name, pinned.version);
|
|
14028
14640
|
const registered = installHook(pinnedInstall.name, { scope, overwrite: true });
|
|
14029
14641
|
if (!registered.success) {
|
|
14030
14642
|
results.push({ hook: arg, success: false, error: registered.error ?? "registration failed" });
|
|
@@ -14065,10 +14677,10 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
|
|
|
14065
14677
|
}
|
|
14066
14678
|
if (options.json) {
|
|
14067
14679
|
console.log(JSON.stringify({
|
|
14068
|
-
updated: results.filter((
|
|
14069
|
-
failed: results.filter((
|
|
14680
|
+
updated: results.filter((r2) => r2.success).map((r2) => r2.hook),
|
|
14681
|
+
failed: results.filter((r2) => !r2.success).map((r2) => ({ hook: r2.hook, error: r2.error }))
|
|
14070
14682
|
}));
|
|
14071
|
-
if (results.some((
|
|
14683
|
+
if (results.some((r2) => !r2.success)) {
|
|
14072
14684
|
process.exitCode = 1;
|
|
14073
14685
|
}
|
|
14074
14686
|
return;
|
|
@@ -14083,11 +14695,11 @@ Updating hooks...
|
|
|
14083
14695
|
console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
|
|
14084
14696
|
}
|
|
14085
14697
|
}
|
|
14086
|
-
if (results.some((
|
|
14698
|
+
if (results.some((r2) => !r2.success)) {
|
|
14087
14699
|
process.exitCode = 1;
|
|
14088
14700
|
}
|
|
14089
14701
|
});
|
|
14090
|
-
|
|
14702
|
+
program3.command("docs").argument("[hook]", "Hook name (shows general docs if omitted)").option("--verbose", "Print full hook README content", false).option("-j, --json", "Output as JSON", false).description("Show documentation for hooks").action((hook, options) => {
|
|
14091
14703
|
if (hook) {
|
|
14092
14704
|
const meta = getHook(hook);
|
|
14093
14705
|
if (!meta) {
|
|
@@ -14100,10 +14712,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
14100
14712
|
return;
|
|
14101
14713
|
}
|
|
14102
14714
|
const hookPath = getHookPath(hook);
|
|
14103
|
-
const readmePath =
|
|
14715
|
+
const readmePath = join18(hookPath, "README.md");
|
|
14104
14716
|
let readme = "";
|
|
14105
|
-
if (
|
|
14106
|
-
readme =
|
|
14717
|
+
if (existsSync17(readmePath)) {
|
|
14718
|
+
readme = readFileSync13(readmePath, "utf-8");
|
|
14107
14719
|
}
|
|
14108
14720
|
if (options.json) {
|
|
14109
14721
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -14221,7 +14833,7 @@ ${meta.displayName} v${meta.version}
|
|
|
14221
14833
|
console.log(` hooks docs --json Machine-readable documentation`);
|
|
14222
14834
|
console.log();
|
|
14223
14835
|
});
|
|
14224
|
-
|
|
14836
|
+
program3.command("upgrade").option("-c, --check", "Check for updates without installing", false).option("-j, --json", "Output as JSON", false).description("Update the @hasna/hooks package to the latest version").action(async (options) => {
|
|
14225
14837
|
const current2 = pkg2.version;
|
|
14226
14838
|
let pm = "npm";
|
|
14227
14839
|
try {
|
|
@@ -14283,7 +14895,7 @@ Upgrading @hasna/hooks (${pm})...
|
|
|
14283
14895
|
\u2713 Upgraded: ${current2} \u2192 ${latest}`));
|
|
14284
14896
|
}
|
|
14285
14897
|
});
|
|
14286
|
-
|
|
14898
|
+
program3.command("profile-export").description("Export all agent profiles as JSON (for backup/cross-machine setup)").option("-o, --output <file>", "Write to file instead of stdout").option("-j, --json", "Output as JSON (default: true)", false).action(async (options) => {
|
|
14287
14899
|
const profiles = exportProfiles();
|
|
14288
14900
|
const json2 = JSON.stringify(profiles, null, 2);
|
|
14289
14901
|
if (options.output) {
|
|
@@ -14295,14 +14907,14 @@ program2.command("profile-export").description("Export all agent profiles as JSO
|
|
|
14295
14907
|
console.log(json2);
|
|
14296
14908
|
}
|
|
14297
14909
|
});
|
|
14298
|
-
|
|
14910
|
+
program3.command("profile-import").argument("<file>", "JSON file to import profiles from (use - for stdin)").description("Import agent profiles from a JSON export file").option("-j, --json", "Output result as JSON", false).action(async (file, options) => {
|
|
14299
14911
|
let raw;
|
|
14300
14912
|
if (file === "-") {
|
|
14301
14913
|
raw = await new Response(Bun.stdin.stream()).text();
|
|
14302
14914
|
} else {
|
|
14303
|
-
const { readFileSync:
|
|
14915
|
+
const { readFileSync: readFileSync14 } = await import("fs");
|
|
14304
14916
|
try {
|
|
14305
|
-
raw =
|
|
14917
|
+
raw = readFileSync14(file, "utf-8");
|
|
14306
14918
|
} catch {
|
|
14307
14919
|
if (options.json) {
|
|
14308
14920
|
console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
|
|
@@ -14333,7 +14945,7 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
14333
14945
|
console.log(chalk2.dim(` Skipped ${result.skipped} (already exist or invalid)`));
|
|
14334
14946
|
}
|
|
14335
14947
|
});
|
|
14336
|
-
var logCmd =
|
|
14948
|
+
var logCmd = program3.command("log").description(`Query hook event logs from SQLite (${getReportedDbPath()})`);
|
|
14337
14949
|
logCmd.command("list").description("List hook events").option("--hook <name>", "Filter by hook name").option("--session <id>", "Filter by session ID").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
14338
14950
|
const db = await openLocalStoreOrExit(options.json);
|
|
14339
14951
|
const limit = parseInt(options.limit) || 50;
|
|
@@ -14428,16 +15040,16 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
|
|
|
14428
15040
|
const m = s.match(/^(\d+)(s|m|h|d)$/);
|
|
14429
15041
|
if (!m)
|
|
14430
15042
|
return 86400000;
|
|
14431
|
-
const
|
|
15043
|
+
const n2 = parseInt(m[1]);
|
|
14432
15044
|
switch (m[2]) {
|
|
14433
15045
|
case "s":
|
|
14434
|
-
return
|
|
15046
|
+
return n2 * 1000;
|
|
14435
15047
|
case "m":
|
|
14436
|
-
return
|
|
15048
|
+
return n2 * 60 * 1000;
|
|
14437
15049
|
case "h":
|
|
14438
|
-
return
|
|
15050
|
+
return n2 * 60 * 60 * 1000;
|
|
14439
15051
|
case "d":
|
|
14440
|
-
return
|
|
15052
|
+
return n2 * 24 * 60 * 60 * 1000;
|
|
14441
15053
|
default:
|
|
14442
15054
|
return 86400000;
|
|
14443
15055
|
}
|
|
@@ -14484,7 +15096,7 @@ logCmd.command("clear").description("Delete hook event logs").option("--hook <na
|
|
|
14484
15096
|
}
|
|
14485
15097
|
console.log(chalk2.green(`\u2713 Cleared ${count} event(s).`));
|
|
14486
15098
|
});
|
|
14487
|
-
var storageCmd =
|
|
15099
|
+
var storageCmd = program3.command("storage").description("Sync local hook data with storage PostgreSQL");
|
|
14488
15100
|
storageCmd.command("status").description("Show storage sync status").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
14489
15101
|
try {
|
|
14490
15102
|
const { getStorageStatus: getStorageStatus2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
@@ -14567,7 +15179,7 @@ storageCmd.command("sync").description("Bidirectional storage sync: pull then pu
|
|
|
14567
15179
|
process.exitCode = 1;
|
|
14568
15180
|
}
|
|
14569
15181
|
});
|
|
14570
|
-
|
|
15182
|
+
program3.command("trust").argument("<hook>", "Hook to trust").option("-j, --json", "Output as JSON", false).description("Trust the current script content of a hook (re-pins its sha256)").action(async (hook, options) => {
|
|
14571
15183
|
const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
14572
15184
|
const { retrustHook: retrustHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
14573
15185
|
const resolved = resolveHook2(hook);
|
|
@@ -14586,14 +15198,14 @@ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json
|
|
|
14586
15198
|
console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
|
|
14587
15199
|
console.log(chalk2.dim(` sha256 ${result.actual}`));
|
|
14588
15200
|
});
|
|
14589
|
-
|
|
15201
|
+
program3.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").description("Serve the local hook registry over HTTP (catalog, artifacts, lock). The publish API key resolves from the @hasna/contracts chain per request (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials) \u2014 there is deliberately no --api-key flag (P1-8).").action(async (options) => {
|
|
14590
15202
|
const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
14591
15203
|
startServeServer2({
|
|
14592
15204
|
port: options.port ? parseInt(options.port, 10) : undefined,
|
|
14593
15205
|
host: options.host
|
|
14594
15206
|
});
|
|
14595
15207
|
});
|
|
14596
|
-
|
|
15208
|
+
program3.command("sync").option("--dry-run", "Print the plan without changing anything", false).option("-j, --json", "Output as JSON", false).description("Sync local hooks with the remote registry (or bundled registry when no registry credential resolves and local mode is opted into)").action(async (options) => {
|
|
14597
15209
|
const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
14598
15210
|
try {
|
|
14599
15211
|
const plan = options.dryRun ? await syncHooks2({ dryRun: true }) : await syncHooks2();
|
|
@@ -14637,7 +15249,7 @@ Hook sync (${source})${plan.dryRun ? " \u2014 dry run" : ""}
|
|
|
14637
15249
|
process.exitCode = 1;
|
|
14638
15250
|
}
|
|
14639
15251
|
});
|
|
14640
|
-
|
|
15252
|
+
program3.command("cf").description("Provision and deploy the hooks registry to Cloudflare").option("--deploy", "Provision D1 + R2 via the Cloudflare API", true).option("--dry-run", "Print the plan without calling the Cloudflare API", false).option("--account-id <id>", "Cloudflare account id (defaults to CF_ACCOUNT_ID env)").option("--database-name <name>", "D1 database name (default hooks-registry)").option("--bucket-name <name>", "R2 bucket name (default hooks-registry-artifacts)").option("--token-file <path>", "Read the API token from a file (defaults to CF_API_TOKEN env)").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
14641
15253
|
const token = readToken(options.tokenFile);
|
|
14642
15254
|
const accountId = options.accountId ?? process.env.CF_ACCOUNT_ID;
|
|
14643
15255
|
if (!options.dryRun) {
|
|
@@ -14686,7 +15298,7 @@ Cloudflare provisioning complete:
|
|
|
14686
15298
|
console.log(chalk2.dim(" wrangler.toml, fill in the D1 database id, then run the commands above."));
|
|
14687
15299
|
console.log();
|
|
14688
15300
|
});
|
|
14689
|
-
|
|
15301
|
+
program3.command("migrate").description("Apply PostgreSQL migrations to the storage database (HASNA_HOOKS_DATABASE_URL)").option("-j, --json", "Output as JSON", false).action(async (options) => {
|
|
14690
15302
|
try {
|
|
14691
15303
|
const { getStoragePg: getStoragePg2, runStorageMigrations: runStorageMigrations2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14692
15304
|
const remote = await getStoragePg2();
|
|
@@ -14709,7 +15321,7 @@ program2.command("migrate").description("Apply PostgreSQL migrations to the stor
|
|
|
14709
15321
|
process.exitCode = 1;
|
|
14710
15322
|
}
|
|
14711
15323
|
});
|
|
14712
|
-
|
|
15324
|
+
program3.command("mcp").option("-s, --stdio", "Use stdio transport (one process per agent)", false).option("--sse", "Use legacy SSE transport (port 39427)", false).option("--http", "Use Streamable HTTP transport (explicit; this is also the default)", false).option("-p, --port <port>", "Port for HTTP/SSE transport (defaults to 8847 for HTTP, 39427 for SSE)").description("Start MCP server for AI agent integration (default: shared Streamable HTTP)").action(async (options) => {
|
|
14713
15325
|
if (options.stdio) {
|
|
14714
15326
|
const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
14715
15327
|
await startStdioServer2();
|
|
@@ -14730,8 +15342,20 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
|
|
|
14730
15342
|
startMcpHttpServer2({ name: "hooks", port: resolveMcpHttpPort2(args), buildServer: createHooksServer2 });
|
|
14731
15343
|
}
|
|
14732
15344
|
});
|
|
14733
|
-
registerEventsCommands(
|
|
15345
|
+
registerEventsCommands(program3, { source: "hooks" });
|
|
15346
|
+
program3.command("safety").description("Register bundled native deletion guards without a registry credential").command("install <guard>").option("--target <target>", "codex or claude", "codex").option("--scope <scope>", "global or project", "global").option("--overwrite", "Replace this guard in place; native trust is still required").action((guard, options) => {
|
|
15347
|
+
if (!isNativeSafetyName(guard) || !["codex", "claude"].includes(options.target) || !["global", "project"].includes(options.scope)) {
|
|
15348
|
+
console.error("Unsupported bundled safety registration");
|
|
15349
|
+
process.exitCode = 1;
|
|
15350
|
+
return;
|
|
15351
|
+
}
|
|
15352
|
+
const result = installHook(guard, { target: options.target, scope: options.scope, overwrite: options.overwrite });
|
|
15353
|
+
console.log(JSON.stringify(result));
|
|
15354
|
+
if (!result.success)
|
|
15355
|
+
process.exitCode = 1;
|
|
15356
|
+
});
|
|
14734
15357
|
var API_INDEPENDENT_COMMANDS = new Set([
|
|
15358
|
+
"safety",
|
|
14735
15359
|
"serve",
|
|
14736
15360
|
"mcp",
|
|
14737
15361
|
"cf",
|
|
@@ -14792,4 +15416,4 @@ function enforceTransportGate() {
|
|
|
14792
15416
|
refuseLocalStore(hooksHostedRouteLocalStoreRefusal(transport.authority.v1BaseUrl));
|
|
14793
15417
|
}
|
|
14794
15418
|
enforceTransportGate();
|
|
14795
|
-
|
|
15419
|
+
program3.parse();
|