@primitive.ai/prim 0.1.0-alpha.90 → 0.1.0-alpha.91

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 (45) hide show
  1. package/README.md +20 -12
  2. package/SKILL.md +6 -6
  3. package/dist/{chunk-LMRYZL3D.js → chunk-5AFRMOPX.js} +1 -1
  4. package/dist/{chunk-RO3F6JYV.js → chunk-5GYQ4J3D.js} +2 -2
  5. package/dist/{chunk-3EEFL6NJ.js → chunk-67VNLLBN.js} +261 -1
  6. package/dist/{chunk-LEMX3IOG.js → chunk-BTNNEH2W.js} +1 -1
  7. package/dist/chunk-EWZMDD44.js +1474 -0
  8. package/dist/{chunk-YWO7O6QG.js → chunk-FBAYCS3X.js} +2 -2
  9. package/dist/{chunk-R26A7TWY.js → chunk-FKXFUUWJ.js} +1 -1
  10. package/dist/{chunk-C7T7B45R.js → chunk-FOLKHXLJ.js} +150 -21
  11. package/dist/chunk-H4UNYFUC.js +196 -0
  12. package/dist/chunk-HU4PW6I6.js +264 -0
  13. package/dist/{chunk-CUE2LKLR.js → chunk-KFXF2V5G.js} +1 -1
  14. package/dist/{chunk-6IWNH776.js → chunk-LFFLHSQO.js} +4 -3
  15. package/dist/{chunk-MAZWKJGL.js → chunk-M6RIJVRE.js} +42 -232
  16. package/dist/{chunk-2JRX7ZR4.js → chunk-NAHHFSLH.js} +16 -5
  17. package/dist/{chunk-FI3LIRCB.js → chunk-OAAP2MY6.js} +2 -2
  18. package/dist/{chunk-Z7YPFWAZ.js → chunk-PCVGBE5B.js} +10 -268
  19. package/dist/{chunk-UZPBB7V4.js → chunk-VW3FDYRT.js} +60 -48
  20. package/dist/chunk-WX52U7Z2.js +112 -0
  21. package/dist/{chunk-PPQYQEIR.js → chunk-ZB4F75SZ.js} +28 -0
  22. package/dist/daemon/server.js +20 -20
  23. package/dist/hook-runtime/daemon/server.js +20 -20
  24. package/dist/hook-runtime/hooks/post-commit.js +20 -20
  25. package/dist/hook-runtime/hooks/post-rewrite.js +20 -20
  26. package/dist/hook-runtime/hooks/post-tool-use.js +651 -132
  27. package/dist/hook-runtime/hooks/pre-tool-use.js +644 -96
  28. package/dist/hook-runtime/hooks/prim-hook.js +612 -96
  29. package/dist/hook-runtime/hooks/session-end.js +610 -27
  30. package/dist/hook-runtime/hooks/session-start.js +1132 -533
  31. package/dist/hook-runtime/index.js +735 -264
  32. package/dist/hook-runtime/statusline-main.js +48 -4
  33. package/dist/hooks/post-commit.js +11 -9
  34. package/dist/hooks/post-rewrite.js +11 -9
  35. package/dist/hooks/post-tool-use.js +73 -48
  36. package/dist/hooks/pre-commit.js +4 -4
  37. package/dist/hooks/pre-tool-use.js +46 -44
  38. package/dist/hooks/prim-hook.js +47 -25
  39. package/dist/hooks/session-end.js +15 -26
  40. package/dist/hooks/session-start.js +59 -32
  41. package/dist/index.js +558 -1436
  42. package/dist/statusline-main.js +48 -4
  43. package/package.json +1 -1
  44. package/dist/chunk-PMRDE2DA.js +0 -310
  45. package/dist/chunk-WB7AUZ4C.js +0 -52
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  getSiteUrl
3
- } from "./chunk-Z7YPFWAZ.js";
3
+ } from "./chunk-PCVGBE5B.js";
4
4
  import {
5
5
  jwtOrganizationId,
6
6
  primConfigDirectory,
7
7
  resolveAuthCredential
8
- } from "./chunk-3EEFL6NJ.js";
8
+ } from "./chunk-67VNLLBN.js";
9
9
 
