@hasna/hooks 0.6.8 → 0.7.2
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 +1 -1
- package/bin/index.js +579 -113
- package/bin/serve.js +503 -38
- package/dist/config.d.ts +7 -5
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/index.js +533 -71
- package/dist/openapi.d.ts +216 -0
- package/hooks/codewith-native-common.test.ts +25 -25
- package/hooks/codewith-native-common.ts +1 -1
- package/hooks/hook-autoformat/package.json +1 -1
- package/hooks/hook-desktopnotify/package.json +1 -1
- package/hooks/hook-envsetup/package.json +1 -1
- package/hooks/hook-knowledge-context/README.md +1 -1
- package/hooks/hook-knowledge-context/src/hook.test.ts +1 -1
- package/hooks/hook-permissionguard/package.json +1 -1
- package/hooks/hook-protectfiles/package.json +1 -1
- package/hooks/hook-spiral-detector/LICENSE +191 -0
- package/hooks/hook-spiral-detector/README.md +23 -0
- package/hooks/hook-spiral-detector/package.json +12 -0
- package/hooks/hook-spiral-detector/src/hook.test.ts +72 -0
- package/hooks/hook-spiral-detector/src/hook.ts +93 -0
- package/hooks/hook-workspace-repos-guard/README.md +4 -3
- package/hooks/hook-workspace-repos-guard/src/hook.test.ts +7 -7
- package/hooks/hook-workspace-repos-guard/src/hook.ts +5 -3
- package/hooks/mention-context/src/hasna-mention-context.py +2 -2
- package/hooks/mention-context/src/hasna-mention-warm.py +1 -1
- package/hooks/pre-bash/README.md +1 -1
- package/hooks/prompt-guard/README.md +1 -1
- package/hooks/session-start/README.md +1 -1
- package/hooks/stop-sync/README.md +1 -1
- package/hooks/worktree-guard/README.md +1 -1
- package/package.json +10 -6
package/bin/index.js
CHANGED
|
@@ -2266,6 +2266,16 @@ var init_registry = __esm(() => {
|
|
|
2266
2266
|
matcher: "",
|
|
2267
2267
|
tags: ["errors", "failures", "logging", "debugging"]
|
|
2268
2268
|
},
|
|
2269
|
+
{
|
|
2270
|
+
name: "spiral-detector",
|
|
2271
|
+
displayName: "Spiral Detector",
|
|
2272
|
+
description: "Interrupts sessions after five consecutive identical command failures",
|
|
2273
|
+
version: "0.1.0",
|
|
2274
|
+
category: "Observability",
|
|
2275
|
+
event: "PostToolUse",
|
|
2276
|
+
matcher: "",
|
|
2277
|
+
tags: ["spiral", "failures", "retries", "safety", "interrupt"]
|
|
2278
|
+
},
|
|
2269
2279
|
{
|
|
2270
2280
|
name: "stylescheck",
|
|
2271
2281
|
displayName: "Styles Check",
|
|
@@ -4059,6 +4069,280 @@ var require_cli_spinners = __commonJS((exports, module) => {
|
|
|
4059
4069
|
module.exports = spinners;
|
|
4060
4070
|
});
|
|
4061
4071
|
|
|
4072
|
+
// ../contracts/dist/client/transport.js
|
|
4073
|
+
import { readFileSync, statSync } from "fs";
|
|
4074
|
+
import { join as join2 } from "path";
|
|
4075
|
+
function envToken(name) {
|
|
4076
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
4077
|
+
}
|
|
4078
|
+
function clientTransportEnvKeys(name) {
|
|
4079
|
+
const envSegment = envToken(name);
|
|
4080
|
+
return {
|
|
4081
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
4082
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
4083
|
+
};
|
|
4084
|
+
}
|
|
4085
|
+
function credentialOverrideEnvKey(name) {
|
|
4086
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
4087
|
+
}
|
|
4088
|
+
function homeDir(env) {
|
|
4089
|
+
const home = env.HOME?.trim();
|
|
4090
|
+
return home ? home : null;
|
|
4091
|
+
}
|
|
4092
|
+
function credentialDiskSources(name, env) {
|
|
4093
|
+
return profileDiskSources(name, env, null);
|
|
4094
|
+
}
|
|
4095
|
+
function profileDiskSources(name, env, profile) {
|
|
4096
|
+
const home = homeDir(env);
|
|
4097
|
+
if (!home || !SAFE_APP_SLUG.test(name))
|
|
4098
|
+
return [];
|
|
4099
|
+
const stem = profile ? `${name}.${profile}` : name;
|
|
4100
|
+
const configStem = profile ? `${name}-${profile}` : name;
|
|
4101
|
+
return [
|
|
4102
|
+
join2(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
|
|
4103
|
+
join2(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
|
|
4104
|
+
];
|
|
4105
|
+
}
|
|
4106
|
+
function parseEnvFile(text) {
|
|
4107
|
+
const values = new Map;
|
|
4108
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
4109
|
+
const line = rawLine.trim();
|
|
4110
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
4111
|
+
continue;
|
|
4112
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
4113
|
+
const equals = withoutExport.indexOf("=");
|
|
4114
|
+
if (equals <= 0)
|
|
4115
|
+
continue;
|
|
4116
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
4117
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
4118
|
+
continue;
|
|
4119
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
4120
|
+
const quote = value[0];
|
|
4121
|
+
if (quote === '"' || quote === "'") {
|
|
4122
|
+
if (value.length < 2 || !value.endsWith(quote))
|
|
4123
|
+
continue;
|
|
4124
|
+
value = value.slice(1, -1);
|
|
4125
|
+
}
|
|
4126
|
+
if (value.length === 0)
|
|
4127
|
+
continue;
|
|
4128
|
+
values.set(key, value);
|
|
4129
|
+
}
|
|
4130
|
+
return values;
|
|
4131
|
+
}
|
|
4132
|
+
function readAppConfigFile(path) {
|
|
4133
|
+
let text;
|
|
4134
|
+
try {
|
|
4135
|
+
const stats = statSync(path);
|
|
4136
|
+
if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
4137
|
+
return null;
|
|
4138
|
+
text = readFileSync(path, "utf8");
|
|
4139
|
+
} catch {
|
|
4140
|
+
return null;
|
|
4141
|
+
}
|
|
4142
|
+
return parseEnvFile(text);
|
|
4143
|
+
}
|
|
4144
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
4145
|
+
const values = readAppConfigFile(path);
|
|
4146
|
+
if (!values)
|
|
4147
|
+
return null;
|
|
4148
|
+
for (const key of apiKeyKeys) {
|
|
4149
|
+
const value = values.get(key)?.trim();
|
|
4150
|
+
if (value)
|
|
4151
|
+
return value;
|
|
4152
|
+
}
|
|
4153
|
+
return null;
|
|
4154
|
+
}
|
|
4155
|
+
function assertUsableCredential(appName, source, value) {
|
|
4156
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
4157
|
+
return;
|
|
4158
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
|
|
4159
|
+
}
|
|
4160
|
+
function sealCredential(fields) {
|
|
4161
|
+
const { apiKey } = fields;
|
|
4162
|
+
const visible = {
|
|
4163
|
+
tier: fields.tier,
|
|
4164
|
+
source: fields.source,
|
|
4165
|
+
deliberate: fields.deliberate,
|
|
4166
|
+
deprecated: fields.deprecated,
|
|
4167
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
4168
|
+
warning: fields.warning
|
|
4169
|
+
};
|
|
4170
|
+
const sealed = { ...visible };
|
|
4171
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
4172
|
+
value: apiKey,
|
|
4173
|
+
enumerable: false,
|
|
4174
|
+
writable: false,
|
|
4175
|
+
configurable: false
|
|
4176
|
+
});
|
|
4177
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
4178
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
4179
|
+
enumerable: false,
|
|
4180
|
+
writable: false,
|
|
4181
|
+
configurable: false
|
|
4182
|
+
});
|
|
4183
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
4184
|
+
value: true,
|
|
4185
|
+
enumerable: false,
|
|
4186
|
+
writable: false,
|
|
4187
|
+
configurable: false
|
|
4188
|
+
});
|
|
4189
|
+
return Object.freeze(sealed);
|
|
4190
|
+
}
|
|
4191
|
+
function firstEnvValue(env, keys) {
|
|
4192
|
+
for (const key of keys) {
|
|
4193
|
+
const value = env[key]?.trim();
|
|
4194
|
+
if (value)
|
|
4195
|
+
return { key, value };
|
|
4196
|
+
}
|
|
4197
|
+
return null;
|
|
4198
|
+
}
|
|
4199
|
+
function deprecationNotified() {
|
|
4200
|
+
const host = globalThis;
|
|
4201
|
+
const existing = host[DEPRECATION_REGISTRY];
|
|
4202
|
+
if (existing instanceof Set)
|
|
4203
|
+
return existing;
|
|
4204
|
+
const created = new Set;
|
|
4205
|
+
host[DEPRECATION_REGISTRY] = created;
|
|
4206
|
+
return created;
|
|
4207
|
+
}
|
|
4208
|
+
function defaultDeprecationSink(message) {
|
|
4209
|
+
if (typeof process !== "undefined" && process.stderr) {
|
|
4210
|
+
process.stderr.write(`${message}
|
|
4211
|
+
`);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
function resolveCredential(name, env, options = {}) {
|
|
4215
|
+
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
4216
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
4217
|
+
const explicitKey = options.apiKey?.trim();
|
|
4218
|
+
if (explicitKey) {
|
|
4219
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
4220
|
+
return sealCredential({
|
|
4221
|
+
apiKey: explicitKey,
|
|
4222
|
+
tier: "argument",
|
|
4223
|
+
source: "explicit apiKey argument",
|
|
4224
|
+
deliberate: true,
|
|
4225
|
+
deprecated: false,
|
|
4226
|
+
diskCandidates: diskPaths,
|
|
4227
|
+
warning: null
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
4231
|
+
const overrideRaw = env[overrideKeyName];
|
|
4232
|
+
if (overrideRaw !== undefined) {
|
|
4233
|
+
const override = overrideRaw.trim();
|
|
4234
|
+
if (!override) {
|
|
4235
|
+
throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
|
|
4236
|
+
}
|
|
4237
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
4238
|
+
return sealCredential({
|
|
4239
|
+
apiKey: override,
|
|
4240
|
+
tier: "override",
|
|
4241
|
+
source: overrideKeyName,
|
|
4242
|
+
deliberate: true,
|
|
4243
|
+
deprecated: false,
|
|
4244
|
+
diskCandidates: diskPaths,
|
|
4245
|
+
warning: null
|
|
4246
|
+
});
|
|
4247
|
+
}
|
|
4248
|
+
const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
|
|
4249
|
+
if (profile) {
|
|
4250
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
4251
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
4252
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
4253
|
+
}
|
|
4254
|
+
const paths = profileDiskSources(name, env, profile);
|
|
4255
|
+
for (const path of paths) {
|
|
4256
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
4257
|
+
if (value) {
|
|
4258
|
+
assertUsableCredential(name, path, value);
|
|
4259
|
+
return sealCredential({
|
|
4260
|
+
apiKey: value,
|
|
4261
|
+
tier: "profile",
|
|
4262
|
+
source: path,
|
|
4263
|
+
deliberate: true,
|
|
4264
|
+
deprecated: false,
|
|
4265
|
+
diskCandidates: paths,
|
|
4266
|
+
warning: null
|
|
4267
|
+
});
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
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);
|
|
4271
|
+
}
|
|
4272
|
+
const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
4273
|
+
if (diskHits.length > 0) {
|
|
4274
|
+
const winner = diskHits[0];
|
|
4275
|
+
assertUsableCredential(name, winner.path, winner.value);
|
|
4276
|
+
const divergentSources = [
|
|
4277
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
|
|
4278
|
+
...(() => {
|
|
4279
|
+
const legacyHit = firstEnvValue(env, apiKeyKeys);
|
|
4280
|
+
return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
|
|
4281
|
+
})()
|
|
4282
|
+
];
|
|
4283
|
+
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.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;
|
|
4284
|
+
return sealCredential({
|
|
4285
|
+
apiKey: winner.value,
|
|
4286
|
+
tier: "disk",
|
|
4287
|
+
source: winner.path,
|
|
4288
|
+
deliberate: false,
|
|
4289
|
+
deprecated: false,
|
|
4290
|
+
diskCandidates: diskPaths,
|
|
4291
|
+
warning
|
|
4292
|
+
});
|
|
4293
|
+
}
|
|
4294
|
+
const legacy = firstEnvValue(env, apiKeyKeys);
|
|
4295
|
+
if (legacy) {
|
|
4296
|
+
assertUsableCredential(name, legacy.key, legacy.value);
|
|
4297
|
+
const where = diskPaths.length > 0 ? `Put the current key in ${diskPaths[0]} \u2014 it is re-read on every call, so rotations take effect immediately.` : `This environment has no HOME, so no credential file could be consulted at all; the disk tier is ` + `unavailable here and this process will keep using the environment snapshot.`;
|
|
4298
|
+
const message = `[${name}] DEPRECATED: the API key came from ${legacy.key} in this process's environment. ` + `Environment variables are a snapshot taken when this process started, so a shell that started ` + `before a key rotation keeps using the old key until it exits. ${where}`;
|
|
4299
|
+
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
4300
|
+
const notified = deprecationNotified();
|
|
4301
|
+
if (!notified.has(name)) {
|
|
4302
|
+
notified.add(name);
|
|
4303
|
+
sink(message);
|
|
4304
|
+
}
|
|
4305
|
+
return sealCredential({
|
|
4306
|
+
apiKey: legacy.value,
|
|
4307
|
+
tier: "legacy-env",
|
|
4308
|
+
source: legacy.key,
|
|
4309
|
+
deliberate: false,
|
|
4310
|
+
deprecated: true,
|
|
4311
|
+
diskCandidates: diskPaths,
|
|
4312
|
+
warning: message
|
|
4313
|
+
});
|
|
4314
|
+
}
|
|
4315
|
+
return null;
|
|
4316
|
+
}
|
|
4317
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, HASNA_STATE_DIR = ".hasna", FLEET_CREDENTIAL_DIR = "cloud", CONFIG_DIR = ".config", CONFIG_NAMESPACE = "hasna", MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, INSPECT_CUSTOM, CREDENTIAL_SEAL, DEPRECATION_REGISTRY, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
|
|
4318
|
+
var init_transport = __esm(() => {
|
|
4319
|
+
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
4320
|
+
appName;
|
|
4321
|
+
attempted;
|
|
4322
|
+
constructor(appName, message, attempted) {
|
|
4323
|
+
super(message);
|
|
4324
|
+
this.name = "CredentialResolutionError";
|
|
4325
|
+
this.appName = appName;
|
|
4326
|
+
this.attempted = attempted;
|
|
4327
|
+
}
|
|
4328
|
+
};
|
|
4329
|
+
MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
4330
|
+
SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
4331
|
+
SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
4332
|
+
ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
4333
|
+
INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
4334
|
+
CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
4335
|
+
DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
|
|
4336
|
+
IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
4337
|
+
AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
4338
|
+
"host",
|
|
4339
|
+
":authority",
|
|
4340
|
+
"forwarded",
|
|
4341
|
+
"x-forwarded-host",
|
|
4342
|
+
"x-original-host"
|
|
4343
|
+
]);
|
|
4344
|
+
});
|
|
4345
|
+
|
|
4062
4346
|
// src/config.ts
|
|
4063
4347
|
var exports_config = {};
|
|
4064
4348
|
__export(exports_config, {
|
|
@@ -4071,43 +4355,43 @@ __export(exports_config, {
|
|
|
4071
4355
|
getCustomHooksDir: () => getCustomHooksDir,
|
|
4072
4356
|
getConfigPath: () => getConfigPath
|
|
4073
4357
|
});
|
|
4074
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4075
|
-
import { join as
|
|
4358
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4359
|
+
import { join as join3 } from "path";
|
|
4076
4360
|
import { homedir as homedir2 } from "os";
|
|
4077
4361
|
function getHooksDataDir() {
|
|
4078
4362
|
const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
|
|
4079
4363
|
if (explicit)
|
|
4080
4364
|
return explicit;
|
|
4081
|
-
return
|
|
4365
|
+
return join3(homedir2(), ".hasna", "hooks");
|
|
4082
4366
|
}
|
|
4083
4367
|
function getCustomHooksDir() {
|
|
4084
|
-
return
|
|
4368
|
+
return join3(getHooksDataDir(), "hooks");
|
|
4085
4369
|
}
|
|
4086
4370
|
function getLockPath() {
|
|
4087
4371
|
const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
|
|
4088
4372
|
if (explicit)
|
|
4089
4373
|
return explicit;
|
|
4090
|
-
return
|
|
4374
|
+
return join3(getHooksDataDir(), "hooks.lock");
|
|
4091
4375
|
}
|
|
4092
4376
|
function getConfigPath() {
|
|
4093
4377
|
const explicit = process.env.HASNA_HOOKS_CONFIG_PATH ?? process.env.HOOKS_CONFIG_PATH;
|
|
4094
4378
|
if (explicit)
|
|
4095
4379
|
return explicit;
|
|
4096
|
-
return
|
|
4380
|
+
return join3(getHooksDataDir(), "config.json");
|
|
4097
4381
|
}
|
|
4098
4382
|
function readConfig() {
|
|
4099
4383
|
try {
|
|
4100
4384
|
const path = getConfigPath();
|
|
4101
4385
|
if (!existsSync2(path))
|
|
4102
4386
|
return {};
|
|
4103
|
-
return JSON.parse(
|
|
4387
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
4104
4388
|
} catch {
|
|
4105
4389
|
return {};
|
|
4106
4390
|
}
|
|
4107
4391
|
}
|
|
4108
4392
|
function writeConfig(config) {
|
|
4109
4393
|
const path = getConfigPath();
|
|
4110
|
-
mkdirSync(
|
|
4394
|
+
mkdirSync(join3(path, ".."), { recursive: true });
|
|
4111
4395
|
writeFileSync(path, JSON.stringify(config, null, 2) + `
|
|
4112
4396
|
`, "utf-8");
|
|
4113
4397
|
return path;
|
|
@@ -4120,9 +4404,11 @@ function resolveApiUrl() {
|
|
|
4120
4404
|
return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
|
|
4121
4405
|
}
|
|
4122
4406
|
function resolveApiKey() {
|
|
4123
|
-
return
|
|
4407
|
+
return resolveCredential("hooks", process.env)?.apiKey;
|
|
4124
4408
|
}
|
|
4125
|
-
var init_config = () => {
|
|
4409
|
+
var init_config = __esm(() => {
|
|
4410
|
+
init_transport();
|
|
4411
|
+
});
|
|
4126
4412
|
|
|
4127
4413
|
// src/lib/semver.ts
|
|
4128
4414
|
var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
|
|
@@ -4148,8 +4434,8 @@ __export(exports_manifest, {
|
|
|
4148
4434
|
ScriptContainmentError: () => ScriptContainmentError,
|
|
4149
4435
|
HOOK_NAME_RE: () => HOOK_NAME_RE
|
|
4150
4436
|
});
|
|
4151
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as
|
|
4152
|
-
import { basename, dirname, isAbsolute, join as
|
|
4437
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync, realpathSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4438
|
+
import { basename, dirname, isAbsolute, join as join4, normalize, resolve, sep } from "path";
|
|
4153
4439
|
import { z } from "zod";
|
|
4154
4440
|
function parseManifest(json) {
|
|
4155
4441
|
const parsed = JSON.parse(json);
|
|
@@ -4182,7 +4468,7 @@ function realpathNearest(path) {
|
|
|
4182
4468
|
tail.unshift(basename(cur));
|
|
4183
4469
|
cur = parent;
|
|
4184
4470
|
}
|
|
4185
|
-
return
|
|
4471
|
+
return join4(realpathSync(cur), ...tail);
|
|
4186
4472
|
}
|
|
4187
4473
|
function assertContainedReal(root, target, raw) {
|
|
4188
4474
|
const rootReal = realpathSync(root);
|
|
@@ -4215,25 +4501,25 @@ function resolveScript(manifest, manifestDir) {
|
|
|
4215
4501
|
if (isInlineScript(manifest))
|
|
4216
4502
|
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4217
4503
|
const rel = scriptRelFor(manifest);
|
|
4218
|
-
const resolved = assertContained(
|
|
4504
|
+
const resolved = assertContained(join4(manifestDir, rel), manifestDir, manifest.script);
|
|
4219
4505
|
if (!existsSync3(resolved)) {
|
|
4220
4506
|
throw new Error(`manifest script file not found: ${resolved}`);
|
|
4221
4507
|
}
|
|
4222
|
-
return { path: rel, content:
|
|
4508
|
+
return { path: rel, content: readFileSync3(resolved, "utf-8") };
|
|
4223
4509
|
}
|
|
4224
4510
|
function customHookDir(name) {
|
|
4225
|
-
return
|
|
4511
|
+
return join4(getCustomHooksDir(), shortManifestName(name));
|
|
4226
4512
|
}
|
|
4227
4513
|
function readCustomManifest(name) {
|
|
4228
4514
|
const dir = customHookDir(name);
|
|
4229
|
-
const manifestPath =
|
|
4515
|
+
const manifestPath = join4(dir, "manifest.json");
|
|
4230
4516
|
if (!existsSync3(manifestPath))
|
|
4231
4517
|
return;
|
|
4232
4518
|
try {
|
|
4233
|
-
const manifest = parseManifest(
|
|
4519
|
+
const manifest = parseManifest(readFileSync3(manifestPath, "utf-8"));
|
|
4234
4520
|
const script = resolveScript(manifest, dir);
|
|
4235
4521
|
const scriptIsInline = isInlineScript(manifest);
|
|
4236
|
-
return { manifest, scriptPath:
|
|
4522
|
+
return { manifest, scriptPath: join4(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4237
4523
|
} catch (err) {
|
|
4238
4524
|
if (err instanceof ScriptContainmentError)
|
|
4239
4525
|
throw err;
|
|
@@ -4257,7 +4543,7 @@ function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
|
|
|
4257
4543
|
if (isAbsolute(scriptRelPath)) {
|
|
4258
4544
|
throw new ScriptContainmentError(scriptRelPath, dir);
|
|
4259
4545
|
}
|
|
4260
|
-
const scriptPath = scriptRelPath.includes("/") ?
|
|
4546
|
+
const scriptPath = scriptRelPath.includes("/") ? join4(dir, scriptRelPath) : join4(dir, scriptRelPath);
|
|
4261
4547
|
assertContainedLexical(scriptPath, dir, scriptRelPath);
|
|
4262
4548
|
mkdirSync2(dir, { recursive: true });
|
|
4263
4549
|
assertContainedReal(dir, scriptPath, scriptRelPath);
|
|
@@ -4265,7 +4551,7 @@ function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
|
|
|
4265
4551
|
if (scriptDir !== dir)
|
|
4266
4552
|
mkdirSync2(scriptDir, { recursive: true });
|
|
4267
4553
|
writeFileSync2(scriptPath, scriptContent, "utf-8");
|
|
4268
|
-
writeFileSync2(
|
|
4554
|
+
writeFileSync2(join4(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
|
|
4269
4555
|
`, "utf-8");
|
|
4270
4556
|
return { dir, scriptPath };
|
|
4271
4557
|
}
|
|
@@ -4309,14 +4595,14 @@ __export(exports_resolve, {
|
|
|
4309
4595
|
resolveHook: () => resolveHook,
|
|
4310
4596
|
resolveBundledHooksDir: () => resolveBundledHooksDir
|
|
4311
4597
|
});
|
|
4312
|
-
import { dirname as dirname2, join as
|
|
4598
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
4313
4599
|
import { existsSync as existsSync4 } from "fs";
|
|
4314
4600
|
import { fileURLToPath } from "url";
|
|
4315
4601
|
function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
|
|
4316
|
-
const sourceLayout =
|
|
4317
|
-
if (existsSync4(
|
|
4602
|
+
const sourceLayout = join5(moduleDir, "..", "..", "hooks");
|
|
4603
|
+
if (existsSync4(join5(sourceLayout, "hook-gitguard")))
|
|
4318
4604
|
return sourceLayout;
|
|
4319
|
-
return
|
|
4605
|
+
return join5(moduleDir, "..", "hooks");
|
|
4320
4606
|
}
|
|
4321
4607
|
function resolveHookDir(name) {
|
|
4322
4608
|
const custom = readCustomManifest(name);
|
|
@@ -4327,10 +4613,10 @@ function resolveHookDir(name) {
|
|
|
4327
4613
|
return;
|
|
4328
4614
|
const short = shortManifestName(name);
|
|
4329
4615
|
const bundledBase = resolveBundledHooksDir();
|
|
4330
|
-
const direct =
|
|
4616
|
+
const direct = join5(bundledBase, short);
|
|
4331
4617
|
if (existsSync4(direct))
|
|
4332
4618
|
return direct;
|
|
4333
|
-
const prefixed =
|
|
4619
|
+
const prefixed = join5(bundledBase, `hook-${short}`);
|
|
4334
4620
|
return existsSync4(prefixed) ? prefixed : undefined;
|
|
4335
4621
|
}
|
|
4336
4622
|
function resolveScriptPath(name) {
|
|
@@ -4340,7 +4626,7 @@ function resolveScriptPath(name) {
|
|
|
4340
4626
|
const dir = resolveHookDir(name);
|
|
4341
4627
|
if (!dir)
|
|
4342
4628
|
return;
|
|
4343
|
-
const script =
|
|
4629
|
+
const script = join5(dir, "src", "hook.ts");
|
|
4344
4630
|
return existsSync4(script) ? script : undefined;
|
|
4345
4631
|
}
|
|
4346
4632
|
function resolveHookMeta(name) {
|
|
@@ -4590,8 +4876,8 @@ var init_migrations = __esm(() => {
|
|
|
4590
4876
|
});
|
|
4591
4877
|
|
|
4592
4878
|
// src/db/legacy-import.ts
|
|
4593
|
-
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as
|
|
4594
|
-
import { join as
|
|
4879
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
4880
|
+
import { join as join6 } from "path";
|
|
4595
4881
|
import { homedir as homedir3 } from "os";
|
|
4596
4882
|
function ensureMetaTable(db) {
|
|
4597
4883
|
db.exec(`
|
|
@@ -4615,7 +4901,7 @@ function nanoid() {
|
|
|
4615
4901
|
function importJsonlFile(db, filePath) {
|
|
4616
4902
|
let count = 0;
|
|
4617
4903
|
try {
|
|
4618
|
-
const lines =
|
|
4904
|
+
const lines = readFileSync4(filePath, "utf-8").split(`
|
|
4619
4905
|
`).filter(Boolean);
|
|
4620
4906
|
for (const line of lines) {
|
|
4621
4907
|
try {
|
|
@@ -4641,7 +4927,7 @@ function importJsonlFile(db, filePath) {
|
|
|
4641
4927
|
function importErrorsLog(db, filePath) {
|
|
4642
4928
|
let count = 0;
|
|
4643
4929
|
try {
|
|
4644
|
-
const lines =
|
|
4930
|
+
const lines = readFileSync4(filePath, "utf-8").split(`
|
|
4645
4931
|
`).filter(Boolean);
|
|
4646
4932
|
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
4647
4933
|
for (const line of lines) {
|
|
@@ -4666,25 +4952,25 @@ function importErrorsLog(db, filePath) {
|
|
|
4666
4952
|
} catch {}
|
|
4667
4953
|
return count;
|
|
4668
4954
|
}
|
|
4669
|
-
function runLegacyImport(db,
|
|
4955
|
+
function runLegacyImport(db, homeDir2 = homedir3()) {
|
|
4670
4956
|
try {
|
|
4671
4957
|
if (isAlreadyDone(db))
|
|
4672
4958
|
return;
|
|
4673
4959
|
let total = 0;
|
|
4674
|
-
const claudeProjectsDir =
|
|
4960
|
+
const claudeProjectsDir = join6(homeDir2, ".claude", "projects");
|
|
4675
4961
|
if (existsSync5(claudeProjectsDir)) {
|
|
4676
4962
|
try {
|
|
4677
4963
|
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
4678
4964
|
for (const dir of projectDirs) {
|
|
4679
|
-
const projectDir =
|
|
4965
|
+
const projectDir = join6(claudeProjectsDir, dir);
|
|
4680
4966
|
try {
|
|
4681
4967
|
const files = readdirSync2(projectDir);
|
|
4682
4968
|
for (const file of files) {
|
|
4683
4969
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
4684
|
-
total += importJsonlFile(db,
|
|
4970
|
+
total += importJsonlFile(db, join6(projectDir, file));
|
|
4685
4971
|
}
|
|
4686
4972
|
if (file === "errors.log") {
|
|
4687
|
-
total += importErrorsLog(db,
|
|
4973
|
+
total += importErrorsLog(db, join6(projectDir, file));
|
|
4688
4974
|
}
|
|
4689
4975
|
}
|
|
4690
4976
|
} catch {}
|
|
@@ -4728,16 +5014,16 @@ __export(exports_db, {
|
|
|
4728
5014
|
});
|
|
4729
5015
|
import { Database } from "bun:sqlite";
|
|
4730
5016
|
import { existsSync as existsSync6, mkdirSync as mkdirSync3, cpSync } from "fs";
|
|
4731
|
-
import { join as
|
|
5017
|
+
import { join as join7 } from "path";
|
|
4732
5018
|
import { homedir as homedir4 } from "os";
|
|
4733
5019
|
function resolveDataDir() {
|
|
4734
5020
|
const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
|
|
4735
5021
|
if (explicit)
|
|
4736
5022
|
return explicit;
|
|
4737
|
-
const newDir =
|
|
4738
|
-
const oldDir =
|
|
5023
|
+
const newDir = join7(homedir4(), ".hasna", "hooks");
|
|
5024
|
+
const oldDir = join7(homedir4(), ".hooks");
|
|
4739
5025
|
if (!existsSync6(newDir) && existsSync6(oldDir)) {
|
|
4740
|
-
mkdirSync3(
|
|
5026
|
+
mkdirSync3(join7(homedir4(), ".hasna"), { recursive: true });
|
|
4741
5027
|
cpSync(oldDir, newDir, { recursive: true });
|
|
4742
5028
|
}
|
|
4743
5029
|
return newDir;
|
|
@@ -4747,7 +5033,7 @@ function getDbPath() {
|
|
|
4747
5033
|
if (explicitDb)
|
|
4748
5034
|
return explicitDb;
|
|
4749
5035
|
const dataDir = resolveDataDir();
|
|
4750
|
-
return
|
|
5036
|
+
return join7(dataDir, "hooks.db");
|
|
4751
5037
|
}
|
|
4752
5038
|
function ensureDir(dbPath) {
|
|
4753
5039
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
@@ -4821,7 +5107,7 @@ __export(exports_store, {
|
|
|
4821
5107
|
LockFileError: () => LockFileError
|
|
4822
5108
|
});
|
|
4823
5109
|
import { createHash } from "crypto";
|
|
4824
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as
|
|
5110
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync } from "fs";
|
|
4825
5111
|
import { readFile as readFile2 } from "fs/promises";
|
|
4826
5112
|
import { randomBytes } from "crypto";
|
|
4827
5113
|
function sha256Of(input) {
|
|
@@ -4887,7 +5173,7 @@ function readLock() {
|
|
|
4887
5173
|
return { hooks: {} };
|
|
4888
5174
|
let raw;
|
|
4889
5175
|
try {
|
|
4890
|
-
raw =
|
|
5176
|
+
raw = readFileSync5(path, "utf-8");
|
|
4891
5177
|
} catch {
|
|
4892
5178
|
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
4893
5179
|
}
|
|
@@ -4984,7 +5270,7 @@ async function verifyScriptHash(name, scriptPath) {
|
|
|
4984
5270
|
return checkScriptHash(name, await sha256File(scriptPath));
|
|
4985
5271
|
}
|
|
4986
5272
|
function retrustHook(name, scriptPath, version, source) {
|
|
4987
|
-
const actual = sha256Of(
|
|
5273
|
+
const actual = sha256Of(readFileSync5(scriptPath));
|
|
4988
5274
|
const db = getDb();
|
|
4989
5275
|
upsertHookRecord(db, {
|
|
4990
5276
|
name,
|
|
@@ -5025,8 +5311,8 @@ __export(exports_installer, {
|
|
|
5025
5311
|
getHookPath: () => getHookPath,
|
|
5026
5312
|
buildCodewithTomlFragment: () => buildCodewithTomlFragment
|
|
5027
5313
|
});
|
|
5028
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as
|
|
5029
|
-
import { join as
|
|
5314
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
5315
|
+
import { join as join8, dirname as dirname3 } from "path";
|
|
5030
5316
|
import { homedir as homedir5 } from "os";
|
|
5031
5317
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5032
5318
|
function normalizeHookName(name) {
|
|
@@ -5066,16 +5352,16 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
5066
5352
|
return globalOverride;
|
|
5067
5353
|
const dir = getTargetSettingsDir(target);
|
|
5068
5354
|
if (scope === "project") {
|
|
5069
|
-
return target === "codewith" ?
|
|
5355
|
+
return target === "codewith" ? join8(process.cwd(), dir, "config.toml") : join8(process.cwd(), dir, "settings.json");
|
|
5070
5356
|
}
|
|
5071
|
-
return target === "codewith" ?
|
|
5357
|
+
return target === "codewith" ? join8(homedir5(), dir, "config.toml") : join8(homedir5(), dir, "settings.json");
|
|
5072
5358
|
}
|
|
5073
5359
|
function getHookPath(name) {
|
|
5074
5360
|
const shortName = shortHookName(name);
|
|
5075
5361
|
const resolved = resolveHookDir(shortName);
|
|
5076
5362
|
if (resolved)
|
|
5077
5363
|
return resolved;
|
|
5078
|
-
return
|
|
5364
|
+
return join8(HOOKS_DIR, normalizeHookName(shortName));
|
|
5079
5365
|
}
|
|
5080
5366
|
function hookExists(name) {
|
|
5081
5367
|
return existsSync8(getHookPath(name));
|
|
@@ -5084,7 +5370,7 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
5084
5370
|
const path = getSettingsPath(scope, target);
|
|
5085
5371
|
try {
|
|
5086
5372
|
if (existsSync8(path)) {
|
|
5087
|
-
return JSON.parse(
|
|
5373
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
5088
5374
|
}
|
|
5089
5375
|
} catch (error) {
|
|
5090
5376
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -5183,7 +5469,7 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
5183
5469
|
function readCodewithConfig(scope, configPath) {
|
|
5184
5470
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
5185
5471
|
try {
|
|
5186
|
-
return existsSync8(path) ?
|
|
5472
|
+
return existsSync8(path) ? readFileSync6(path, "utf-8") : "";
|
|
5187
5473
|
} catch {
|
|
5188
5474
|
return "";
|
|
5189
5475
|
}
|
|
@@ -5193,7 +5479,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
|
|
|
5193
5479
|
const dir = dirname3(path);
|
|
5194
5480
|
if (!existsSync8(dir))
|
|
5195
5481
|
mkdirSync5(dir, { recursive: true });
|
|
5196
|
-
const existing = existsSync8(path) ?
|
|
5482
|
+
const existing = existsSync8(path) ? readFileSync6(path, "utf-8") : "";
|
|
5197
5483
|
const sep2 = existing.trim() ? `
|
|
5198
5484
|
|
|
5199
5485
|
` : "";
|
|
@@ -5238,7 +5524,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
5238
5524
|
target,
|
|
5239
5525
|
fragment,
|
|
5240
5526
|
applied: false,
|
|
5241
|
-
note: "Codewith install is fragment-only by default;
|
|
5527
|
+
note: "Codewith install is fragment-only by default; configs should own applying this TOML."
|
|
5242
5528
|
};
|
|
5243
5529
|
}
|
|
5244
5530
|
if (!codewithConfigPath) {
|
|
@@ -5265,7 +5551,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
5265
5551
|
fragment,
|
|
5266
5552
|
applied: true,
|
|
5267
5553
|
configPath: path,
|
|
5268
|
-
note: "Direct Codewith config write was explicitly requested; prefer
|
|
5554
|
+
note: "Direct Codewith config write was explicitly requested; prefer configs for managed machines."
|
|
5269
5555
|
};
|
|
5270
5556
|
} catch (error) {
|
|
5271
5557
|
return {
|
|
@@ -5496,7 +5782,7 @@ function codewithHasHookEntry(name, scope = "global") {
|
|
|
5496
5782
|
if (!existsSync8(path))
|
|
5497
5783
|
return false;
|
|
5498
5784
|
try {
|
|
5499
|
-
return codewithHasHookEntryInText(
|
|
5785
|
+
return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
|
|
5500
5786
|
} catch {
|
|
5501
5787
|
return false;
|
|
5502
5788
|
}
|
|
@@ -5528,7 +5814,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
5528
5814
|
if (target === "codewith" || target === "all") {
|
|
5529
5815
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
5530
5816
|
if (existsSync8(codewithPath)) {
|
|
5531
|
-
const before =
|
|
5817
|
+
const before = readFileSync6(codewithPath, "utf-8");
|
|
5532
5818
|
const after = removeCodewithHookEntry(before, shortName);
|
|
5533
5819
|
if (after.removed) {
|
|
5534
5820
|
writeFileSync4(codewithPath, after.text, "utf-8");
|
|
@@ -5574,7 +5860,7 @@ var init_installer = __esm(() => {
|
|
|
5574
5860
|
init_manifest();
|
|
5575
5861
|
init_store();
|
|
5576
5862
|
__dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
5577
|
-
HOOKS_DIR = existsSync8(
|
|
5863
|
+
HOOKS_DIR = existsSync8(join8(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join8(__dirname2, "..", "..", "hooks") : join8(__dirname2, "..", "hooks");
|
|
5578
5864
|
EVENT_MAP = {
|
|
5579
5865
|
claude: {
|
|
5580
5866
|
PreToolUse: "PreToolUse",
|
|
@@ -5721,14 +6007,14 @@ var init_redact = __esm(() => {
|
|
|
5721
6007
|
});
|
|
5722
6008
|
|
|
5723
6009
|
// src/lib/profiles.ts
|
|
5724
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as
|
|
5725
|
-
import { join as
|
|
6010
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
|
|
6011
|
+
import { join as join9 } from "path";
|
|
5726
6012
|
import { homedir as homedir6 } from "os";
|
|
5727
6013
|
function resolveProfilesDir() {
|
|
5728
|
-
const newDir =
|
|
5729
|
-
const oldDir =
|
|
6014
|
+
const newDir = join9(homedir6(), ".hasna", "hooks", "profiles");
|
|
6015
|
+
const oldDir = join9(homedir6(), ".hooks", "profiles");
|
|
5730
6016
|
if (!existsSync9(newDir) && existsSync9(oldDir)) {
|
|
5731
|
-
mkdirSync6(
|
|
6017
|
+
mkdirSync6(join9(homedir6(), ".hasna", "hooks"), { recursive: true });
|
|
5732
6018
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
5733
6019
|
}
|
|
5734
6020
|
return newDir;
|
|
@@ -5739,7 +6025,7 @@ function ensureProfilesDir() {
|
|
|
5739
6025
|
}
|
|
5740
6026
|
}
|
|
5741
6027
|
function profilePath(id) {
|
|
5742
|
-
return
|
|
6028
|
+
return join9(PROFILES_DIR, `${id}.json`);
|
|
5743
6029
|
}
|
|
5744
6030
|
function shortUuid() {
|
|
5745
6031
|
return crypto.randomUUID().slice(0, 8);
|
|
@@ -5767,7 +6053,7 @@ function getProfile(id) {
|
|
|
5767
6053
|
try {
|
|
5768
6054
|
if (!existsSync9(path))
|
|
5769
6055
|
return null;
|
|
5770
|
-
return JSON.parse(
|
|
6056
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
5771
6057
|
} catch {
|
|
5772
6058
|
return null;
|
|
5773
6059
|
}
|
|
@@ -5780,7 +6066,7 @@ function listProfiles() {
|
|
|
5780
6066
|
const profiles = [];
|
|
5781
6067
|
for (const file of files) {
|
|
5782
6068
|
try {
|
|
5783
|
-
const content =
|
|
6069
|
+
const content = readFileSync7(join9(PROFILES_DIR, file), "utf-8");
|
|
5784
6070
|
profiles.push(JSON.parse(content));
|
|
5785
6071
|
} catch {}
|
|
5786
6072
|
}
|
|
@@ -5923,7 +6209,7 @@ var init_db_writer = __esm(() => {
|
|
|
5923
6209
|
|
|
5924
6210
|
// src/lib/hook-env.ts
|
|
5925
6211
|
import { dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
|
|
5926
|
-
import { existsSync as existsSync10, statSync } from "fs";
|
|
6212
|
+
import { existsSync as existsSync10, statSync as statSync2 } from "fs";
|
|
5927
6213
|
function isDeniedEnvName(name) {
|
|
5928
6214
|
const upper = name.toUpperCase();
|
|
5929
6215
|
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
@@ -5950,7 +6236,7 @@ function isUnsafePathEntry(entry, home) {
|
|
|
5950
6236
|
if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
|
|
5951
6237
|
return true;
|
|
5952
6238
|
try {
|
|
5953
|
-
if ((
|
|
6239
|
+
if ((statSync2(entry).mode & 2) !== 0)
|
|
5954
6240
|
return true;
|
|
5955
6241
|
} catch {}
|
|
5956
6242
|
return false;
|
|
@@ -6083,7 +6369,7 @@ __export(exports_run, {
|
|
|
6083
6369
|
});
|
|
6084
6370
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
6085
6371
|
import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
6086
|
-
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as
|
|
6372
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join10 } from "path";
|
|
6087
6373
|
function extensionOf(scriptPath) {
|
|
6088
6374
|
const base = basename2(scriptPath);
|
|
6089
6375
|
const dot = base.lastIndexOf(".");
|
|
@@ -6154,7 +6440,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
6154
6440
|
async function executeVerifiedScript(options) {
|
|
6155
6441
|
const scriptDir = dirname5(options.scriptPath);
|
|
6156
6442
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
6157
|
-
const tempPath =
|
|
6443
|
+
const tempPath = join10(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
6158
6444
|
let proc = null;
|
|
6159
6445
|
let timedOut = false;
|
|
6160
6446
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -6245,15 +6531,15 @@ __export(exports_custom_install, {
|
|
|
6245
6531
|
installCustomSource: () => installCustomSource,
|
|
6246
6532
|
ensureCustomDir: () => ensureCustomDir
|
|
6247
6533
|
});
|
|
6248
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as
|
|
6249
|
-
import { join as
|
|
6534
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync4 } from "fs";
|
|
6535
|
+
import { join as join11 } from "path";
|
|
6250
6536
|
import { tmpdir } from "os";
|
|
6251
6537
|
function manifestAt(dir) {
|
|
6252
|
-
const path =
|
|
6538
|
+
const path = join11(dir, "manifest.json");
|
|
6253
6539
|
if (!existsSync11(path))
|
|
6254
6540
|
return;
|
|
6255
6541
|
try {
|
|
6256
|
-
return parseManifest(
|
|
6542
|
+
return parseManifest(readFileSync8(path, "utf-8"));
|
|
6257
6543
|
} catch {
|
|
6258
6544
|
return;
|
|
6259
6545
|
}
|
|
@@ -6272,8 +6558,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
6272
6558
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
6273
6559
|
}
|
|
6274
6560
|
async function cloneToTemp(gitUrl) {
|
|
6275
|
-
const tmp = mkdtempSync(
|
|
6276
|
-
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl,
|
|
6561
|
+
const tmp = mkdtempSync(join11(tmpdir(), "hooks-install-"));
|
|
6562
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join11(tmp, "repo")], {
|
|
6277
6563
|
stdout: "pipe",
|
|
6278
6564
|
stderr: "pipe"
|
|
6279
6565
|
});
|
|
@@ -6286,7 +6572,7 @@ async function cloneToTemp(gitUrl) {
|
|
|
6286
6572
|
rmSync4(tmp, { recursive: true, force: true });
|
|
6287
6573
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
6288
6574
|
}
|
|
6289
|
-
return
|
|
6575
|
+
return join11(tmp, "repo");
|
|
6290
6576
|
}
|
|
6291
6577
|
function isGitUrl(value) {
|
|
6292
6578
|
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
@@ -6314,7 +6600,7 @@ async function installCustomSource(source) {
|
|
|
6314
6600
|
throw new Error(`No such local path: ${source}`);
|
|
6315
6601
|
}
|
|
6316
6602
|
if (source.endsWith("manifest.json")) {
|
|
6317
|
-
const manifest = parseManifest(
|
|
6603
|
+
const manifest = parseManifest(readFileSync8(source, "utf-8"));
|
|
6318
6604
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
6319
6605
|
}
|
|
6320
6606
|
return copyManifestDir(source, source, "local");
|
|
@@ -6363,12 +6649,12 @@ __export(exports_sync, {
|
|
|
6363
6649
|
commitSyncArtifacts: () => commitSyncArtifacts
|
|
6364
6650
|
});
|
|
6365
6651
|
import { createHash as createHash2 } from "crypto";
|
|
6366
|
-
import { readFileSync as
|
|
6652
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
6367
6653
|
function sha256OfText(text) {
|
|
6368
6654
|
return createHash2("sha256").update(text).digest("hex");
|
|
6369
6655
|
}
|
|
6370
6656
|
function sha256OfPath(path) {
|
|
6371
|
-
return createHash2("sha256").update(
|
|
6657
|
+
return createHash2("sha256").update(readFileSync9(path)).digest("hex");
|
|
6372
6658
|
}
|
|
6373
6659
|
function collectBundledCatalog() {
|
|
6374
6660
|
const out = [];
|
|
@@ -7114,6 +7400,160 @@ function secureEqual(a, b) {
|
|
|
7114
7400
|
return diff === 0;
|
|
7115
7401
|
}
|
|
7116
7402
|
|
|
7403
|
+
// src/openapi.ts
|
|
7404
|
+
var openApiDocument;
|
|
7405
|
+
var init_openapi = __esm(() => {
|
|
7406
|
+
openApiDocument = {
|
|
7407
|
+
openapi: "3.1.0",
|
|
7408
|
+
info: {
|
|
7409
|
+
title: "Hooks Registry API",
|
|
7410
|
+
version: "0.1.0",
|
|
7411
|
+
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)."
|
|
7412
|
+
},
|
|
7413
|
+
servers: [{ url: "/" }],
|
|
7414
|
+
paths: {
|
|
7415
|
+
"/health": {
|
|
7416
|
+
get: {
|
|
7417
|
+
summary: "Liveness probe",
|
|
7418
|
+
operationId: "getHealth",
|
|
7419
|
+
responses: {
|
|
7420
|
+
"200": {
|
|
7421
|
+
description: "Service is alive",
|
|
7422
|
+
content: {
|
|
7423
|
+
"application/json": {
|
|
7424
|
+
schema: { type: "object", additionalProperties: true }
|
|
7425
|
+
}
|
|
7426
|
+
}
|
|
7427
|
+
}
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7430
|
+
},
|
|
7431
|
+
"/ready": {
|
|
7432
|
+
get: {
|
|
7433
|
+
summary: "Readiness probe \u2014 the hooks.lock store is readable",
|
|
7434
|
+
operationId: "getReady",
|
|
7435
|
+
responses: {
|
|
7436
|
+
"200": { description: "Lock store is readable" },
|
|
7437
|
+
"503": { description: "Lock store is not readable" }
|
|
7438
|
+
}
|
|
7439
|
+
}
|
|
7440
|
+
},
|
|
7441
|
+
"/version": {
|
|
7442
|
+
get: {
|
|
7443
|
+
summary: "Package version",
|
|
7444
|
+
operationId: "getVersion",
|
|
7445
|
+
responses: {
|
|
7446
|
+
"200": {
|
|
7447
|
+
description: "The installed @hasna/hooks version",
|
|
7448
|
+
content: {
|
|
7449
|
+
"application/json": {
|
|
7450
|
+
schema: { type: "object", properties: { version: { type: "string" } } }
|
|
7451
|
+
}
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
}
|
|
7455
|
+
}
|
|
7456
|
+
},
|
|
7457
|
+
"/openapi.json": {
|
|
7458
|
+
get: {
|
|
7459
|
+
summary: "This OpenAPI document",
|
|
7460
|
+
operationId: "getOpenApi",
|
|
7461
|
+
responses: { "200": { description: "OpenAPI 3.1 document" } }
|
|
7462
|
+
}
|
|
7463
|
+
},
|
|
7464
|
+
"/api/v1/catalog": {
|
|
7465
|
+
get: {
|
|
7466
|
+
summary: "List the full hook catalog with versions and sha256",
|
|
7467
|
+
operationId: "getCatalog",
|
|
7468
|
+
responses: {
|
|
7469
|
+
"200": {
|
|
7470
|
+
description: "Catalog entries",
|
|
7471
|
+
content: {
|
|
7472
|
+
"application/json": {
|
|
7473
|
+
schema: {
|
|
7474
|
+
type: "object",
|
|
7475
|
+
properties: {
|
|
7476
|
+
hooks: {
|
|
7477
|
+
type: "array",
|
|
7478
|
+
items: {
|
|
7479
|
+
type: "object",
|
|
7480
|
+
properties: {
|
|
7481
|
+
name: { type: "string" },
|
|
7482
|
+
version: { type: "string" },
|
|
7483
|
+
sha256: { type: "string" },
|
|
7484
|
+
events: { type: "array", items: { type: "string" } }
|
|
7485
|
+
}
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
}
|
|
7494
|
+
}
|
|
7495
|
+
},
|
|
7496
|
+
"/api/v1/lock": {
|
|
7497
|
+
get: {
|
|
7498
|
+
summary: "Read the published hooks.lock with per-hook versions",
|
|
7499
|
+
operationId: "getLock",
|
|
7500
|
+
responses: { "200": { description: "The lock file" } }
|
|
7501
|
+
}
|
|
7502
|
+
},
|
|
7503
|
+
"/api/v1/hooks/{name}/{version}": {
|
|
7504
|
+
get: {
|
|
7505
|
+
summary: "Fetch one hook's manifest and script",
|
|
7506
|
+
operationId: "getHookArtifact",
|
|
7507
|
+
parameters: [
|
|
7508
|
+
{ name: "name", in: "path", required: true, schema: { type: "string" } },
|
|
7509
|
+
{ name: "version", in: "path", required: true, schema: { type: "string" } }
|
|
7510
|
+
],
|
|
7511
|
+
responses: {
|
|
7512
|
+
"200": {
|
|
7513
|
+
description: "Hook artifact; x-hook-sha256 header carries the script hash"
|
|
7514
|
+
},
|
|
7515
|
+
"400": { description: "Invalid URL encoding or semver version" },
|
|
7516
|
+
"404": { description: "Hook not found locally" }
|
|
7517
|
+
}
|
|
7518
|
+
}
|
|
7519
|
+
},
|
|
7520
|
+
"/api/v1/hooks": {
|
|
7521
|
+
put: {
|
|
7522
|
+
summary: "Publish/retrust a hook (requires the API key)",
|
|
7523
|
+
operationId: "putHook",
|
|
7524
|
+
requestBody: {
|
|
7525
|
+
required: true,
|
|
7526
|
+
content: {
|
|
7527
|
+
"application/json": {
|
|
7528
|
+
schema: {
|
|
7529
|
+
type: "object",
|
|
7530
|
+
properties: { name: { type: "string" }, version: { type: "string" } }
|
|
7531
|
+
}
|
|
7532
|
+
}
|
|
7533
|
+
}
|
|
7534
|
+
},
|
|
7535
|
+
responses: {
|
|
7536
|
+
"200": { description: "Hook retrusted and pin updated" },
|
|
7537
|
+
"401": { description: "Missing or invalid API key" },
|
|
7538
|
+
"404": { description: "Hook not found in local store" },
|
|
7539
|
+
"409": { description: "Version mismatch with the local store" }
|
|
7540
|
+
},
|
|
7541
|
+
security: [{ apiKey: [] }]
|
|
7542
|
+
}
|
|
7543
|
+
}
|
|
7544
|
+
},
|
|
7545
|
+
components: {
|
|
7546
|
+
securitySchemes: {
|
|
7547
|
+
apiKey: {
|
|
7548
|
+
type: "http",
|
|
7549
|
+
scheme: "bearer",
|
|
7550
|
+
description: "The registry API key (HASNA_HOOKS_API_KEY / HOOKS_API_KEY)."
|
|
7551
|
+
}
|
|
7552
|
+
}
|
|
7553
|
+
}
|
|
7554
|
+
};
|
|
7555
|
+
});
|
|
7556
|
+
|
|
7117
7557
|
// src/serve.ts
|
|
7118
7558
|
var exports_serve = {};
|
|
7119
7559
|
__export(exports_serve, {
|
|
@@ -7123,6 +7563,17 @@ __export(exports_serve, {
|
|
|
7123
7563
|
SERVE_HOST: () => SERVE_HOST,
|
|
7124
7564
|
DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
|
|
7125
7565
|
});
|
|
7566
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
7567
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
7568
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7569
|
+
function packageVersion() {
|
|
7570
|
+
try {
|
|
7571
|
+
const pkgPath = join12(dirname6(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
7572
|
+
return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
|
|
7573
|
+
} catch {
|
|
7574
|
+
return "0.0.0";
|
|
7575
|
+
}
|
|
7576
|
+
}
|
|
7126
7577
|
async function buildCatalog() {
|
|
7127
7578
|
const byName = new Map;
|
|
7128
7579
|
for (const meta of HOOKS) {
|
|
@@ -7195,6 +7646,20 @@ function handleServeRequest(req, apiKey) {
|
|
|
7195
7646
|
if (url.pathname === "/health" && req.method === "GET") {
|
|
7196
7647
|
return Promise.resolve(json({ status: "ok", name: SERVE_SERVICE_NAME }));
|
|
7197
7648
|
}
|
|
7649
|
+
if (url.pathname === "/ready" && req.method === "GET") {
|
|
7650
|
+
try {
|
|
7651
|
+
readLock();
|
|
7652
|
+
return Promise.resolve(json({ status: "ready", name: SERVE_SERVICE_NAME }));
|
|
7653
|
+
} catch {
|
|
7654
|
+
return Promise.resolve(json({ status: "not-ready", name: SERVE_SERVICE_NAME }, 503));
|
|
7655
|
+
}
|
|
7656
|
+
}
|
|
7657
|
+
if (url.pathname === "/version" && req.method === "GET") {
|
|
7658
|
+
return Promise.resolve(json({ version: packageVersion() }));
|
|
7659
|
+
}
|
|
7660
|
+
if (url.pathname === "/openapi.json" && req.method === "GET") {
|
|
7661
|
+
return Promise.resolve(json(openApiDocument));
|
|
7662
|
+
}
|
|
7198
7663
|
if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
|
|
7199
7664
|
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
7200
7665
|
}
|
|
@@ -7273,6 +7738,7 @@ var init_serve = __esm(() => {
|
|
|
7273
7738
|
init_store();
|
|
7274
7739
|
init_resolve();
|
|
7275
7740
|
init_config();
|
|
7741
|
+
init_openapi();
|
|
7276
7742
|
init_semver();
|
|
7277
7743
|
if (false) {}
|
|
7278
7744
|
});
|
|
@@ -7373,9 +7839,9 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
7373
7839
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7374
7840
|
import { z as z2 } from "zod";
|
|
7375
7841
|
import { createServer } from "http";
|
|
7376
|
-
import { existsSync as existsSync12, readFileSync as
|
|
7377
|
-
import { join as
|
|
7378
|
-
import { fileURLToPath as
|
|
7842
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
|
|
7843
|
+
import { join as join13, dirname as dirname7 } from "path";
|
|
7844
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
7379
7845
|
async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
7380
7846
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
7381
7847
|
const resolved = resolveHook2(name);
|
|
@@ -7383,7 +7849,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
|
7383
7849
|
if (!resolved || !hookScript || !existsSync12(hookScript)) {
|
|
7384
7850
|
throw new Error(`Hook '${name}' not found`);
|
|
7385
7851
|
}
|
|
7386
|
-
const content =
|
|
7852
|
+
const content = readFileSync11(hookScript);
|
|
7387
7853
|
const sha = sha256Of(content);
|
|
7388
7854
|
const check = checkScriptHash(name, sha);
|
|
7389
7855
|
if (!check.ok) {
|
|
@@ -7608,14 +8074,14 @@ function createHooksServer() {
|
|
|
7608
8074
|
continue;
|
|
7609
8075
|
}
|
|
7610
8076
|
const hookDir = getHookPath(name);
|
|
7611
|
-
if (!existsSync12(
|
|
8077
|
+
if (!existsSync12(join13(hookDir, "src", "hook.ts"))) {
|
|
7612
8078
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
7613
8079
|
hookHealthy = false;
|
|
7614
8080
|
}
|
|
7615
8081
|
}
|
|
7616
8082
|
if (meta && settingsExist) {
|
|
7617
8083
|
try {
|
|
7618
|
-
const settings = JSON.parse(
|
|
8084
|
+
const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
7619
8085
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
7620
8086
|
const eventName = meta.event.split(":")[0];
|
|
7621
8087
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -7645,10 +8111,10 @@ function createHooksServer() {
|
|
|
7645
8111
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
7646
8112
|
}
|
|
7647
8113
|
const hookPath = getHookPath(name);
|
|
7648
|
-
const readmePath =
|
|
8114
|
+
const readmePath = join13(hookPath, "README.md");
|
|
7649
8115
|
let readme = "";
|
|
7650
8116
|
if (existsSync12(readmePath)) {
|
|
7651
|
-
readme =
|
|
8117
|
+
readme = readFileSync11(readmePath, "utf-8");
|
|
7652
8118
|
}
|
|
7653
8119
|
if (verbose) {
|
|
7654
8120
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -7978,7 +8444,7 @@ function createHooksServer() {
|
|
|
7978
8444
|
let settings = {};
|
|
7979
8445
|
try {
|
|
7980
8446
|
if (existsSync12(settingsPath))
|
|
7981
|
-
settings = JSON.parse(
|
|
8447
|
+
settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
7982
8448
|
} catch {}
|
|
7983
8449
|
if (!settings.hooks)
|
|
7984
8450
|
settings.hooks = {};
|
|
@@ -7987,8 +8453,8 @@ function createHooksServer() {
|
|
|
7987
8453
|
disabled.push(name);
|
|
7988
8454
|
settings.hooks.__disabled = disabled;
|
|
7989
8455
|
const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
|
|
7990
|
-
const { dirname:
|
|
7991
|
-
mkdirSync8(
|
|
8456
|
+
const { dirname: dirname8 } = await import("path");
|
|
8457
|
+
mkdirSync8(dirname8(settingsPath), { recursive: true });
|
|
7992
8458
|
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
7993
8459
|
`);
|
|
7994
8460
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
@@ -8001,7 +8467,7 @@ function createHooksServer() {
|
|
|
8001
8467
|
let settings = {};
|
|
8002
8468
|
try {
|
|
8003
8469
|
if (existsSync12(settingsPath))
|
|
8004
|
-
settings = JSON.parse(
|
|
8470
|
+
settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
|
|
8005
8471
|
} catch {}
|
|
8006
8472
|
if (settings.hooks?.__disabled) {
|
|
8007
8473
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|
|
@@ -8361,13 +8827,13 @@ var init_server = __esm(() => {
|
|
|
8361
8827
|
init_store();
|
|
8362
8828
|
init_redact();
|
|
8363
8829
|
init_storage();
|
|
8364
|
-
__dirname3 =
|
|
8830
|
+
__dirname3 = dirname7(fileURLToPath4(import.meta.url));
|
|
8365
8831
|
pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
8366
8832
|
try {
|
|
8367
8833
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
8368
|
-
const p =
|
|
8834
|
+
const p = join13(__dirname3, rel);
|
|
8369
8835
|
if (existsSync12(p)) {
|
|
8370
|
-
pkg = JSON.parse(
|
|
8836
|
+
pkg = JSON.parse(readFileSync11(p, "utf-8"));
|
|
8371
8837
|
break;
|
|
8372
8838
|
}
|
|
8373
8839
|
}
|
|
@@ -9638,9 +10104,9 @@ var {
|
|
|
9638
10104
|
|
|
9639
10105
|
// src/cli/index.tsx
|
|
9640
10106
|
import chalk2 from "chalk";
|
|
9641
|
-
import { existsSync as existsSync13, readFileSync as
|
|
9642
|
-
import { join as
|
|
9643
|
-
import { fileURLToPath as
|
|
10107
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12, rmSync as rmSync5 } from "fs";
|
|
10108
|
+
import { join as join14, dirname as dirname8 } from "path";
|
|
10109
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
9644
10110
|
|
|
9645
10111
|
// src/cli/components/App.tsx
|
|
9646
10112
|
import { useState as useState7 } from "react";
|
|
@@ -10821,9 +11287,9 @@ init_resolve();
|
|
|
10821
11287
|
init_store();
|
|
10822
11288
|
init_semver();
|
|
10823
11289
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
10824
|
-
var __dirname4 =
|
|
10825
|
-
var pkgPath = existsSync13(
|
|
10826
|
-
var pkg2 = JSON.parse(
|
|
11290
|
+
var __dirname4 = dirname8(fileURLToPath5(import.meta.url));
|
|
11291
|
+
var pkgPath = existsSync13(join14(__dirname4, "..", "package.json")) ? join14(__dirname4, "..", "package.json") : join14(__dirname4, "..", "..", "package.json");
|
|
11292
|
+
var pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
10827
11293
|
var program2 = new Command;
|
|
10828
11294
|
function resolveScope(options) {
|
|
10829
11295
|
if (options.project)
|
|
@@ -10857,7 +11323,7 @@ function projectLogRows2(rows) {
|
|
|
10857
11323
|
function readToken(tokenFile) {
|
|
10858
11324
|
if (tokenFile) {
|
|
10859
11325
|
try {
|
|
10860
|
-
return
|
|
11326
|
+
return readFileSync12(tokenFile, "utf-8").trim();
|
|
10861
11327
|
} catch {
|
|
10862
11328
|
return;
|
|
10863
11329
|
}
|
|
@@ -10994,7 +11460,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
10994
11460
|
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
|
|
10995
11461
|
process.exit(1);
|
|
10996
11462
|
}
|
|
10997
|
-
const content =
|
|
11463
|
+
const content = readFileSync12(hookScript);
|
|
10998
11464
|
const sha = sha256Of2(content);
|
|
10999
11465
|
const check = checkScriptHash2(hook, sha);
|
|
11000
11466
|
if (!check.ok) {
|
|
@@ -11100,7 +11566,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
|
|
|
11100
11566
|
process.stderr.write(stderr);
|
|
11101
11567
|
process.exit(exitCode);
|
|
11102
11568
|
});
|
|
11103
|
-
program2.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, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").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
|
|
11569
|
+
program2.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, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").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) => {
|
|
11104
11570
|
const scope = resolveScope(options);
|
|
11105
11571
|
const target = resolveTarget(options);
|
|
11106
11572
|
let toInstall = hooks;
|
|
@@ -11288,7 +11754,7 @@ Dry run \u2014 would install (${scope}, ${target}):
|
|
|
11288
11754
|
return;
|
|
11289
11755
|
}
|
|
11290
11756
|
const successCount = results.filter((r) => r.success).length;
|
|
11291
|
-
const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (
|
|
11757
|
+
const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (configs should apply)" : scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
|
|
11292
11758
|
console.log(chalk2.bold(`
|
|
11293
11759
|
Installing hooks (${scope}, ${target})...
|
|
11294
11760
|
`));
|
|
@@ -11570,7 +12036,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
11570
12036
|
let wiringCount = 0;
|
|
11571
12037
|
if (settingsExist) {
|
|
11572
12038
|
try {
|
|
11573
|
-
const settings = JSON.parse(
|
|
12039
|
+
const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
|
|
11574
12040
|
wiringCount = countSettingsWiring(settings);
|
|
11575
12041
|
} catch {}
|
|
11576
12042
|
}
|
|
@@ -11590,7 +12056,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
11590
12056
|
continue;
|
|
11591
12057
|
}
|
|
11592
12058
|
const hookDir = getHookPath(name);
|
|
11593
|
-
const hookScript =
|
|
12059
|
+
const hookScript = join14(hookDir, "src", "hook.ts");
|
|
11594
12060
|
if (!existsSync13(hookScript)) {
|
|
11595
12061
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
11596
12062
|
hookHealthy = false;
|
|
@@ -11598,7 +12064,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
|
|
|
11598
12064
|
}
|
|
11599
12065
|
if (meta && settingsExist) {
|
|
11600
12066
|
try {
|
|
11601
|
-
const settings = JSON.parse(
|
|
12067
|
+
const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
|
|
11602
12068
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
11603
12069
|
const eventName = meta.event.split(":")[0];
|
|
11604
12070
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -11765,10 +12231,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
|
|
|
11765
12231
|
return;
|
|
11766
12232
|
}
|
|
11767
12233
|
const hookPath = getHookPath(hook);
|
|
11768
|
-
const readmePath =
|
|
12234
|
+
const readmePath = join14(hookPath, "README.md");
|
|
11769
12235
|
let readme = "";
|
|
11770
12236
|
if (existsSync13(readmePath)) {
|
|
11771
|
-
readme =
|
|
12237
|
+
readme = readFileSync12(readmePath, "utf-8");
|
|
11772
12238
|
}
|
|
11773
12239
|
if (options.json) {
|
|
11774
12240
|
console.log(JSON.stringify({ ...meta, readme }));
|
|
@@ -11822,7 +12288,7 @@ ${meta.displayName} v${meta.version}
|
|
|
11822
12288
|
installation: {
|
|
11823
12289
|
global: "hooks install gitguard",
|
|
11824
12290
|
project: "hooks install gitguard --project",
|
|
11825
|
-
codewith: "hooks install session-start --target codewith # emits TOML for
|
|
12291
|
+
codewith: "hooks install session-start --target codewith # emits TOML for configs to apply",
|
|
11826
12292
|
category: 'hooks install --category "Git Safety"',
|
|
11827
12293
|
all: "hooks install --all"
|
|
11828
12294
|
},
|
|
@@ -11965,9 +12431,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
|
|
|
11965
12431
|
if (file === "-") {
|
|
11966
12432
|
raw = await new Response(Bun.stdin.stream()).text();
|
|
11967
12433
|
} else {
|
|
11968
|
-
const { readFileSync:
|
|
12434
|
+
const { readFileSync: readFileSync13 } = await import("fs");
|
|
11969
12435
|
try {
|
|
11970
|
-
raw =
|
|
12436
|
+
raw = readFileSync13(file, "utf-8");
|
|
11971
12437
|
} catch {
|
|
11972
12438
|
if (options.json) {
|
|
11973
12439
|
console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
|