@hasna/hooks 0.6.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/bin/index.js +1024 -461
  3. package/bin/serve.js +626 -158
  4. package/dist/config.d.ts +7 -5
  5. package/dist/db/pg-migrations.d.ts +1 -1
  6. package/dist/index.js +898 -340
  7. package/dist/lib/hook-env.d.ts +60 -4
  8. package/dist/lib/manifest.d.ts +11 -0
  9. package/dist/lib/semver.d.ts +27 -0
  10. package/dist/openapi.d.ts +216 -0
  11. package/dist/storage.js +34 -34
  12. package/hooks/codewith-native-common.test.ts +25 -25
  13. package/hooks/codewith-native-common.ts +1 -1
  14. package/hooks/hook-autoformat/package.json +1 -1
  15. package/hooks/hook-desktopnotify/package.json +1 -1
  16. package/hooks/hook-envsetup/package.json +1 -1
  17. package/hooks/hook-knowledge-context/README.md +1 -1
  18. package/hooks/hook-knowledge-context/src/hook.test.ts +1 -1
  19. package/hooks/hook-permissionguard/package.json +1 -1
  20. package/hooks/hook-protectfiles/package.json +1 -1
  21. package/hooks/hook-spiral-detector/LICENSE +191 -0
  22. package/hooks/hook-spiral-detector/README.md +23 -0
  23. package/hooks/hook-spiral-detector/package.json +12 -0
  24. package/hooks/hook-spiral-detector/src/hook.test.ts +72 -0
  25. package/hooks/hook-spiral-detector/src/hook.ts +93 -0
  26. package/hooks/hook-workspace-repos-guard/README.md +4 -3
  27. package/hooks/hook-workspace-repos-guard/src/hook.test.ts +7 -7
  28. package/hooks/hook-workspace-repos-guard/src/hook.ts +5 -3
  29. package/hooks/mention-context/src/hasna-mention-context.py +2 -2
  30. package/hooks/mention-context/src/hasna-mention-warm.py +1 -1
  31. package/hooks/pre-bash/README.md +1 -1
  32. package/hooks/prompt-guard/README.md +1 -1
  33. package/hooks/session-start/README.md +1 -1
  34. package/hooks/stop-sync/README.md +1 -1
  35. package/hooks/worktree-guard/README.md +1 -1
  36. package/package.json +10 -6
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ function up(db) {
49
49
  db.exec(idx);
50
50
  }
51
51
  }