10
10
  // src/journal.ts
11
11
  import {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scheduleCollectScopeRefresh
3
- } from "./chunk-MAZWKJGL.js";
3
+ } from "./chunk-M6RIJVRE.js";
4
4
 
5
5
  // src/ingest-response.ts
6
6
  var IngestAcknowledgementError = class extends Error {
@@ -1,21 +1,26 @@
1
+ import {
2
+ isValidRepoSyncId,
3
+ setRepoSyncId,
4
+ setRepositoryBindingState
5
+ } from "./chunk-M6RIJVRE.js";
6
+ import {
7
+ HttpError,
8
+ getClient
9
+ } from "./chunk-PCVGBE5B.js";
1
10
  import {
2
11
  binFile,
3
12
  compareSemver,
13
+ cursorConfigDirectory,
4
14
  pinnedNpxCommand
5
- } from "./chunk-PMRDE2DA.js";
15
+ } from "./chunk-EWZMDD44.js";
6
16
  import {
7
17
  gitToplevel,
8
- githubRepositoryFullName,
9
- isValidRepoSyncId,
10
- setRepoSyncId,
11
- setRepositoryBindingState
12
- } from "./chunk-MAZWKJGL.js";
18
+ githubRepositoryFullName
19
+ } from "./chunk-H4UNYFUC.js";
13
20
  import {
14
- HttpError,
15
21
  atomicWriteFile,
16
- getClient,
17
22
  withFileLock
18
- } from "./chunk-Z7YPFWAZ.js";
23
+ } from "./chunk-67VNLLBN.js";
19
24
 
20
25
  // src/lib/bin-cache.ts
21
26
  import { chmodSync, mkdirSync } from "fs";
@@ -51,6 +56,7 @@ function warmBinCache() {
51
56
  }
52
57
 
53
58
  // src/commands/skill.ts
