@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.
- package/README.md +20 -12
- package/SKILL.md +6 -6
- package/dist/{chunk-LMRYZL3D.js → chunk-5AFRMOPX.js} +1 -1
- package/dist/{chunk-RO3F6JYV.js → chunk-5GYQ4J3D.js} +2 -2
- package/dist/{chunk-3EEFL6NJ.js → chunk-67VNLLBN.js} +261 -1
- package/dist/{chunk-LEMX3IOG.js → chunk-BTNNEH2W.js} +1 -1
- package/dist/chunk-EWZMDD44.js +1474 -0
- package/dist/{chunk-YWO7O6QG.js → chunk-FBAYCS3X.js} +2 -2
- package/dist/{chunk-R26A7TWY.js → chunk-FKXFUUWJ.js} +1 -1
- package/dist/{chunk-C7T7B45R.js → chunk-FOLKHXLJ.js} +150 -21
- package/dist/chunk-H4UNYFUC.js +196 -0
- package/dist/chunk-HU4PW6I6.js +264 -0
- package/dist/{chunk-CUE2LKLR.js → chunk-KFXF2V5G.js} +1 -1
- package/dist/{chunk-6IWNH776.js → chunk-LFFLHSQO.js} +4 -3
- package/dist/{chunk-MAZWKJGL.js → chunk-M6RIJVRE.js} +42 -232
- package/dist/{chunk-2JRX7ZR4.js → chunk-NAHHFSLH.js} +16 -5
- package/dist/{chunk-FI3LIRCB.js → chunk-OAAP2MY6.js} +2 -2
- package/dist/{chunk-Z7YPFWAZ.js → chunk-PCVGBE5B.js} +10 -268
- package/dist/{chunk-UZPBB7V4.js → chunk-VW3FDYRT.js} +60 -48
- package/dist/chunk-WX52U7Z2.js +112 -0
- package/dist/{chunk-PPQYQEIR.js → chunk-ZB4F75SZ.js} +28 -0
- package/dist/daemon/server.js +20 -20
- package/dist/hook-runtime/daemon/server.js +20 -20
- package/dist/hook-runtime/hooks/post-commit.js +20 -20
- package/dist/hook-runtime/hooks/post-rewrite.js +20 -20
- package/dist/hook-runtime/hooks/post-tool-use.js +651 -132
- package/dist/hook-runtime/hooks/pre-tool-use.js +644 -96
- package/dist/hook-runtime/hooks/prim-hook.js +612 -96
- package/dist/hook-runtime/hooks/session-end.js +610 -27
- package/dist/hook-runtime/hooks/session-start.js +1132 -533
- package/dist/hook-runtime/index.js +735 -264
- package/dist/hook-runtime/statusline-main.js +48 -4
- package/dist/hooks/post-commit.js +11 -9
- package/dist/hooks/post-rewrite.js +11 -9
- package/dist/hooks/post-tool-use.js +73 -48
- package/dist/hooks/pre-commit.js +4 -4
- package/dist/hooks/pre-tool-use.js +46 -44
- package/dist/hooks/prim-hook.js +47 -25
- package/dist/hooks/session-end.js +15 -26
- package/dist/hooks/session-start.js +59 -32
- package/dist/index.js +558 -1436
- package/dist/statusline-main.js +48 -4
- package/package.json +1 -1
- package/dist/chunk-PMRDE2DA.js +0 -310
- package/dist/chunk-WB7AUZ4C.js +0 -52
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getSiteUrl
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PCVGBE5B.js";
|
|
4
4
|
import {
|
|
5
5
|
jwtOrganizationId,
|
|
6
6
|
primConfigDirectory,
|
|
7
7
|
resolveAuthCredential
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-67VNLLBN.js";
|
|
9
9
|
|
|
10
10
|
// src/journal.ts
|
|
11
11
|
import {
|
|
@@ -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-
|
|
15
|
+
} from "./chunk-EWZMDD44.js";
|
|
6
16
|
import {
|
|
7
17
|
gitToplevel,
|
|
8
|
-
githubRepositoryFullName
|
|
9
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|