52
- var init_001_initial = __esm(() => {
52
+ var init__001_initial = __esm(() => {
53
53
  init_schema();
54
54
  });
55
55
 
@@ -78,7 +78,7 @@ function up2(db) {
78
78
  throw err;
79
79
  }
80
80
  }
81
- var init_002_session_events = __esm(() => {
81
+ var init__002_session_events = __esm(() => {
82
82
  init_schema();
83
83
  });
84
84
 
@@ -107,7 +107,7 @@ function up3(db) {
107
107
  throw err;
108
108
  }
109
109
  }
110
- var init_003_user_prompt_submit_event = __esm(() => {
110
+ var init__003_user_prompt_submit_event = __esm(() => {
111
111
  init_schema();
112
112
  });
113
113
 
@@ -131,7 +131,7 @@ var CREATE_HOOKS_TABLE = `
131
131
  last_verified_at TEXT
132
132
  )
133
133
  `, CREATE_HOOKS_INDEXES;
134
- var init_004_hooks_table = __esm(() => {
134
+ var init__004_hooks_table = __esm(() => {
135
135
  CREATE_HOOKS_INDEXES = [
136
136
  `CREATE INDEX IF NOT EXISTS idx_hooks_name ON hooks (name)`
137
137
  ];
@@ -162,7 +162,7 @@ function up5(db) {
162
162
  throw err;
163
163
  }
164
164
  }
165
- var init_005_subagent_start_event = __esm(() => {
165
+ var init__005_subagent_start_event = __esm(() => {
166
166
  init_schema();
167
167
  });
168
168
 
@@ -194,11 +194,11 @@ function runMigrations(db) {
194
194
  }
195
195
  var MIGRATIONS;
196
196
  var init_migrations = __esm(() => {
197
- init_001_initial();
198
- init_002_session_events();
199
- init_003_user_prompt_submit_event();
200
- init_004_hooks_table();
201
- init_005_subagent_start_event();
197
+ init__001_initial();
198
+ init__002_session_events();
199
+ init__003_user_prompt_submit_event();
200
+ init__004_hooks_table();
201
+ init__005_subagent_start_event();
202
202
  MIGRATIONS = [
203
203
  { version: "001_initial", up },
204
204
  { version: "002_session_events", up: up2 },
@@ -453,7 +453,7 @@ var init_redact = __esm(() => {
453
453
  ];
454
454
  });
455
455
 
456
- // node_modules/zod/v3/helpers/util.js
456
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
457
457
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
458
458
  const t = typeof data;
459
459
  switch (t) {
@@ -584,7 +584,7 @@ var init_util = __esm(() => {
584
584
  ]);
585
585
  });
586
586
 
587
- // node_modules/zod/v3/ZodError.js
587
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
588
588
  var ZodIssueCode, quotelessJson = (obj) => {
589
589
  const json = JSON.stringify(obj, null, 2);
590
590
  return json.replace(/"([^"]+)":/g, "$1:");
@@ -705,7 +705,7 @@ var init_ZodError = __esm(() => {
705
705
  };
706
706
  });
707
707
 
708
- // node_modules/zod/v3/locales/en.js
708
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
709
709
  var errorMap = (issue, _ctx) => {
710
710
  let message;
711
711
  switch (issue.code) {
@@ -812,7 +812,7 @@ var init_en = __esm(() => {
812
812
  en_default = errorMap;
813
813
  });
814
814
 
815
- // node_modules/zod/v3/errors.js
815
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
816
816
  function setErrorMap(map) {
817
817
  overrideErrorMap = map;
818
818
  }
@@ -825,7 +825,7 @@ var init_errors = __esm(() => {
825
825
  overrideErrorMap = en_default;
826
826
  });
827
827
 
828
- // node_modules/zod/v3/helpers/parseUtil.js
828
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
829
829
  function addIssueToContext(ctx, issueData) {
830
830
  const overrideMap = getErrorMap();
831
831
  const issue = makeIssue({
@@ -930,10 +930,10 @@ var init_parseUtil = __esm(() => {
930
930
  });
931
931
  });
932
932
 
933
- // node_modules/zod/v3/helpers/typeAliases.js
933
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typeAliases.js
934
934
  var init_typeAliases = () => {};
935
935
 
936
- // node_modules/zod/v3/helpers/errorUtil.js
936
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
937
937
  var errorUtil;
938
938
  var init_errorUtil = __esm(() => {
939
939
  (function(errorUtil2) {
@@ -942,7 +942,7 @@ var init_errorUtil = __esm(() => {
942
942
  })(errorUtil || (errorUtil = {}));
943
943
  });
944
944
 
945
- // node_modules/zod/v3/types.js
945
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
946
946
  class ParseInputLazyPath {
947
947
  constructor(parent, value, path, key) {
948
948
  this._cachedPath = [];
@@ -4293,116 +4293,116 @@ var init_types = __esm(() => {
4293
4293
  NEVER = INVALID;
4294
4294
  });
4295
4295
 
4296
- // node_modules/zod/v3/external.js
4296
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
4297
4297
  var exports_external = {};
4298
4298
  __export(exports_external, {
4299
- void: () => voidType,
4300
- util: () => util,
4301
- unknown: () => unknownType,
4302
- union: () => unionType,
4303
- undefined: () => undefinedType,
4304
- tuple: () => tupleType,
4305
- transformer: () => effectsType,
4306
- symbol: () => symbolType,
4307
- string: () => stringType,
4308
- strictObject: () => strictObjectType,
4309
- setErrorMap: () => setErrorMap,
4310
- set: () => setType,
4311
- record: () => recordType,
4312
- quotelessJson: () => quotelessJson,
4313
- promise: () => promiseType,
4314
- preprocess: () => preprocessType,
4315
- pipeline: () => pipelineType,
4316
- ostring: () => ostring,
4317
- optional: () => optionalType,
4318
- onumber: () => onumber,
4319
- oboolean: () => oboolean,
4320
- objectUtil: () => objectUtil,
4321
- object: () => objectType,
4322
- number: () => numberType,
4323
- nullable: () => nullableType,
4324
- null: () => nullType,
4325
- never: () => neverType,
4326
- nativeEnum: () => nativeEnumType,
4327
- nan: () => nanType,
4328
- map: () => mapType,
4329
- makeIssue: () => makeIssue,
4330
- literal: () => literalType,
4331
- lazy: () => lazyType,
4332
- late: () => late,
4333
- isValid: () => isValid,
4334
- isDirty: () => isDirty,
4335
- isAsync: () => isAsync,
4336
- isAborted: () => isAborted,
4337
- intersection: () => intersectionType,
4338
- instanceof: () => instanceOfType,
4339
- getParsedType: () => getParsedType,
4340
- getErrorMap: () => getErrorMap,
4341
- function: () => functionType,
4342
- enum: () => enumType,
4343
- effect: () => effectsType,
4344
- discriminatedUnion: () => discriminatedUnionType,
4345
- defaultErrorMap: () => en_default,
4346
- datetimeRegex: () => datetimeRegex,
4347
- date: () => dateType,
4348
- custom: () => custom,
4349
- coerce: () => coerce,
4350
- boolean: () => booleanType,
4351
- bigint: () => bigIntType,
4352
- array: () => arrayType,
4353
- any: () => anyType,
4354
- addIssueToContext: () => addIssueToContext,
4355
- ZodVoid: () => ZodVoid,
4356
- ZodUnknown: () => ZodUnknown,
4357
- ZodUnion: () => ZodUnion,
4358
- ZodUndefined: () => ZodUndefined,
4359
- ZodType: () => ZodType,
4360
- ZodTuple: () => ZodTuple,
4361
- ZodTransformer: () => ZodEffects,
4362
- ZodSymbol: () => ZodSymbol,
4363
- ZodString: () => ZodString,
4364
- ZodSet: () => ZodSet,
4365
- ZodSchema: () => ZodType,
4366
- ZodRecord: () => ZodRecord,
4367
- ZodReadonly: () => ZodReadonly,
4368
- ZodPromise: () => ZodPromise,
4369
- ZodPipeline: () => ZodPipeline,
4370
- ZodParsedType: () => ZodParsedType,
4371
- ZodOptional: () => ZodOptional,
4372
- ZodObject: () => ZodObject,
4373
- ZodNumber: () => ZodNumber,
4374
- ZodNullable: () => ZodNullable,
4375
- ZodNull: () => ZodNull,
4376
- ZodNever: () => ZodNever,
4377
- ZodNativeEnum: () => ZodNativeEnum,
4378
- ZodNaN: () => ZodNaN,
4379
- ZodMap: () => ZodMap,
4380
- ZodLiteral: () => ZodLiteral,
4381
- ZodLazy: () => ZodLazy,
4382
- ZodIssueCode: () => ZodIssueCode,
4383
- ZodIntersection: () => ZodIntersection,
4384
- ZodFunction: () => ZodFunction,
4385
- ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind,
4386
- ZodError: () => ZodError,
4387
- ZodEnum: () => ZodEnum,
4388
- ZodEffects: () => ZodEffects,
4389
- ZodDiscriminatedUnion: () => ZodDiscriminatedUnion,
4390
- ZodDefault: () => ZodDefault,
4391
- ZodDate: () => ZodDate,
4392
- ZodCatch: () => ZodCatch,
4393
- ZodBranded: () => ZodBranded,
4394
- ZodBoolean: () => ZodBoolean,
4395
- ZodBigInt: () => ZodBigInt,
4396
- ZodArray: () => ZodArray,
4397
- ZodAny: () => ZodAny,
4398
- Schema: () => ZodType,
4399
- ParseStatus: () => ParseStatus,
4400
- OK: () => OK,
4401
- NEVER: () => NEVER,
4402
- INVALID: () => INVALID,
4403
- EMPTY_PATH: () => EMPTY_PATH,
4299
+ BRAND: () => BRAND,
4404
4300
  DIRTY: () => DIRTY,
4405
- BRAND: () => BRAND
4301
+ EMPTY_PATH: () => EMPTY_PATH,
4302
+ INVALID: () => INVALID,
4303
+ NEVER: () => NEVER,
4304
+ OK: () => OK,
4305
+ ParseStatus: () => ParseStatus,
4306
+ Schema: () => ZodType,
4307
+ ZodAny: () => ZodAny,
4308
+ ZodArray: () => ZodArray,
4309
+ ZodBigInt: () => ZodBigInt,
4310
+ ZodBoolean: () => ZodBoolean,
4311
+ ZodBranded: () => ZodBranded,
4312
+ ZodCatch: () => ZodCatch,
4313
+ ZodDate: () => ZodDate,
4314
+ ZodDefault: () => ZodDefault,
4315
+ ZodDiscriminatedUnion: () => ZodDiscriminatedUnion,
4316
+ ZodEffects: () => ZodEffects,
4317
+ ZodEnum: () => ZodEnum,
4318
+ ZodError: () => ZodError,
4319
+ ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind,
4320
+ ZodFunction: () => ZodFunction,
4321
+ ZodIntersection: () => ZodIntersection,
4322
+ ZodIssueCode: () => ZodIssueCode,
4323
+ ZodLazy: () => ZodLazy,
4324
+ ZodLiteral: () => ZodLiteral,
4325
+ ZodMap: () => ZodMap,
4326
+ ZodNaN: () => ZodNaN,
4327
+ ZodNativeEnum: () => ZodNativeEnum,
4328
+ ZodNever: () => ZodNever,
4329
+ ZodNull: () => ZodNull,
4330
+ ZodNullable: () => ZodNullable,
4331
+ ZodNumber: () => ZodNumber,
4332
+ ZodObject: () => ZodObject,
4333
+ ZodOptional: () => ZodOptional,
4334
+ ZodParsedType: () => ZodParsedType,
4335
+ ZodPipeline: () => ZodPipeline,
4336
+ ZodPromise: () => ZodPromise,
4337
+ ZodReadonly: () => ZodReadonly,
4338
+ ZodRecord: () => ZodRecord,
4339
+ ZodSchema: () => ZodType,
4340
+ ZodSet: () => ZodSet,
4341
+ ZodString: () => ZodString,
4342
+ ZodSymbol: () => ZodSymbol,
4343
+ ZodTransformer: () => ZodEffects,
4344
+ ZodTuple: () => ZodTuple,
4345
+ ZodType: () => ZodType,
4346
+ ZodUndefined: () => ZodUndefined,
4347
+ ZodUnion: () => ZodUnion,
4348
+ ZodUnknown: () => ZodUnknown,
4349
+ ZodVoid: () => ZodVoid,
4350
+ addIssueToContext: () => addIssueToContext,
4351
+ any: () => anyType,
4352
+ array: () => arrayType,
4353
+ bigint: () => bigIntType,
4354
+ boolean: () => booleanType,
4355
+ coerce: () => coerce,
4356
+ custom: () => custom,
4357
+ date: () => dateType,
4358
+ datetimeRegex: () => datetimeRegex,
4359
+ defaultErrorMap: () => en_default,
4360
+ discriminatedUnion: () => discriminatedUnionType,
4361
+ effect: () => effectsType,
4362
+ enum: () => enumType,
4363
+ function: () => functionType,
4364
+ getErrorMap: () => getErrorMap,
4365
+ getParsedType: () => getParsedType,
4366
+ instanceof: () => instanceOfType,
4367
+ intersection: () => intersectionType,
4368
+ isAborted: () => isAborted,
4369
+ isAsync: () => isAsync,
4370
+ isDirty: () => isDirty,
4371
+ isValid: () => isValid,
4372
+ late: () => late,
4373
+ lazy: () => lazyType,
4374
+ literal: () => literalType,
4375
+ makeIssue: () => makeIssue,
4376
+ map: () => mapType,
4377
+ nan: () => nanType,
4378
+ nativeEnum: () => nativeEnumType,
4379
+ never: () => neverType,
4380
+ null: () => nullType,
4381
+ nullable: () => nullableType,
4382
+ number: () => numberType,
4383
+ object: () => objectType,
4384
+ objectUtil: () => objectUtil,
4385
+ oboolean: () => oboolean,
4386
+ onumber: () => onumber,
4387
+ optional: () => optionalType,
4388
+ ostring: () => ostring,
4389
+ pipeline: () => pipelineType,
4390
+ preprocess: () => preprocessType,
4391
+ promise: () => promiseType,
4392
+ quotelessJson: () => quotelessJson,
4393
+ record: () => recordType,
4394
+ set: () => setType,
4395
+ setErrorMap: () => setErrorMap,
4396
+ strictObject: () => strictObjectType,
4397
+ string: () => stringType,
4398
+ symbol: () => symbolType,
4399
+ transformer: () => effectsType,
4400
+ tuple: () => tupleType,
4401
+ undefined: () => undefinedType,
4402
+ union: () => unionType,
4403
+ unknown: () => unknownType,
4404
+ util: () => util,
4405
+ void: () => voidType
4406
4406
  });
4407
4407
  var init_external = __esm(() => {
4408
4408
  init_errors();
@@ -4413,50 +4413,324 @@ var init_external = __esm(() => {
4413
4413
  init_ZodError();
4414
4414
  });
4415
4415
 
4416
- // node_modules/zod/index.js
4416
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/index.js
4417
4417
  var init_zod = __esm(() => {
4418
4418
  init_external();
4419
4419
  init_external();
4420
4420
  });
4421
4421
 
4422
- // src/config.ts
4423
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
4422
+ // ../contracts/dist/client/transport.js
4423
+ import { readFileSync as readFileSync2, statSync } from "fs";
4424
4424
  import { join as join3 } from "path";
4425
+ function envToken(name) {
4426
+ return name.toUpperCase().replace(/-/g, "_");
4427
+ }
4428
+ function clientTransportEnvKeys(name) {
4429
+ const envSegment = envToken(name);
4430
+ return {
4431
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
4432
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
4433
+ };
4434
+ }
4435
+ function credentialOverrideEnvKey(name) {
4436
+ return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
4437
+ }
4438
+ function homeDir(env) {
4439
+ const home = env.HOME?.trim();
4440
+ return home ? home : null;
4441
+ }
4442
+ function credentialDiskSources(name, env) {
4443
+ return profileDiskSources(name, env, null);
4444
+ }
4445
+ function profileDiskSources(name, env, profile) {
4446
+ const home = homeDir(env);
4447
+ if (!home || !SAFE_APP_SLUG.test(name))
4448
+ return [];
4449
+ const stem = profile ? `${name}.${profile}` : name;
4450
+ const configStem = profile ? `${name}-${profile}` : name;
4451
+ return [
4452
+ join3(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
4453
+ join3(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
4454
+ ];
4455
+ }
4456
+ function parseEnvFile(text) {
4457
+ const values = new Map;
4458
+ for (const rawLine of text.split(/\r?\n/)) {
4459
+ const line = rawLine.trim();
4460
+ if (line.length === 0 || line.startsWith("#"))
4461
+ continue;
4462
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
4463
+ const equals = withoutExport.indexOf("=");
4464
+ if (equals <= 0)
4465
+ continue;
4466
+ const key = withoutExport.slice(0, equals).trim();
4467
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
4468
+ continue;
4469
+ let value = withoutExport.slice(equals + 1).trim();
4470
+ const quote = value[0];
4471
+ if (quote === '"' || quote === "'") {
4472
+ if (value.length < 2 || !value.endsWith(quote))
4473
+ continue;
4474
+ value = value.slice(1, -1);
4475
+ }
4476
+ if (value.length === 0)
4477
+ continue;
4478
+ values.set(key, value);
4479
+ }
4480
+ return values;
4481
+ }
4482
+ function readAppConfigFile(path) {
4483
+ let text;
4484
+ try {
4485
+ const stats = statSync(path);
4486
+ if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
4487
+ return null;
4488
+ text = readFileSync2(path, "utf8");
4489
+ } catch {
4490
+ return null;
4491
+ }
4492
+ return parseEnvFile(text);
4493
+ }
4494
+ function readCredentialFile(path, apiKeyKeys) {
4495
+ const values = readAppConfigFile(path);
4496
+ if (!values)
4497
+ return null;
4498
+ for (const key of apiKeyKeys) {
4499
+ const value = values.get(key)?.trim();
4500
+ if (value)
4501
+ return value;
4502
+ }
4503
+ return null;
4504
+ }
4505
+ function assertUsableCredential(appName, source, value) {
4506
+ if (!ILLEGAL_IN_HEADER_VALUE.test(value))
4507
+ return;
4508
+ 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]);
4509
+ }
4510
+ function sealCredential(fields) {
4511
+ const { apiKey } = fields;
4512
+ const visible = {
4513
+ tier: fields.tier,
4514
+ source: fields.source,
4515
+ deliberate: fields.deliberate,
4516
+ deprecated: fields.deprecated,
4517
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
4518
+ warning: fields.warning
4519
+ };
4520
+ const sealed = { ...visible };
4521
+ Object.defineProperty(sealed, "apiKey", {
4522
+ value: apiKey,
4523
+ enumerable: false,
4524
+ writable: false,
4525
+ configurable: false
4526
+ });
4527
+ Object.defineProperty(sealed, INSPECT_CUSTOM, {
4528
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
4529
+ enumerable: false,
4530
+ writable: false,
4531
+ configurable: false
4532
+ });
4533
+ Object.defineProperty(sealed, CREDENTIAL_SEAL, {
4534
+ value: true,
4535
+ enumerable: false,
4536
+ writable: false,
4537
+ configurable: false
4538
+ });
4539
+ return Object.freeze(sealed);
4540
+ }
4541
+ function firstEnvValue(env, keys) {
4542
+ for (const key of keys) {
4543
+ const value = env[key]?.trim();
4544
+ if (value)
4545
+ return { key, value };
4546
+ }
4547
+ return null;
4548
+ }
4549
+ function deprecationNotified() {
4550
+ const host = globalThis;
4551
+ const existing = host[DEPRECATION_REGISTRY];
4552
+ if (existing instanceof Set)
4553
+ return existing;
4554
+ const created = new Set;
4555
+ host[DEPRECATION_REGISTRY] = created;
4556
+ return created;
4557
+ }
4558
+ function defaultDeprecationSink(message) {
4559
+ if (typeof process !== "undefined" && process.stderr) {
4560
+ process.stderr.write(`${message}
4561
+ `);
4562
+ }
4563
+ }
4564
+ function resolveCredential(name, env, options = {}) {
4565
+ const { apiKeyKeys } = clientTransportEnvKeys(name);
4566
+ const diskPaths = credentialDiskSources(name, env);
4567
+ const explicitKey = options.apiKey?.trim();
4568
+ if (explicitKey) {
4569
+ assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
4570
+ return sealCredential({
4571
+ apiKey: explicitKey,
4572
+ tier: "argument",
4573
+ source: "explicit apiKey argument",
4574
+ deliberate: true,
4575
+ deprecated: false,
4576
+ diskCandidates: diskPaths,
4577
+ warning: null
4578
+ });
4579
+ }
4580
+ const overrideKeyName = credentialOverrideEnvKey(name);
4581
+ const overrideRaw = env[overrideKeyName];
4582
+ if (overrideRaw !== undefined) {
4583
+ const override = overrideRaw.trim();
4584
+ if (!override) {
4585
+ 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]);
4586
+ }
4587
+ assertUsableCredential(name, overrideKeyName, override);
4588
+ return sealCredential({
4589
+ apiKey: override,
4590
+ tier: "override",
4591
+ source: overrideKeyName,
4592
+ deliberate: true,
4593
+ deprecated: false,
4594
+ diskCandidates: diskPaths,
4595
+ warning: null
4596
+ });
4597
+ }
4598
+ const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
4599
+ if (profile) {
4600
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
4601
+ if (!SAFE_PROFILE.test(profile)) {
4602
+ throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
4603
+ }
4604
+ const paths = profileDiskSources(name, env, profile);
4605
+ for (const path of paths) {
4606
+ const value = readCredentialFile(path, apiKeyKeys);
4607
+ if (value) {
4608
+ assertUsableCredential(name, path, value);
4609
+ return sealCredential({
4610
+ apiKey: value,
4611
+ tier: "profile",
4612
+ source: path,
4613
+ deliberate: true,
4614
+ deprecated: false,
4615
+ diskCandidates: paths,
4616
+ warning: null
4617
+ });
4618
+ }
4619
+ }
4620
+ 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);
4621
+ }
4622
+ const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
4623
+ if (diskHits.length > 0) {
4624
+ const winner = diskHits[0];
4625
+ assertUsableCredential(name, winner.path, winner.value);
4626
+ const divergentSources = [
4627
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
4628
+ ...(() => {
4629
+ const legacyHit = firstEnvValue(env, apiKeyKeys);
4630
+ return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
4631
+ })()
4632
+ ];
4633
+ 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;
4634
+ return sealCredential({
4635
+ apiKey: winner.value,
4636
+ tier: "disk",
4637
+ source: winner.path,
4638
+ deliberate: false,
4639
+ deprecated: false,
4640
+ diskCandidates: diskPaths,
4641
+ warning
4642
+ });
4643
+ }
4644
+ const legacy = firstEnvValue(env, apiKeyKeys);
4645
+ if (legacy) {
4646
+ assertUsableCredential(name, legacy.key, legacy.value);
4647
+ 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.`;
4648
+ 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}`;
4649
+ const sink = options.onDeprecation ?? defaultDeprecationSink;
4650
+ const notified = deprecationNotified();
4651
+ if (!notified.has(name)) {
4652
+ notified.add(name);
4653
+ sink(message);
4654
+ }
4655
+ return sealCredential({
4656
+ apiKey: legacy.value,
4657
+ tier: "legacy-env",
4658
+ source: legacy.key,
4659
+ deliberate: false,
4660
+ deprecated: true,
4661
+ diskCandidates: diskPaths,
4662
+ warning: message
4663
+ });
4664
+ }
4665
+ return null;
4666
+ }
4667
+ 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;
4668
+ var init_transport = __esm(() => {
4669
+ CredentialResolutionError = class CredentialResolutionError extends Error {
4670
+ appName;
4671
+ attempted;
4672
+ constructor(appName, message, attempted) {
4673
+ super(message);
4674
+ this.name = "CredentialResolutionError";
4675
+ this.appName = appName;
4676
+ this.attempted = attempted;
4677
+ }
4678
+ };
4679
+ MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
4680
+ SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
4681
+ SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
4682
+ ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
4683
+ INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
4684
+ CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
4685
+ DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
4686
+ IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
4687
+ AUTHORITY_OVERRIDE_HEADERS = new Set([
4688
+ "host",
4689
+ ":authority",
4690
+ "forwarded",
4691
+ "x-forwarded-host",
4692
+ "x-original-host"
4693
+ ]);
4694
+ });
4695
+
4696
+ // src/config.ts
4697
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
4698
+ import { join as join4 } from "path";
4425
4699
  import { homedir as homedir3 } from "os";
4426
4700
  function getHooksDataDir() {
4427
4701
  const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
4428
4702
  if (explicit)
4429
4703
  return explicit;
4430
- return join3(homedir3(), ".hasna", "hooks");
4704
+ return join4(homedir3(), ".hasna", "hooks");
4431
4705
  }
4432
4706
  function getCustomHooksDir() {
4433
- return join3(getHooksDataDir(), "hooks");
4707
+ return join4(getHooksDataDir(), "hooks");
4434
4708
  }
4435
4709
  function getLockPath() {
4436
4710
  const explicit = process.env.HASNA_HOOKS_LOCK_PATH ?? process.env.HOOKS_LOCK_PATH;
4437
4711
  if (explicit)
4438
4712
  return explicit;
4439
- return join3(getHooksDataDir(), "hooks.lock");
4713
+ return join4(getHooksDataDir(), "hooks.lock");
4440
4714
  }
4441
4715
  function getConfigPath() {
4442
4716
  const explicit = process.env.HASNA_HOOKS_CONFIG_PATH ?? process.env.HOOKS_CONFIG_PATH;
4443
4717
  if (explicit)
4444
4718
  return explicit;
4445
- return join3(getHooksDataDir(), "config.json");
4719
+ return join4(getHooksDataDir(), "config.json");
4446
4720
  }
4447
4721
  function readConfig() {
4448
4722
  try {
4449
4723
  const path = getConfigPath();
4450
4724
  if (!existsSync3(path))
4451
4725
  return {};
4452
- return JSON.parse(readFileSync2(path, "utf-8"));
4726
+ return JSON.parse(readFileSync3(path, "utf-8"));
4453
4727
  } catch {
4454
4728
  return {};
4455
4729
  }
4456
4730
  }
4457
4731
  function writeConfig(config) {
4458
4732
  const path = getConfigPath();
4459
- mkdirSync2(join3(path, ".."), { recursive: true });
4733
+ mkdirSync2(join4(path, ".."), { recursive: true });
4460
4734
  writeFileSync(path, JSON.stringify(config, null, 2) + `
4461
4735
  `, "utf-8");
4462
4736
  return path;
@@ -4469,36 +4743,38 @@ function resolveApiUrl() {
4469
4743
  return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
4470
4744
  }
4471
4745
  function resolveApiKey() {
4472
- return process.env.HASNA_HOOKS_API_KEY ?? process.env.HOOKS_API_KEY;
4746
+ return resolveCredential("hooks", process.env)?.apiKey;
4473
4747
  }
4474
- var init_config = () => {};
4748
+ var init_config = __esm(() => {
4749
+ init_transport();
4750
+ });
4475
4751
 
4476
4752
  // src/lib/semver.ts
4477
- var SEMVER_PATTERN;
4753
+ var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
4478
4754
  var init_semver = __esm(() => {
4479
- SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
4755
+ SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
4480
4756
  });
4481
4757
 
4482
4758
  // src/lib/manifest.ts
4483
4759
  var exports_manifest = {};
4484
4760
  __export(exports_manifest, {
4485
- writeCustomHook: () => writeCustomHook,
4486
- shortManifestName: () => shortManifestName,
4487
- scriptRelFor: () => scriptRelFor,
4488
- resolveScript: () => resolveScript,
4489
- readCustomManifest: () => readCustomManifest,
4490
- parseManifest: () => parseManifest,
4491
- normalizeManifestName: () => normalizeManifestName,
4492
- manifestSchema: () => manifestSchema,
4493
- listCustomHooks: () => listCustomHooks,
4494
- isInlineScript: () => isInlineScript,
4495
- customHookDir: () => customHookDir,
4496
- assertContained: () => assertContained,
4761
+ HOOK_NAME_RE: () => HOOK_NAME_RE,
4497
4762
  ScriptContainmentError: () => ScriptContainmentError,
4498
- HOOK_NAME_RE: () => HOOK_NAME_RE
4763
+ assertContained: () => assertContained,
4764
+ customHookDir: () => customHookDir,
4765
+ isInlineScript: () => isInlineScript,
4766
+ listCustomHooks: () => listCustomHooks,
4767
+ manifestSchema: () => manifestSchema,
4768
+ normalizeManifestName: () => normalizeManifestName,
4769
+ parseManifest: () => parseManifest,
4770
+ readCustomManifest: () => readCustomManifest,
4771
+ resolveScript: () => resolveScript,
4772
+ scriptRelFor: () => scriptRelFor,
4773
+ shortManifestName: () => shortManifestName,
4774
+ writeCustomHook: () => writeCustomHook
4499
4775
  });
4500
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync, writeFileSync as writeFileSync2 } from "fs";
4501
- import { basename, dirname, isAbsolute, join as join4, normalize, resolve, sep } from "path";
4776
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync, writeFileSync as writeFileSync2 } from "fs";
4777
+ import { basename, dirname, isAbsolute, join as join5, normalize, resolve, sep } from "path";
4502
4778
  function parseManifest(json) {
4503
4779
  const parsed = JSON.parse(json);
4504
4780
  return manifestSchema.parse(parsed);
@@ -4530,7 +4806,7 @@ function realpathNearest(path) {
4530
4806
  tail.unshift(basename(cur));
4531
4807
  cur = parent;
4532
4808
  }
4533
- return join4(realpathSync(cur), ...tail);
4809
+ return join5(realpathSync(cur), ...tail);
4534
4810
  }
4535
4811
  function assertContainedReal(root, target, raw) {
4536
4812
  const rootReal = realpathSync(root);
@@ -4563,25 +4839,25 @@ function resolveScript(manifest, manifestDir) {
4563
4839
  if (isInlineScript(manifest))
4564
4840
  return { path: scriptRelFor(manifest), content: manifest.script };
4565
4841
  const rel = scriptRelFor(manifest);
4566
- const resolved = assertContained(join4(manifestDir, rel), manifestDir, manifest.script);
4842
+ const resolved = assertContained(join5(manifestDir, rel), manifestDir, manifest.script);
4567
4843
  if (!existsSync4(resolved)) {
4568
4844
  throw new Error(`manifest script file not found: ${resolved}`);
4569
4845
  }
4570
- return { path: rel, content: readFileSync3(resolved, "utf-8") };
4846
+ return { path: rel, content: readFileSync4(resolved, "utf-8") };
4571
4847
  }
4572
4848
  function customHookDir(name) {
4573
- return join4(getCustomHooksDir(), shortManifestName(name));
4849
+ return join5(getCustomHooksDir(), shortManifestName(name));
4574
4850
  }
4575
4851
  function readCustomManifest(name) {
4576
4852
  const dir = customHookDir(name);
4577
- const manifestPath = join4(dir, "manifest.json");
4853
+ const manifestPath = join5(dir, "manifest.json");
4578
4854
  if (!existsSync4(manifestPath))
4579
4855
  return;
4580
4856
  try {
4581
- const manifest = parseManifest(readFileSync3(manifestPath, "utf-8"));
4857
+ const manifest = parseManifest(readFileSync4(manifestPath, "utf-8"));
4582
4858
  const script = resolveScript(manifest, dir);
4583
4859
  const scriptIsInline = isInlineScript(manifest);
4584
- return { manifest, scriptPath: join4(dir, script.path), scriptContent: script.content, scriptIsInline };
4860
+ return { manifest, scriptPath: join5(dir, script.path), scriptContent: script.content, scriptIsInline };
4585
4861
  } catch (err) {
4586
4862
  if (err instanceof ScriptContainmentError)
4587
4863
  throw err;
@@ -4605,7 +4881,7 @@ function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
4605
4881
  if (isAbsolute(scriptRelPath)) {
4606
4882
  throw new ScriptContainmentError(scriptRelPath, dir);
4607
4883
  }
4608
- const scriptPath = scriptRelPath.includes("/") ? join4(dir, scriptRelPath) : join4(dir, scriptRelPath);
4884
+ const scriptPath = scriptRelPath.includes("/") ? join5(dir, scriptRelPath) : join5(dir, scriptRelPath);
4609
4885
  assertContainedLexical(scriptPath, dir, scriptRelPath);
4610
4886
  mkdirSync3(dir, { recursive: true });
4611
4887
  assertContainedReal(dir, scriptPath, scriptRelPath);
@@ -4613,7 +4889,7 @@ function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
4613
4889
  if (scriptDir !== dir)
4614
4890
  mkdirSync3(scriptDir, { recursive: true });
4615
4891
  writeFileSync2(scriptPath, scriptContent, "utf-8");
4616
- writeFileSync2(join4(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
4892
+ writeFileSync2(join5(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
4617
4893
  `, "utf-8");
4618
4894
  return { dir, scriptPath };
4619
4895
  }
@@ -4638,7 +4914,8 @@ var init_manifest = __esm(() => {
4638
4914
  script: exports_external.string().min(1, "script must name a relative path or inline content"),
4639
4915
  script_kind: exports_external.enum(["inline", "file"]).optional(),
4640
4916
  args: exports_external.array(exports_external.string()).optional(),
4641
- timeout_ms: exports_external.number().int().positive().optional()
4917
+ timeout_ms: exports_external.number().int().positive().optional(),
4918
+ env: exports_external.record(exports_external.string(), exports_external.string()).optional()
4642
4919
  });
4643
4920
  ScriptContainmentError = class ScriptContainmentError extends Error {
4644
4921
  constructor(path, hookDir, raw) {
@@ -4651,26 +4928,26 @@ var init_manifest = __esm(() => {
4651
4928
  // src/lib/store.ts
4652
4929
  var exports_store = {};
4653
4930
  __export(exports_store, {
4654
- writeLock: () => writeLock,
4655
- verifyScriptHash: () => verifyScriptHash,
4656
- upsertHookRecord: () => upsertHookRecord,
4657
- sha256Of: () => sha256Of,
4658
- sha256File: () => sha256File,
4659
- setPinnedHook: () => setPinnedHook,
4660
- retrustHook: () => retrustHook,
4661
- removePinnedHook: () => removePinnedHook,
4662
- removeHookRecord: () => removeHookRecord,
4663
- removeHookFromStore: () => removeHookFromStore,
4664
- readLock: () => readLock,
4665
- pinInstalledHook: () => pinInstalledHook,
4666
- listHookRecords: () => listHookRecords,
4667
- getPinnedHook: () => getPinnedHook,
4668
- getHookRecord: () => getHookRecord,
4931
+ LockFileError: () => LockFileError,
4669
4932
  checkScriptHash: () => checkScriptHash,
4670
- LockFileError: () => LockFileError
4933
+ getHookRecord: () => getHookRecord,
4934
+ getPinnedHook: () => getPinnedHook,
4935
+ listHookRecords: () => listHookRecords,
4936
+ pinInstalledHook: () => pinInstalledHook,
4937
+ readLock: () => readLock,
4938
+ removeHookFromStore: () => removeHookFromStore,
4939
+ removeHookRecord: () => removeHookRecord,
4940
+ removePinnedHook: () => removePinnedHook,
4941
+ retrustHook: () => retrustHook,
4942
+ setPinnedHook: () => setPinnedHook,
4943
+ sha256File: () => sha256File,
4944
+ sha256Of: () => sha256Of,
4945
+ upsertHookRecord: () => upsertHookRecord,
4946
+ verifyScriptHash: () => verifyScriptHash,
4947
+ writeLock: () => writeLock
4671
4948
  });
4672
4949
  import { createHash } from "crypto";
4673
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync } from "fs";
4950
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync } from "fs";
4674
4951
  import { readFile } from "fs/promises";
4675
4952
  import { randomBytes } from "crypto";
4676
4953
  function sha256Of(input) {
@@ -4736,7 +5013,7 @@ function readLock() {
4736
5013
  return { hooks: {} };
4737
5014
  let raw;
4738
5015
  try {
4739
- raw = readFileSync4(path, "utf-8");
5016
+ raw = readFileSync5(path, "utf-8");
4740
5017
  } catch {
4741
5018
  throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
4742
5019
  }
@@ -4833,7 +5110,7 @@ async function verifyScriptHash(name, scriptPath) {
4833
5110
  return checkScriptHash(name, await sha256File(scriptPath));
4834
5111
  }
4835
5112
  function retrustHook(name, scriptPath, version, source) {
4836
- const actual = sha256Of(readFileSync4(scriptPath));
5113
+ const actual = sha256Of(readFileSync5(scriptPath));
4837
5114
  const db = getDb();
4838
5115
  upsertHookRecord(db, {
4839
5116
  name,
@@ -4860,24 +5137,24 @@ var init_store = __esm(() => {
4860
5137
  // src/lib/profiles.ts
4861
5138
  var exports_profiles = {};
4862
5139
  __export(exports_profiles, {
4863
- updateProfile: () => updateProfile,
4864
- touchProfile: () => touchProfile,
4865
- listProfiles: () => listProfiles,
4866
- importProfiles: () => importProfiles,
4867
- getProfilesDir: () => getProfilesDir,
4868
- getProfile: () => getProfile,
4869
- exportProfiles: () => exportProfiles,
5140
+ createProfile: () => createProfile,
4870
5141
  deleteProfile: () => deleteProfile,
4871
- createProfile: () => createProfile
5142
+ exportProfiles: () => exportProfiles,
5143
+ getProfile: () => getProfile,
5144
+ getProfilesDir: () => getProfilesDir,
5145
+ importProfiles: () => importProfiles,
5146
+ listProfiles: () => listProfiles,
5147
+ touchProfile: () => touchProfile,
5148
+ updateProfile: () => updateProfile
4872
5149
  });
4873
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
4874
- import { join as join7 } from "path";
5150
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
5151
+ import { join as join8 } from "path";
4875
5152
  import { homedir as homedir5 } from "os";
4876
5153
  function resolveProfilesDir() {
4877
- const newDir = join7(homedir5(), ".hasna", "hooks", "profiles");
4878
- const oldDir = join7(homedir5(), ".hooks", "profiles");
5154
+ const newDir = join8(homedir5(), ".hasna", "hooks", "profiles");
5155
+ const oldDir = join8(homedir5(), ".hooks", "profiles");
4879
5156
  if (!existsSync8(newDir) && existsSync8(oldDir)) {
4880
- mkdirSync6(join7(homedir5(), ".hasna", "hooks"), { recursive: true });
5157
+ mkdirSync6(join8(homedir5(), ".hasna", "hooks"), { recursive: true });
4881
5158
  cpSync2(oldDir, newDir, { recursive: true });
4882
5159
  }
4883
5160
  return newDir;
@@ -4888,7 +5165,7 @@ function ensureProfilesDir() {
4888
5165
  }
4889
5166
  }
4890
5167
  function profilePath(id) {
4891
- return join7(PROFILES_DIR, `${id}.json`);
5168
+ return join8(PROFILES_DIR, `${id}.json`);
4892
5169
  }
4893
5170
  function shortUuid() {
4894
5171
  return crypto.randomUUID().slice(0, 8);
@@ -4919,7 +5196,7 @@ function getProfile(id) {
4919
5196
  try {
4920
5197
  if (!existsSync8(path))
4921
5198
  return null;
4922
- return JSON.parse(readFileSync6(path, "utf-8"));
5199
+ return JSON.parse(readFileSync7(path, "utf-8"));
4923
5200
  } catch {
4924
5201
  return null;
4925
5202
  }
@@ -4932,7 +5209,7 @@ function listProfiles() {
4932
5209
  const profiles = [];
4933
5210
  for (const file of files) {
4934
5211
  try {
4935
- const content = readFileSync6(join7(PROFILES_DIR, file), "utf-8");
5212
+ const content = readFileSync7(join8(PROFILES_DIR, file), "utf-8");
4936
5213
  profiles.push(JSON.parse(content));
4937
5214
  } catch {}
4938
5215
  }
@@ -5001,6 +5278,8 @@ var init_profiles = __esm(() => {
5001
5278
  });
5002
5279
 
5003
5280
  // src/lib/hook-env.ts
5281
+ import { dirname as dirname5, isAbsolute as isAbsolute2 } from "path";
5282
+ import { existsSync as existsSync10, statSync as statSync2 } from "fs";
5004
5283
  function isDeniedEnvName(name) {
5005
5284
  const upper = name.toUpperCase();
5006
5285
  if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
@@ -5009,13 +5288,74 @@ function isDeniedEnvName(name) {
5009
5288
  return true;
5010
5289
  return DENY_CONTAINS.some((token) => upper.includes(token));
5011
5290
  }
5291
+ function isInterpreterInjectionEnvName(name) {
5292
+ if (INTERPRETER_INJECTION.has(name))
5293
+ return true;
5294
+ return INTERPRETER_INJECTION_PREFIXES.some((prefix) => name.startsWith(prefix));
5295
+ }
5296
+ function isBashFunctionEnvName(name) {
5297
+ return name.startsWith("BASH_FUNC_");
5298
+ }
5299
+ function isUnsafePathEntry(entry, home) {
5300
+ if (entry === "" || !isAbsolute2(entry))
5301
+ return true;
5302
+ if (home && (entry === home || entry.startsWith(home + "/")))
5303
+ return true;
5304
+ if (entry === "/tmp" || entry.startsWith("/tmp/"))
5305
+ return true;
5306
+ if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
5307
+ return true;
5308
+ try {
5309
+ if ((statSync2(entry).mode & 2) !== 0)
5310
+ return true;
5311
+ } catch {}
5312
+ return false;
5313
+ }
5314
+ function sanitizeHookPath(pathValue, home) {
5315
+ const baseline = PATH_BASELINE.slice();
5316
+ if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
5317
+ baseline.push("/opt/homebrew/bin");
5318
+ const bunDir = dirname5(process.execPath);
5319
+ const seen = new Set;
5320
+ const out = [];
5321
+ for (const dir of baseline) {
5322
+ if (seen.has(dir))
5323
+ continue;
5324
+ seen.add(dir);
5325
+ out.push(dir);
5326
+ }
5327
+ if (!seen.has(bunDir)) {
5328
+ seen.add(bunDir);
5329
+ out.push(bunDir);
5330
+ }
5331
+ for (const entry of pathValue.split(":")) {
5332
+ if (seen.has(entry))
5333
+ continue;
5334
+ if (isUnsafePathEntry(entry, home))
5335
+ continue;
5336
+ seen.add(entry);
5337
+ out.push(entry);
5338
+ }
5339
+ return out.join(":");
5340
+ }
5012
5341
  function buildHookEnv(source, extra) {
5013
5342
  const out = {};
5014
5343
  for (const name of ALLOWLIST) {
5344
+ if (name === "PATH")
5345
+ continue;
5015
5346
  const value = source[name] ?? extra?.[name];
5016
5347
  if (value !== undefined && value !== null)
5017
5348
  out[name] = value;
5018
5349
  }
5350
+ const pathOverride = extra?.PATH;
5351
+ if (pathOverride !== undefined && pathOverride !== null) {
5352
+ out.PATH = pathOverride;
5353
+ } else {
5354
+ const parentPath = source.PATH;
5355
+ if (parentPath !== undefined && parentPath !== null) {
5356
+ out.PATH = sanitizeHookPath(parentPath, source.HOME ?? extra?.HOME);
5357
+ }
5358
+ }
5019
5359
  for (const [from, to] of CONFIG_PROJECTIONS) {
5020
5360
  const value = source[from] ?? extra?.[from];
5021
5361
  if (value !== undefined && value !== null)
@@ -5027,14 +5367,44 @@ function buildHookEnv(source, extra) {
5027
5367
  continue;
5028
5368
  if (isDeniedEnvName(name))
5029
5369
  continue;
5370
+ if (isInterpreterInjectionEnvName(name))
5371
+ continue;
5372
+ if (isBashFunctionEnvName(name))
5373
+ continue;
5030
5374
  if (value !== undefined && value !== null)
5031
5375
  out[name] = value;
5032
5376
  }
5033
5377
  return out;
5034
5378
  }
5035
- var ALLOWLIST, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, CONFIG_PROJECTIONS;
5379
+ var ALLOWLIST, INTERPRETER_INJECTION, INTERPRETER_INJECTION_PREFIXES, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, PATH_BASELINE, CONFIG_PROJECTIONS;
5036
5380
  var init_hook_env = __esm(() => {
5037
5381
  ALLOWLIST = new Set(["PATH", "HOME", "LANG", "TZ", "SHELL", "TERM", "USER", "PWD"]);
5382
+ INTERPRETER_INJECTION = new Set([
5383
+ "BASH_ENV",
5384
+ "ENV",
5385
+ "BASHOPTS",
5386
+ "SHELLOPTS",
5387
+ "NODE_OPTIONS",
5388
+ "NODE_PATH",
5389
+ "PYTHONSTARTUP",
5390
+ "PYTHONINSPECT",
5391
+ "PYTHONPATH",
5392
+ "PYTHONHOME",
5393
+ "LD_PRELOAD",
5394
+ "LD_LIBRARY_PATH",
5395
+ "GCONV_PATH",
5396
+ "LOCPATH",
5397
+ "PERL5OPT",
5398
+ "RUBYOPT",
5399
+ "NODE_EXTRA_CA_CERTS",
5400
+ "NODE_TLS_REJECT_UNAUTHORIZED",
5401
+ "SSL_CERT_FILE",
5402
+ "SSL_CERT_DIR",
5403
+ "CURL_CA_BUNDLE",
5404
+ "GIT_SSL_CAINFO",
5405
+ "AWS_CA_BUNDLE"
5406
+ ]);
5407
+ INTERPRETER_INJECTION_PREFIXES = ["GIT_CONFIG_"];
5038
5408
  DENY_SUFFIXES = ["KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "CREDENTIALS", "URL", "URI"];
5039
5409
  DENY_PREFIXES = [
5040
5410
  "HASNA_",
@@ -5052,6 +5422,7 @@ var init_hook_env = __esm(() => {
5052
5422
  "MEMENTOS_"
5053
5423
  ];
5054
5424
  DENY_CONTAINS = ["DATABASE_URL"];
5425
+ PATH_BASELINE = ["/usr/bin", "/bin", "/usr/local/bin", "/sbin", "/usr/sbin"];
5055
5426
  CONFIG_PROJECTIONS = [
5056
5427
  ["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
5057
5428
  ["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
@@ -5063,12 +5434,12 @@ var init_hook_env = __esm(() => {
5063
5434
  // src/lib/run.ts
5064
5435
  var exports_run = {};
5065
5436
  __export(exports_run, {
5066
- executeVerifiedScript: () => executeVerifiedScript,
5067
- HookTimeoutError: () => HookTimeoutError
5437
+ HookTimeoutError: () => HookTimeoutError,
5438
+ executeVerifiedScript: () => executeVerifiedScript
5068
5439
  });
5069
5440
  import { randomBytes as randomBytes2 } from "crypto";
5070
5441
  import { rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
5071
- import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
5442
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join11 } from "path";
5072
5443
  function extensionOf(scriptPath) {
5073
5444
  const base = basename2(scriptPath);
5074
5445
  const dot = base.lastIndexOf(".");
@@ -5095,13 +5466,13 @@ function interpreterFor(name, scriptPath, content) {
5095
5466
  if (interp === "bash" || interp === "sh")
5096
5467
  return { command: ["/bin/bash"], tempExt: "sh" };
5097
5468
  if (interp === "node" || interp === "bun")
5098
- return { command: ["bun", "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
5469
+ return { command: [process.execPath, "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
5099
5470
  throw new Error(`Refusing to run hook '${name}': shebang '${firstLine}' is not a recognized interpreter (supported: bash/sh, node/bun)`);
5100
5471
  }
5101
5472
  if (BASH_EXTENSIONS.has(ext))
5102
5473
  return { command: ["/bin/bash"], tempExt: "sh" };
5103
5474
  if (BUN_EXTENSIONS.has(ext))
5104
- return { command: ["bun", "run"], tempExt: ext };
5475
+ return { command: [process.execPath, "run"], tempExt: ext };
5105
5476
  throw new Error(`Refusing to run hook '${name}': unsupported script extension '.${ext}' (supported: .sh .bash .ts .tsx .js .jsx .mjs .cjs .mts .cts, or a recognized shebang)`);
5106
5477
  }
5107
5478
  async function readPipeWithDeadline(stream, deadlineMs) {
@@ -5137,9 +5508,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
5137
5508
  return chunks.join("");
5138
5509
  }
5139
5510
  async function executeVerifiedScript(options) {
5140
- const scriptDir = dirname4(options.scriptPath);
5511
+ const scriptDir = dirname6(options.scriptPath);
5141
5512
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
5142
- const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
5513
+ const tempPath = join11(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
5143
5514
  let proc = null;
5144
5515
  let timedOut = false;
5145
5516
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -5148,7 +5519,7 @@ async function executeVerifiedScript(options) {
5148
5519
  }, options.timeout) : null;
5149
5520
  try {
5150
5521
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
5151
- proc = Bun.spawn([...interpreter.command, isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
5522
+ proc = Bun.spawn([...interpreter.command, isAbsolute3(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
5152
5523
  stdin: new Response(options.stdin),
5153
5524
  stdout: "pipe",
5154
5525
  stderr: "pipe",
@@ -5226,10 +5597,10 @@ var init_run = __esm(() => {
5226
5597
  // src/lib/db-writer.ts
5227
5598
  var exports_db_writer = {};
5228
5599
  __export(exports_db_writer, {
5229
- writeHookEvent: () => writeHookEvent,
5230
- resolveEventType: () => resolveEventType,
5600
+ normalizeEventType: () => normalizeEventType,
5231
5601
  recordHookRun: () => recordHookRun,
5232
- normalizeEventType: () => normalizeEventType
5602
+ resolveEventType: () => resolveEventType,
5603
+ writeHookEvent: () => writeHookEvent
5233
5604
  });
5234
5605
  function nanoid2() {
5235
5606
  return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
@@ -6128,6 +6499,16 @@ var HOOKS = [
6128
6499
  matcher: "",
6129
6500
  tags: ["errors", "failures", "logging", "debugging"]
6130
6501
  },
6502
+ {
6503
+ name: "spiral-detector",
6504
+ displayName: "Spiral Detector",
6505
+ description: "Interrupts sessions after five consecutive identical command failures",
6506
+ version: "0.1.0",
6507
+ category: "Observability",
6508
+ event: "PostToolUse",
6509
+ matcher: "",
6510
+ tags: ["spiral", "failures", "retries", "safety", "interrupt"]
6511
+ },
6131
6512
  {
6132
6513
  name: "stylescheck",
6133
6514
  displayName: "Styles Check",
@@ -6293,21 +6674,21 @@ function getHook(name) {
6293
6674
  return HOOKS.find((h) => h.name === name);
6294
6675
  }
6295
6676
  // src/lib/installer.ts
6296
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
6297
- import { join as join6, dirname as dirname3 } from "path";
6677
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
6678
+ import { join as join7, dirname as dirname3 } from "path";
6298
6679
  import { homedir as homedir4 } from "os";
6299
6680
  import { fileURLToPath as fileURLToPath2 } from "url";
6300
6681
 
6301
6682
  // src/lib/resolve.ts
6302
- import { dirname as dirname2, join as join5 } from "path";
6683
+ import { dirname as dirname2, join as join6 } from "path";
6303
6684
  import { existsSync as existsSync5 } from "fs";
6304
6685
  import { fileURLToPath } from "url";
6305
6686
  init_manifest();
6306
6687
  function resolveBundledHooksDir(moduleDir = dirname2(fileURLToPath(import.meta.url))) {
6307
- const sourceLayout = join5(moduleDir, "..", "..", "hooks");
6308
- if (existsSync5(join5(sourceLayout, "hook-gitguard")))
6688
+ const sourceLayout = join6(moduleDir, "..", "..", "hooks");
6689
+ if (existsSync5(join6(sourceLayout, "hook-gitguard")))
6309
6690
  return sourceLayout;
6310
- return join5(moduleDir, "..", "hooks");
6691
+ return join6(moduleDir, "..", "hooks");
6311
6692
  }
6312
6693
  function resolveHookDir(name) {
6313
6694
  const custom2 = readCustomManifest(name);
@@ -6318,10 +6699,10 @@ function resolveHookDir(name) {
6318
6699
  return;
6319
6700
  const short = shortManifestName(name);
6320
6701
  const bundledBase = resolveBundledHooksDir();
6321
- const direct = join5(bundledBase, short);
6702
+ const direct = join6(bundledBase, short);
6322
6703
  if (existsSync5(direct))
6323
6704
  return direct;
6324
- const prefixed = join5(bundledBase, `hook-${short}`);
6705
+ const prefixed = join6(bundledBase, `hook-${short}`);
6325
6706
  return existsSync5(prefixed) ? prefixed : undefined;
6326
6707
  }
6327
6708
  function resolveScriptPath(name) {
@@ -6331,7 +6712,7 @@ function resolveScriptPath(name) {
6331
6712
  const dir = resolveHookDir(name);
6332
6713
  if (!dir)
6333
6714
  return;
6334
- const script = join5(dir, "src", "hook.ts");
6715
+ const script = join6(dir, "src", "hook.ts");
6335
6716
  return existsSync5(script) ? script : undefined;
6336
6717
  }
6337
6718
  function resolveHookMeta(name) {
@@ -6387,7 +6768,7 @@ function resolveHook(name) {
6387
6768
  init_manifest();
6388
6769
  init_store();
6389
6770
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
6390
- var HOOKS_DIR = existsSync7(join6(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join6(__dirname2, "..", "..", "hooks") : join6(__dirname2, "..", "hooks");
6771
+ var HOOKS_DIR = existsSync7(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
6391
6772
  function normalizeHookName(name) {
6392
6773
  return name.startsWith("hook-") ? name : `hook-${name}`;
6393
6774
  }
@@ -6457,16 +6838,16 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
6457
6838
  return globalOverride;
6458
6839
  const dir = getTargetSettingsDir(target);
6459
6840
  if (scope === "project") {
6460
- return target === "codewith" ? join6(process.cwd(), dir, "config.toml") : join6(process.cwd(), dir, "settings.json");
6841
+ return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
6461
6842
  }
6462
- return target === "codewith" ? join6(homedir4(), dir, "config.toml") : join6(homedir4(), dir, "settings.json");
6843
+ return target === "codewith" ? join7(homedir4(), dir, "config.toml") : join7(homedir4(), dir, "settings.json");
6463
6844
  }
6464
6845
  function getHookPath(name) {
6465
6846
  const shortName = shortHookName(name);
6466
6847
  const resolved = resolveHookDir(shortName);
6467
6848
  if (resolved)
6468
6849
  return resolved;
6469
- return join6(HOOKS_DIR, normalizeHookName(shortName));
6850
+ return join7(HOOKS_DIR, normalizeHookName(shortName));
6470
6851
  }
6471
6852
  function hookExists(name) {
6472
6853
  return existsSync7(getHookPath(name));
@@ -6475,7 +6856,7 @@ function readSettings(scope = "global", target = "claude") {
6475
6856
  const path = getSettingsPath(scope, target);
6476
6857
  try {
6477
6858
  if (existsSync7(path)) {
6478
- return JSON.parse(readFileSync5(path, "utf-8"));
6859
+ return JSON.parse(readFileSync6(path, "utf-8"));
6479
6860
  }
6480
6861
  } catch (error) {
6481
6862
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -6574,7 +6955,7 @@ function buildCodewithTomlFragment(name, profile) {
6574
6955
  function readCodewithConfig(scope, configPath) {
6575
6956
  const path = getSettingsPath(scope, "codewith", configPath);
6576
6957
  try {
6577
- return existsSync7(path) ? readFileSync5(path, "utf-8") : "";
6958
+ return existsSync7(path) ? readFileSync6(path, "utf-8") : "";
6578
6959
  } catch {
6579
6960
  return "";
6580
6961
  }
@@ -6584,7 +6965,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
6584
6965
  const dir = dirname3(path);
6585
6966
  if (!existsSync7(dir))
6586
6967
  mkdirSync5(dir, { recursive: true });
6587
- const existing = existsSync7(path) ? readFileSync5(path, "utf-8") : "";
6968
+ const existing = existsSync7(path) ? readFileSync6(path, "utf-8") : "";
6588
6969
  const sep2 = existing.trim() ? `
6589
6970
 
6590
6971
  ` : "";
@@ -6629,7 +7010,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
6629
7010
  target,
6630
7011
  fragment,
6631
7012
  applied: false,
6632
- note: "Codewith install is fragment-only by default; open-configs should own applying this TOML."
7013
+ note: "Codewith install is fragment-only by default; configs should own applying this TOML."
6633
7014
  };
6634
7015
  }
6635
7016
  if (!codewithConfigPath) {
@@ -6656,7 +7037,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
6656
7037
  fragment,
6657
7038
  applied: true,
6658
7039
  configPath: path,
6659
- note: "Direct Codewith config write was explicitly requested; prefer open-configs for managed machines."
7040
+ note: "Direct Codewith config write was explicitly requested; prefer configs for managed machines."
6660
7041
  };
6661
7042
  } catch (error) {
6662
7043
  return {
@@ -6888,7 +7269,7 @@ function codewithHasHookEntry(name, scope = "global") {
6888
7269
  if (!existsSync7(path))
6889
7270
  return false;
6890
7271
  try {
6891
- return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
7272
+ return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
6892
7273
  } catch {
6893
7274
  return false;
6894
7275
  }
@@ -6920,7 +7301,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
6920
7301
  if (target === "codewith" || target === "all") {
6921
7302
  const codewithPath = getSettingsPath(scope, "codewith");
6922
7303
  if (existsSync7(codewithPath)) {
6923
- const before = readFileSync5(codewithPath, "utf-8");
7304
+ const before = readFileSync6(codewithPath, "utf-8");
6924
7305
  const after = removeCodewithHookEntry(before, shortName);
6925
7306
  if (after.removed) {
6926
7307
  writeFileSync4(codewithPath, after.text, "utf-8");
@@ -6964,20 +7345,20 @@ init_profiles();
6964
7345
  init_config();
6965
7346
  init_store();
6966
7347
  init_manifest();
6967
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
7348
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
6968
7349
 
6969
7350
  // src/lib/custom-install.ts
6970
7351
  init_config();
6971
7352
  init_manifest();
6972
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
6973
- import { join as join8 } from "path";
7353
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
7354
+ import { join as join9 } from "path";
6974
7355
  import { tmpdir } from "os";
6975
7356
  function manifestAt(dir) {
6976
- const path = join8(dir, "manifest.json");
7357
+ const path = join9(dir, "manifest.json");
6977
7358
  if (!existsSync9(path))
6978
7359
  return;
6979
7360
  try {
6980
- return parseManifest(readFileSync7(path, "utf-8"));
7361
+ return parseManifest(readFileSync8(path, "utf-8"));
6981
7362
  } catch {
6982
7363
  return;
6983
7364
  }
@@ -6996,8 +7377,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
6996
7377
  return { name, version: manifest.version, kind, source, dir, scriptPath };
6997
7378
  }
6998
7379
  async function cloneToTemp(gitUrl) {
6999
- const tmp = mkdtempSync(join8(tmpdir(), "hooks-install-"));
7000
- const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join8(tmp, "repo")], {
7380
+ const tmp = mkdtempSync(join9(tmpdir(), "hooks-install-"));
7381
+ const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join9(tmp, "repo")], {
7001
7382
  stdout: "pipe",
7002
7383
  stderr: "pipe"
7003
7384
  });
@@ -7010,7 +7391,7 @@ async function cloneToTemp(gitUrl) {
7010
7391
  rmSync3(tmp, { recursive: true, force: true });
7011
7392
  throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
7012
7393
  }
7013
- return join8(tmp, "repo");
7394
+ return join9(tmp, "repo");
7014
7395
  }
7015
7396
  function isGitUrl(value) {
7016
7397
  return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
@@ -7038,7 +7419,7 @@ async function installCustomSource(source) {
7038
7419
  throw new Error(`No such local path: ${source}`);
7039
7420
  }
7040
7421
  if (source.endsWith("manifest.json")) {
7041
- const manifest = parseManifest(readFileSync7(source, "utf-8"));
7422
+ const manifest = parseManifest(readFileSync8(source, "utf-8"));
7042
7423
  return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
7043
7424
  }
7044
7425
  return copyManifestDir(source, source, "local");
@@ -7069,7 +7450,7 @@ async function installManifestUrl(url) {
7069
7450
  }
7070
7451
  // src/lib/sync.ts
7071
7452
  import { createHash as createHash2 } from "crypto";
7072
- import { readFileSync as readFileSync8 } from "fs";
7453
+ import { readFileSync as readFileSync9 } from "fs";
7073
7454
  init_config();
7074
7455
  init_db();
7075
7456
  init_store();
@@ -7078,7 +7459,7 @@ function sha256OfText(text) {
7078
7459
  return createHash2("sha256").update(text).digest("hex");
7079
7460
  }
7080
7461
  function sha256OfPath(path) {
7081
- return createHash2("sha256").update(readFileSync8(path)).digest("hex");
7462
+ return createHash2("sha256").update(readFileSync9(path)).digest("hex");
7082
7463
  }
7083
7464
  function collectBundledCatalog() {
7084
7465
  const out = [];
@@ -7333,6 +7714,9 @@ async function fetchPinnedHook(name, version, apiUrl) {
7333
7714
  init_manifest();
7334
7715
  init_store();
7335
7716
  init_config();
7717
+ import { readFileSync as readFileSync10 } from "fs";
7718
+ import { dirname as dirname4, join as join10 } from "path";
7719
+ import { fileURLToPath as fileURLToPath3 } from "url";
7336
7720
 
7337
7721
  // src/lib/secure-compare.ts
7338
7722
  function secureEqual(a, b) {
@@ -7347,11 +7731,170 @@ function secureEqual(a, b) {
7347
7731
  return diff === 0;
7348
7732
  }
7349
7733
 
7734
+ // src/openapi.ts
7735
+ var openApiDocument = {
7736
+ openapi: "3.1.0",
7737
+ info: {
7738
+ title: "Hooks Registry API",
7739
+ version: "0.1.0",
7740
+ 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)."
7741
+ },
7742
+ servers: [{ url: "/" }],
7743
+ paths: {
7744
+ "/health": {
7745
+ get: {
7746
+ summary: "Liveness probe",
7747
+ operationId: "getHealth",
7748
+ responses: {
7749
+ "200": {
7750
+ description: "Service is alive",
7751
+ content: {
7752
+ "application/json": {
7753
+ schema: { type: "object", additionalProperties: true }
7754
+ }
7755
+ }
7756
+ }
7757
+ }
7758
+ }
7759
+ },
7760
+ "/ready": {
7761
+ get: {
7762
+ summary: "Readiness probe \u2014 the hooks.lock store is readable",
7763
+ operationId: "getReady",
7764
+ responses: {
7765
+ "200": { description: "Lock store is readable" },
7766
+ "503": { description: "Lock store is not readable" }
7767
+ }
7768
+ }
7769
+ },
7770
+ "/version": {
7771
+ get: {
7772
+ summary: "Package version",
7773
+ operationId: "getVersion",
7774
+ responses: {
7775
+ "200": {
7776
+ description: "The installed @hasna/hooks version",
7777
+ content: {
7778
+ "application/json": {
7779
+ schema: { type: "object", properties: { version: { type: "string" } } }
7780
+ }
7781
+ }
7782
+ }
7783
+ }
7784
+ }
7785
+ },
7786
+ "/openapi.json": {
7787
+ get: {
7788
+ summary: "This OpenAPI document",
7789
+ operationId: "getOpenApi",
7790
+ responses: { "200": { description: "OpenAPI 3.1 document" } }
7791
+ }
7792
+ },
7793
+ "/api/v1/catalog": {
7794
+ get: {
7795
+ summary: "List the full hook catalog with versions and sha256",
7796
+ operationId: "getCatalog",
7797
+ responses: {
7798
+ "200": {
7799
+ description: "Catalog entries",
7800
+ content: {
7801
+ "application/json": {
7802
+ schema: {
7803
+ type: "object",
7804
+ properties: {
7805
+ hooks: {
7806
+ type: "array",
7807
+ items: {
7808
+ type: "object",
7809
+ properties: {
7810
+ name: { type: "string" },
7811
+ version: { type: "string" },
7812
+ sha256: { type: "string" },
7813
+ events: { type: "array", items: { type: "string" } }
7814
+ }
7815
+ }
7816
+ }
7817
+ }
7818
+ }
7819
+ }
7820
+ }
7821
+ }
7822
+ }
7823
+ }
7824
+ },
7825
+ "/api/v1/lock": {
7826
+ get: {
7827
+ summary: "Read the published hooks.lock with per-hook versions",
7828
+ operationId: "getLock",
7829
+ responses: { "200": { description: "The lock file" } }
7830
+ }
7831
+ },
7832
+ "/api/v1/hooks/{name}/{version}": {
7833
+ get: {
7834
+ summary: "Fetch one hook's manifest and script",
7835
+ operationId: "getHookArtifact",
7836
+ parameters: [
7837
+ { name: "name", in: "path", required: true, schema: { type: "string" } },
7838
+ { name: "version", in: "path", required: true, schema: { type: "string" } }
7839
+ ],
7840
+ responses: {
7841
+ "200": {
7842
+ description: "Hook artifact; x-hook-sha256 header carries the script hash"
7843
+ },
7844
+ "400": { description: "Invalid URL encoding or semver version" },
7845
+ "404": { description: "Hook not found locally" }
7846
+ }
7847
+ }
7848
+ },
7849
+ "/api/v1/hooks": {
7850
+ put: {
7851
+ summary: "Publish/retrust a hook (requires the API key)",
7852
+ operationId: "putHook",
7853
+ requestBody: {
7854
+ required: true,
7855
+ content: {
7856
+ "application/json": {
7857
+ schema: {
7858
+ type: "object",
7859
+ properties: { name: { type: "string" }, version: { type: "string" } }
7860
+ }
7861
+ }
7862
+ }
7863
+ },
7864
+ responses: {
7865
+ "200": { description: "Hook retrusted and pin updated" },
7866
+ "401": { description: "Missing or invalid API key" },
7867
+ "404": { description: "Hook not found in local store" },
7868
+ "409": { description: "Version mismatch with the local store" }
7869
+ },
7870
+ security: [{ apiKey: [] }]
7871
+ }
7872
+ }
7873
+ },
7874
+ components: {
7875
+ securitySchemes: {
7876
+ apiKey: {
7877
+ type: "http",
7878
+ scheme: "bearer",
7879
+ description: "The registry API key (HASNA_HOOKS_API_KEY / HOOKS_API_KEY)."
7880
+ }
7881
+ }
7882
+ }
7883
+ };
7884
+
7350
7885
  // src/serve.ts
7351
7886
  init_semver();
7352
7887
  var DEFAULT_SERVE_PORT = 39428;
7353
7888
  var SERVE_HOST = "127.0.0.1";
7354
7889
  var SERVE_SERVICE_NAME = "hooks-registry";
7890
+ function packageVersion() {
7891
+ try {
7892
+ const pkgPath = join10(dirname4(fileURLToPath3(import.meta.url)), "..", "package.json");
7893
+ return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
7894
+ } catch {
7895
+ return "0.0.0";
7896
+ }
7897
+ }
7355
7898
  async function buildCatalog() {
7356
7899
  const byName = new Map;
7357
7900
  for (const meta of HOOKS) {
@@ -7424,6 +7967,20 @@ function handleServeRequest(req, apiKey) {
7424
7967
  if (url.pathname === "/health" && req.method === "GET") {
7425
7968
  return Promise.resolve(json({ status: "ok", name: SERVE_SERVICE_NAME }));
7426
7969
  }
7970
+ if (url.pathname === "/ready" && req.method === "GET") {
7971
+ try {
7972
+ readLock();
7973
+ return Promise.resolve(json({ status: "ready", name: SERVE_SERVICE_NAME }));
7974
+ } catch {
7975
+ return Promise.resolve(json({ status: "not-ready", name: SERVE_SERVICE_NAME }, 503));
7976
+ }
7977
+ }
7978
+ if (url.pathname === "/version" && req.method === "GET") {
7979
+ return Promise.resolve(json({ version: packageVersion() }));
7980
+ }
7981
+ if (url.pathname === "/openapi.json" && req.method === "GET") {
7982
+ return Promise.resolve(json(openApiDocument));
7983
+ }
7427
7984
  if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
7428
7985
  return buildCatalog().then((catalog) => json({ hooks: catalog }));
7429
7986
  }
@@ -7594,11 +8151,11 @@ async function runHook(name, input, options = {}) {
7594
8151
  throw new Error(`Hook '${name}' not found`);
7595
8152
  const hookScript = getHookPath(name) + "/src/hook.ts";
7596
8153
  const resolvedScript = custom2?.scriptPath;
7597
- const script = resolvedScript ?? (existsSync10(hookScript) ? hookScript : undefined);
8154
+ const script = resolvedScript ?? (existsSync11(hookScript) ? hookScript : undefined);
7598
8155
  if (!script)
7599
8156
  throw new Error(`Hook script not found: ${name}`);
7600
8157
  const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
7601
- const content = readFileSync9(script);
8158
+ const content = readFileSync11(script);
7602
8159
  const check = checkScriptHash2(name, sha256Of2(content));
7603
8160
  if (!check.ok) {
7604
8161
  throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);
@@ -7629,6 +8186,7 @@ async function runHook(name, input, options = {}) {
7629
8186
  scriptPath: script,
7630
8187
  content,
7631
8188
  args: custom2?.manifest.args ?? [],
8189
+ env: custom2?.manifest.env,
7632
8190
  stdin: JSON.stringify(hookInput),
7633
8191
  timeout: effectiveTimeout ?? undefined
7634
8192
  }));
@@ -7686,102 +8244,102 @@ async function runHook(name, input, options = {}) {
7686
8244
  return { output, stderr: stderrText, exitCode };
7687
8245
  }
7688
8246
  export {
7689
- writeLock,
7690
- writeCustomHook,
7691
- writeConfig,
7692
- verifyScriptHash,
7693
- upsertHookRecord,
7694
- updateProfile,
7695
- uninstallHook,
7696
- touchProfile,
7697
- syncHooks,
7698
- storageSync,
7699
- storagePush,
7700
- storagePull,
7701
- startServeServer,
7702
- sha256Of,
7703
- sha256File,
7704
- setPinnedHook,
7705
- searchHooks,
7706
- runStorageMigrations,
7707
- runHook,
7708
- retrustHook,
7709
- resolveTables,
7710
- resolveScriptPath,
7711
- resolveScript,
7712
- resolveHookMeta,
7713
- resolveHookDir,
7714
- resolveHook,
7715
- resolveApiUrl,
7716
- resolveApiKey,
7717
- removeProjectHook,
7718
- removePinnedHook,
7719
- removeHookRecord,
7720
- removeHookFromStore,
7721
- removeHook,
7722
- readLock,
7723
- readCustomManifest,
7724
- readConfig,
7725
- provisionCloudflareResources,
7726
- planSync,
7727
- pinInstalledHook,
7728
- parseStorageTables,
7729
- parseManifest,
7730
- listProjectHooks,
7731
- listProfiles,
7732
- listHookRecords,
7733
- listCustomHooks,
7734
- isEventSupported,
7735
- isCustomSource,
7736
- installHooksForProject,
7737
- installHooks,
7738
- installHookForProject,
7739
- installHook,
7740
- installCustomSource,
7741
- importProfiles,
7742
- hookExists,
7743
- handleServeRequest,
7744
- getSyncMetaAll,
7745
- getStorageStatus,
7746
- getStoragePg,
7747
- getStorageDatabaseUrl,
7748
- getStorageDatabaseEnvName,
7749
- getStorageDatabaseEnv,
7750
- getStorageBackend,
7751
- getSettingsPath,
7752
- getRegisteredHooksForTarget,
7753
- getRegisteredHooks,
7754
- getProfilesDir,
7755
- getProfile,
7756
- getPinnedHook,
7757
- getLockPath,
7758
- getInstalledHooks,
7759
- getHooksDataDir,
7760
- getHooksByCategory,
7761
- getHookRecord,
7762
- getHookPath,
7763
- getHookEvents,
7764
- getHook,
7765
- getCustomHooksDir,
7766
- getConfigPath,
7767
- fetchPinnedHook,
7768
- exportProfiles,
7769
- deleteProfile,
7770
- createProfile,
7771
- checkScriptHash,
7772
- buildCodewithTomlFragment,
7773
- STORAGE_TABLES,
7774
- STORAGE_DATABASE_ENV,
7775
- STORAGE_BACKEND_ENV,
7776
- STORAGE_BACKENDS,
7777
- RETIRED_STORAGE_MODE_ENV,
7778
- HOOK_EVENTS,
7779
- HOOKS_STORAGE_TABLES,
7780
- HOOKS_STORAGE_FALLBACK_ENV,
7781
- HOOKS_STORAGE_ENV,
7782
- HOOKS_STORAGE_BACKEND_FALLBACK_ENV,
7783
- HOOKS_STORAGE_BACKEND_ENV,
7784
- HOOKS,
8247
+ CATEGORIES,
7785
8248
  DEFAULT_SERVE_PORT,
7786
- CATEGORIES
8249
+ HOOKS,
8250
+ HOOKS_STORAGE_BACKEND_ENV,
8251
+ HOOKS_STORAGE_BACKEND_FALLBACK_ENV,
8252
+ HOOKS_STORAGE_ENV,
8253
+ HOOKS_STORAGE_FALLBACK_ENV,
8254
+ HOOKS_STORAGE_TABLES,
8255
+ HOOK_EVENTS,
8256
+ RETIRED_STORAGE_MODE_ENV,
8257
+ STORAGE_BACKENDS,
8258
+ STORAGE_BACKEND_ENV,
8259
+ STORAGE_DATABASE_ENV,
8260
+ STORAGE_TABLES,
8261
+ buildCodewithTomlFragment,
8262
+ checkScriptHash,
8263
+ createProfile,
8264
+ deleteProfile,
8265
+ exportProfiles,
8266
+ fetchPinnedHook,
8267
+ getConfigPath,
8268
+ getCustomHooksDir,
8269
+ getHook,
8270
+ getHookEvents,
8271
+ getHookPath,
8272
+ getHookRecord,
8273
+ getHooksByCategory,
8274
+ getHooksDataDir,
8275
+ getInstalledHooks,
8276
+ getLockPath,
8277
+ getPinnedHook,
8278
+ getProfile,
8279
+ getProfilesDir,
8280
+ getRegisteredHooks,
8281
+ getRegisteredHooksForTarget,
8282
+ getSettingsPath,
8283
+ getStorageBackend,
8284
+ getStorageDatabaseEnv,
8285
+ getStorageDatabaseEnvName,
8286
+ getStorageDatabaseUrl,
8287
+ getStoragePg,
8288
+ getStorageStatus,
8289
+ getSyncMetaAll,
8290
+ handleServeRequest,
8291
+ hookExists,
8292
+ importProfiles,
8293
+ installCustomSource,
8294
+ installHook,
8295
+ installHookForProject,
8296
+ installHooks,
8297
+ installHooksForProject,
8298
+ isCustomSource,
8299
+ isEventSupported,
8300
+ listCustomHooks,
8301
+ listHookRecords,
8302
+ listProfiles,
8303
+ listProjectHooks,
8304
+ parseManifest,
8305
+ parseStorageTables,
8306
+ pinInstalledHook,
8307
+ planSync,
8308
+ provisionCloudflareResources,
8309
+ readConfig,
8310
+ readCustomManifest,
8311
+ readLock,
8312
+ removeHook,
8313
+ removeHookFromStore,
8314
+ removeHookRecord,
8315
+ removePinnedHook,
8316
+ removeProjectHook,
8317
+ resolveApiKey,
8318
+ resolveApiUrl,
8319
+ resolveHook,
8320
+ resolveHookDir,
8321
+ resolveHookMeta,
8322
+ resolveScript,
8323
+ resolveScriptPath,
8324
+ resolveTables,
8325
+ retrustHook,
8326
+ runHook,
8327
+ runStorageMigrations,
8328
+ searchHooks,
8329
+ setPinnedHook,
8330
+ sha256File,
8331
+ sha256Of,
8332
+ startServeServer,
8333
+ storagePull,
8334
+ storagePush,
8335
+ storageSync,
8336
+ syncHooks,
8337
+ touchProfile,
8338
+ uninstallHook,
8339
+ updateProfile,
8340
+ upsertHookRecord,
8341
+ verifyScriptHash,
8342
+ writeConfig,
8343
+ writeCustomHook,
8344
+ writeLock
7787
8345
  };