59
+ import { createHash as createHash2 } from "crypto";
54
60
  import {
55
61
  constants as constants2,
56
62
  closeSync as closeSync2,
@@ -59,7 +65,10 @@ import {
59
65
  lstatSync as lstatSync2,
60
66
  openSync as openSync2,
61
67
  readFileSync as readFileSync2,
62
- readSync as readSync2
68
+ readSync as readSync2,
69
+ readdirSync as readdirSync2,
70
+ rmdirSync as rmdirSync2,
71
+ unlinkSync
63
72
  } from "fs";
64
73
  import { homedir as homedir3 } from "os";
65
74
  import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
@@ -413,6 +422,107 @@ var AGENT_TARGET = {
413
422
  codex: "AGENTS.md",
414
423
  hermes: ".hermes.md"
415
424
  };
425
+ var CURSOR_SKILL_OWNER = "@primitive.ai/prim";
426
+ var CURSOR_SKILL_MANIFEST = ".prim-owned.json";
427
+ function sha256(content) {
428
+ return createHash2("sha256").update(content).digest("hex");
429
+ }
430
+ function cursorSkillDirectory(cwd, scope) {
431
+ const base = scope === "user" ? cursorConfigDirectory() : gitToplevel(cwd) ?? cwd;
432
+ return join3(base, scope === "user" ? "skills" : ".cursor/skills", "prim");
433
+ }
434
+ function cursorSkillPaths(cwd, scope) {
435
+ const directory = cursorSkillDirectory(cwd, scope);
436
+ return {
437
+ directory,
438
+ skill: join3(directory, "SKILL.md"),
439
+ manifest: join3(directory, CURSOR_SKILL_MANIFEST)
440
+ };
441
+ }
442
+ function readCursorSkillManifest(path) {
443
+ try {
444
+ const value = JSON.parse(readFileSync2(path, "utf8"));
445
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
446
+ const record = value;
447
+ if (record.version !== 1 || record.owner !== CURSOR_SKILL_OWNER || typeof record.contentSha256 !== "string" || !/^[0-9a-f]{64}$/u.test(record.contentSha256)) {
448
+ return;
449
+ }
450
+ return record;
451
+ } catch {
452
+ return;
453
+ }
454
+ }
455
+ function cursorSkillStatus(cwd, scope) {
456
+ const paths = cursorSkillPaths(cwd, scope);
457
+ const manifest = readCursorSkillManifest(paths.manifest);
458
+ const content = existsSync2(paths.skill) ? readFileSync2(paths.skill, "utf8") : void 0;
459
+ const owned = manifest !== void 0;
460
+ const installed = owned && content !== void 0 && sha256(content) === manifest.contentSha256;
461
+ return {
462
+ installed,
463
+ owned,
464
+ edited: owned && content !== void 0 && !installed,
465
+ target: paths.skill
466
+ };
467
+ }
468
+ function validCursorSkillScope(scope) {
469
+ if (scope === void 0 || scope === "project" || scope === "user") return true;
470
+ console.error(`Unknown --scope "${scope}" (expected user or project)`);
471
+ return false;
472
+ }
473
+ function installCursorSkill(cwd, opts) {
474
+ const paths = cursorSkillPaths(cwd, opts.scope);
475
+ const status = cursorSkillStatus(cwd, opts.scope);
476
+ const manifestExists = existsSync2(paths.manifest);
477
+ const skillExists = existsSync2(paths.skill);
478
+ if (manifestExists && !status.owned || !manifestExists && skillExists || status.edited) {
479
+ console.error(`Refusing to overwrite a foreign or edited Cursor skill at ${paths.skill}`);
480
+ return 1;
481
+ }
482
+ const content = loadSkill();
483
+ const manifest = {
484
+ version: 1,
485
+ owner: CURSOR_SKILL_OWNER,
486
+ contentSha256: sha256(content)
487
+ };
488
+ if (status.installed && readFileSync2(paths.skill, "utf8") === content) {
489
+ console.log(`No changes \u2014 Cursor skill already up to date at ${paths.skill}`);
490
+ return 0;
491
+ }
492
+ if (opts.dryRun) {
493
+ const existing = skillExists ? readFileSync2(paths.skill, "utf8") : "";
494
+ process.stdout.write(createPatch(paths.skill, existing, content, "current", "proposed"));
495
+ return 0;
496
+ }
497
+ atomicWriteFile(paths.skill, content, { ensureParent: true });
498
+ atomicWriteFile(paths.manifest, `${JSON.stringify(manifest, null, 2)}
499
+ `, { ensureParent: true });
500
+ console.log(`Installed Cursor skill at ${paths.skill}`);
501
+ return 0;
502
+ }
503
+ function uninstallCursorSkill(cwd, scope) {
504
+ const paths = cursorSkillPaths(cwd, scope);
505
+ const status = cursorSkillStatus(cwd, scope);
506
+ if (!status.owned) {
507
+ console.log(`No Primitive-owned Cursor skill at ${paths.skill}`);
508
+ return 0;
509
+ }
510
+ if (status.edited) {
511
+ console.error(`Refusing to remove an edited Cursor skill at ${paths.skill}`);
512
+ return 1;
513
+ }
514
+ if (existsSync2(paths.skill)) unlinkSync(paths.skill);
515
+ if (existsSync2(paths.manifest)) unlinkSync(paths.manifest);
516
+ try {
517
+ if (readdirSync2(paths.directory).length === 0) rmdirSync2(paths.directory);
518
+ } catch {
519
+ }
520
+ console.log(`Removed Primitive-owned Cursor skill from ${paths.skill}`);
521
+ return 0;
522
+ }
523
+ function hasUsableCursorSkill(projectRoot) {
524
+ return cursorSkillStatus(projectRoot, "project").installed || cursorSkillStatus(projectRoot, "user").installed;
525
+ }
416
526
  function userTargetFor(agent) {
417
527
  if (agent === "claude") return join3(homedir3(), ".claude", "CLAUDE.md");
418
528
  if (agent === "codex") return join3(homedir3(), ".codex", "AGENTS.md");
@@ -468,18 +578,18 @@ function resolveTarget(cwd, opts) {
468
578
  if (opts.target) return resolve2(cwd, opts.target);
469
579
  if (opts.scope === "user") {
470
580
  if (!opts.agent) {
471
- console.error("--scope user requires --agent (claude, codex, or hermes)");
581
+ console.error("--scope user requires --agent (claude, codex, cursor, or hermes)");
472
582
  return null;
473
583
  }
474
584
  const userTarget = userTargetFor(opts.agent);
475
585
  if (userTarget) return userTarget;
476
- console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
586
+ console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, cursor, or hermes)`);
477
587
  return null;
478
588
  }
479
589
  if (opts.agent) {
480
590
  const mapped = AGENT_TARGET[opts.agent];
481
591
  if (typeof mapped === "string") return resolve2(cwd, mapped);
482
- console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
592
+ console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, cursor, or hermes)`);
483
593
  return null;
484
594
  }
485
595
  const matches = detectTargets(cwd);
@@ -491,6 +601,10 @@ function resolveTarget(cwd, opts) {
491
601
  }
492
602
  function runInstall(cwd, opts) {
493
603
  if (opts.agent === "claude" && !opts.target) return installClaudePlugin(cwd, opts);
604
+ if (opts.agent === "cursor" && !opts.target) {
605
+ if (!validCursorSkillScope(opts.scope)) return 1;
606
+ return installCursorSkill(cwd, opts);
607
+ }
494
608
  const target = resolveTarget(cwd, opts);
495
609
  if (target === null) return 1;
496
610
  const existing = existsSync2(target) ? readFileSync2(target, "utf-8") : "";
@@ -511,6 +625,10 @@ function runInstall(cwd, opts) {
511
625
  }
512
626
  function runUninstall(cwd, opts) {
513
627
  if (opts.agent === "claude" && !opts.target) return uninstallClaudePlugin(cwd, opts);
628
+ if (opts.agent === "cursor" && !opts.target) {
629
+ if (!validCursorSkillScope(opts.scope)) return 1;
630
+ return uninstallCursorSkill(cwd, opts.scope);
631
+ }
514
632
  const target = resolveTarget(cwd, opts);
515
633
  if (target === null) return 1;
516
634
  if (!existsSync2(target)) {
@@ -529,6 +647,16 @@ function runUninstall(cwd, opts) {
529
647
  }
530
648
  function runStatus(cwd, opts) {
531
649
  if (opts.agent === "claude" && !opts.target) return statusClaudePlugin(cwd, opts);
650
+ if (opts.agent === "cursor" && !opts.target) {
651
+ if (!validCursorSkillScope(opts.scope)) return 1;
652
+ const status = cursorSkillStatus(cwd, opts.scope);
653
+ if (opts.json) printJson(status);
654
+ else
655
+ console.log(
656
+ status.installed ? `Primitive Cursor skill installed at ${status.target}` : `No intact Primitive-owned Cursor skill at ${status.target}`
657
+ );
658
+ return status.installed ? 0 : 1;
659
+ }
532
660
  const target = resolveTarget(cwd, opts);
533
661
  if (target === null) return 1;
534
662
  const fileExists = existsSync2(target);
@@ -554,7 +682,7 @@ function runStatus(cwd, opts) {
554
682
  }
555
683
  function registerSkillCommands(program) {
556
684
  const skill = program.command("skill").description("Manage the prim skill in your project rules file");
557
- skill.command("install").description("Install the prim skill block into your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").option("--dry-run", "Print a unified diff without writing").action((opts) => {
685
+ skill.command("install").description("Install the prim skill block into your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, cursor, or hermes").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").option("--dry-run", "Print a unified diff without writing").action((opts) => {
558
686
  try {
559
687
  process.exit(runInstall(process.cwd(), opts));
560
688
  } catch (err) {
@@ -562,17 +690,17 @@ function registerSkillCommands(program) {
562
690
  process.exit(2);
563
691
  }
564
692
  });
565
- skill.command("uninstall").description("Remove the prim skill block from your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").action((opts) => {
693
+ skill.command("uninstall").description("Remove the prim skill block from your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, cursor, or hermes").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").action((opts) => {
566
694
  process.exit(runUninstall(process.cwd(), opts));
567
695
  });
568
- skill.command("status").description("Report whether the prim skill block is installed").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").option("--json", "Output as JSON").action((opts) => {
696
+ skill.command("status").description("Report whether the prim skill block is installed").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, cursor, or hermes").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").option("--json", "Output as JSON").action((opts) => {
569
697
  process.exit(runStatus(process.cwd(), opts));
570
698
  });
571
699
  }
572
700
 
573
701
  // src/lib/post-commit-hook.ts
574
702
  import { execFileSync } from "child_process";
575
- import { createHash as createHash2, randomUUID } from "crypto";
703
+ import { createHash as createHash3, randomUUID } from "crypto";
576
704
  import {
577
705
  constants as constants3,
578
706
  closeSync as closeSync3,
@@ -584,7 +712,7 @@ import {
584
712
  openSync as openSync3,
585
713
  readFileSync as readFileSync3,
586
714
  renameSync,
587
- unlinkSync,
715
+ unlinkSync as unlinkSync2,
588
716
  writeFileSync
589
717
  } from "fs";
590
718
  import { basename, dirname as dirname3, isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
@@ -838,7 +966,7 @@ function assertUsableHuskyDispatcher(target, spec) {
838
966
  const huskyRuntime = resolve3(dirname3(target.dispatcherPath), "h");
839
967
  const runtimeStat = assertSafeFile(huskyRuntime, spec);
840
968
  const runtime = runtimeStat ? readHookFile(huskyRuntime) : void 0;
841
- const runtimeHash = runtime ? createHash2("sha256").update(runtime).digest("hex") : void 0;
969
+ const runtimeHash = runtime ? createHash3("sha256").update(runtime).digest("hex") : void 0;
842
970
  if (!runtimeHash || !HUSKY_V9_RUNTIME_HASHES.has(runtimeHash)) {
843
971
  throw new Error(`unrecognized Husky v9 hook runtime: ${huskyRuntime}`);
844
972
  }
@@ -1018,7 +1146,7 @@ function rewriteHookAtomically(path, next, expected, spec, stat) {
1018
1146
  }
1019
1147
  }
1020
1148
  try {
1021
- unlinkSync(temporary);
1149
+ unlinkSync2(temporary);
1022
1150
  } catch (error) {
1023
1151
  if (errorCode(error) !== "ENOENT") {
1024
1152
  }
@@ -1027,7 +1155,7 @@ function rewriteHookAtomically(path, next, expected, spec, stat) {
1027
1155
  }
1028
1156
  function unlinkHookUnchanged(path, expected, stat, spec) {
1029
1157
  assertTargetUnchanged(path, expected, spec, stat);
1030
- unlinkSync(path);
1158
+ unlinkSync2(path);
1031
1159
  }
1032
1160
  function ensureEffectivePostCommitHook(cwd = process.cwd()) {
1033
1161
  const target = resolveEffectivePostCommitHook(cwd);
@@ -1396,5 +1524,6 @@ export {
1396
1524
  printJson,
1397
1525
  refreshClaudePlugins,
1398
1526
  hasUsableCodexGuidance,
1527
+ hasUsableCursorSkill,
1399
1528
  registerSkillCommands
1400
1529
  };
@@ -0,0 +1,196 @@
1
+ // src/lib/git.ts
2
+ import { execFileSync } from "child_process";
3
+ import { createHash } from "crypto";
4
+ import { lstatSync, realpathSync } from "fs";
5
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
6
+ var GIT_TIMEOUT_MS = 1e3;
7
+ var GITHUB_OWNER_PATTERN = /^(?![A-Za-z0-9-]*--)[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
8
+ var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]{1,100}$/u;
9
+ function gitToplevel(cwd) {
10
+ try {
11
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
12
+ cwd,
13
+ encoding: "utf-8",
14
+ stdio: ["ignore", "pipe", "ignore"],
15
+ timeout: GIT_TIMEOUT_MS
16
+ }).trim();
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ function gitValue(cwd, args) {
22
+ try {
23
+ const value = execFileSync("git", args, {
24
+ cwd,
25
+ encoding: "utf-8",
26
+ stdio: ["ignore", "pipe", "ignore"],
27
+ timeout: GIT_TIMEOUT_MS
28
+ }).trim();
29
+ return value || void 0;
30
+ } catch {
31
+ return void 0;
32
+ }
33
+ }
34
+ function currentBranch(cwd) {
35
+ return gitValue(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
36
+ }
37
+ function githubRepositoryFullName(cwd) {
38
+ const remote = gitValue(cwd, ["config", "--get", "remote.origin.url"]);
39
+ if (!remote || [...remote].some((char) => char < " " || char === "\x7F")) return null;
40
+ const match = /^(?:git@github\.com:([^?#]+)|(?:https|ssh):\/\/(?:[^/@]+@)?github\.com(?::\d+)?\/([^?#]+))$/i.exec(
41
+ remote
42
+ );
43
+ const path = match?.[1] ?? match?.[2];
44
+ if (!path) return null;
45
+ const parts = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
46
+ if (parts.length !== 2 || !GITHUB_OWNER_PATTERN.test(parts[0] ?? "") || !GITHUB_REPOSITORY_PATTERN.test(parts[1] ?? "")) {
47
+ return null;
48
+ }
49
+ return `${parts[0]}/${parts[1]}`;
50
+ }
51
+ function canonicalGitRoot(cwd) {
52
+ try {
53
+ const top = gitToplevel(cwd);
54
+ return top ? realpathSync.native(top) : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ function normalizeOriginRemote(remote) {
60
+ const value = remote.trim();
61
+ if (!value) return void 0;
62
+ const scp = !value.includes("://") ? /^(?:[^@/\s]+@)?(?<host>[^:/\s]+):(?<path>.+)$/.exec(value)?.groups : void 0;
63
+ if (scp?.host && scp.path) {
64
+ const path = scp.path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
65
+ return path ? `${scp.host.toLowerCase()}/${path}` : void 0;
66
+ }
67
+ try {
68
+ const url = new URL(value);
69
+ url.username = "";
70
+ url.password = "";
71
+ url.search = "";
72
+ url.hash = "";
73
+ const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
74
+ if (url.host && path) return `${url.host.toLowerCase()}/${path}`;
75
+ return void 0;
76
+ } catch {
77
+ return void 0;
78
+ }
79
+ }
80
+ function opaqueRepoKey(source, value) {
81
+ return `repo_v1_${createHash("sha256").update(`${source}\0${value}`).digest("hex")}`;
82
+ }
83
+ function resolveRepositoryContext(cwd = process.cwd()) {
84
+ const top = gitToplevel(cwd);
85
+ if (!top) return null;
86
+ let repoRoot;
87
+ try {
88
+ repoRoot = realpathSync.native(top);
89
+ } catch {
90
+ repoRoot = resolve(top);
91
+ }
92
+ const origin = gitValue(repoRoot, ["config", "--get", "remote.origin.url"]);
93
+ const repoFullName = githubRepositoryFullName(repoRoot) ?? void 0;
94
+ const normalizedOrigin = origin ? normalizeOriginRemote(origin) : void 0;
95
+ if (normalizedOrigin) {
96
+ return {
97
+ repoRoot,
98
+ repoKey: opaqueRepoKey("origin", normalizedOrigin),
99
+ identitySource: "origin",
100
+ repoFullName
101
+ };
102
+ }
103
+ const roots = gitValue(repoRoot, ["rev-list", "--max-parents=0", "HEAD"])?.split(/\s+/).filter(Boolean).sort();
104
+ if (roots && roots.length > 0) {
105
+ return {
106
+ repoRoot,
107
+ repoKey: opaqueRepoKey("root_commit", roots.join("\n")),
108
+ identitySource: "root_commit",
109
+ repoFullName
110
+ };
111
+ }
112
+ return { repoRoot, repoFullName };
113
+ }
114
+ function nearestExistingAncestor(path) {
115
+ const suffix = [];
116
+ let cursor = path;
117
+ while (true) {
118
+ try {
119
+ lstatSync(cursor);
120
+ break;
121
+ } catch (error) {
122
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
123
+ if (code !== "ENOENT" && code !== "ENOTDIR") return null;
124
+ const parent = dirname(cursor);
125
+ if (parent === cursor) return null;
126
+ suffix.unshift(cursor.slice(parent.length + (parent.endsWith(sep) ? 0 : 1)));
127
+ cursor = parent;
128
+ }
129
+ }
130
+ try {
131
+ return { real: realpathSync.native(cursor), suffix };
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+ function canonicalRepositoryPathScoped(filePath, context, baseDir) {
137
+ if (!filePath || filePath !== filePath.trim() || filePath.includes("\0")) {
138
+ return { ok: false, reason: "invalid_path" };
139
+ }
140
+ const absolute = resolve(baseDir, filePath);
141
+ const resolved = nearestExistingAncestor(absolute);
142
+ if (!resolved) return { ok: false, reason: "invalid_path" };
143
+ const target = resolve(resolved.real, ...resolved.suffix);
144
+ const rel = relative(context.repoRoot, target);
145
+ if (!rel || rel === ".") return { ok: false, reason: "invalid_path" };
146
+ if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) {
147
+ return { ok: false, reason: "outside_repository" };
148
+ }
149
+ return { ok: true, file: sep === "/" ? rel : rel.split(sep).join("/") };
150
+ }
151
+ function canonicalRepositoryPathLegacy(filePath, cwd, repositoryRoot = canonicalGitRoot(cwd)) {
152
+ if (!repositoryRoot || !filePath || [...filePath].some((char) => char < " " || char === "\x7F" || "~*?[]{}$`\\".includes(char)))
153
+ return null;
154
+ const absolute = resolve(cwd, filePath);
155
+ let target;
156
+ try {
157
+ const stat = lstatSync(absolute);
158
+ if (!stat.isFile() || stat.isSymbolicLink()) return null;
159
+ target = realpathSync.native(absolute);
160
+ } catch (error) {
161
+ if (!(error instanceof Error) || error.code !== "ENOENT") {
162
+ return null;
163
+ }
164
+ try {
165
+ const parent = realpathSync.native(dirname(absolute));
166
+ if (!lstatSync(parent).isDirectory()) return null;
167
+ target = resolve(parent, basename(absolute));
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+ const rel = relative(repositoryRoot, target);
173
+ if (!rel || isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) {
174
+ return null;
175
+ }
176
+ return sep === "/" ? rel : rel.split(sep).join("/");
177
+ }
178
+ function canonicalRepositoryPath(filePath, contextOrCwd, baseDirOrRoot) {
179
+ if (typeof contextOrCwd === "string") {
180
+ return canonicalRepositoryPathLegacy(filePath, contextOrCwd, baseDirOrRoot);
181
+ }
182
+ return canonicalRepositoryPathScoped(
183
+ filePath,
184
+ contextOrCwd,
185
+ baseDirOrRoot ?? contextOrCwd.repoRoot
186
+ );
187
+ }
188
+
189
+ export {
190
+ gitToplevel,
191
+ currentBranch,
192
+ githubRepositoryFullName,
193
+ canonicalGitRoot,
194
+ resolveRepositoryContext,
195
+ canonicalRepositoryPath
196
+ };