@primitive.ai/prim 0.1.0-alpha.67 → 0.1.0-alpha.69
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 +39 -1
- package/SKILL.md +1 -1
- package/dist/{chunk-XPVNH3J6.js → chunk-2KW7DVFJ.js} +121 -130
- package/dist/chunk-3EEFL6NJ.js +529 -0
- package/dist/chunk-4A2XB2PC.js +310 -0
- package/dist/{chunk-G5D3FF3D.js → chunk-BQUJ4AFJ.js} +1 -83
- package/dist/chunk-CUE2LKLR.js +155 -0
- package/dist/chunk-FI3LIRCB.js +262 -0
- package/dist/{chunk-CPSE3Y3T.js → chunk-H64F6QHV.js} +27 -8
- package/dist/chunk-KKP2NYZ3.js +505 -0
- package/dist/{chunk-JQFN6LBO.js → chunk-LEMX3IOG.js} +6 -37
- package/dist/{chunk-U6L2RU67.js → chunk-LMRYZL3D.js} +52 -45
- package/dist/{chunk-XUAP572Z.js → chunk-RO3F6JYV.js} +8 -7
- package/dist/{chunk-TDRXHBEX.js → chunk-TJTVHDJ5.js} +41 -18
- package/dist/{chunk-TCDUH7AN.js → chunk-WB7AUZ4C.js} +17 -0
- package/dist/chunk-XGADQPCI.js +26 -0
- package/dist/{chunk-UN5O5SYG.js → chunk-YWO7O6QG.js} +14 -53
- package/dist/chunk-Z7YPFWAZ.js +917 -0
- package/dist/{chunk-BDGQEJA5.js → chunk-ZOSNEO5N.js} +2 -2
- package/dist/daemon/server.js +11804 -470
- package/dist/hook-runtime/daemon/server.js +13353 -0
- package/dist/hook-runtime/hooks/post-commit.js +1128 -0
- package/dist/hook-runtime/hooks/post-rewrite.js +1148 -0
- package/dist/hook-runtime/hooks/post-tool-use.js +7586 -0
- package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
- package/dist/hook-runtime/hooks/pre-tool-use.js +7099 -0
- package/dist/hook-runtime/hooks/prim-hook.js +7691 -0
- package/dist/hook-runtime/hooks/session-end.js +495 -0
- package/dist/hook-runtime/hooks/session-start.js +11284 -0
- package/dist/hook-runtime/index.js +42775 -0
- package/dist/hook-runtime/statusline-main.js +765 -0
- package/dist/hooks/post-commit.js +7 -4
- package/dist/hooks/post-rewrite.js +7 -4
- package/dist/hooks/post-tool-use.js +64 -32
- package/dist/hooks/pre-commit.js +4 -3
- package/dist/hooks/pre-tool-use.js +118 -38
- package/dist/hooks/prim-hook.js +72 -27
- package/dist/hooks/session-end.js +5 -4
- package/dist/hooks/session-start.js +36 -25
- package/dist/index.js +15039 -2555
- package/dist/statusline-main.js +488 -96
- package/package.json +11 -5
- package/dist/chunk-CZ24W5QU.js +0 -123
- package/dist/chunk-RTLFE6EZ.js +0 -518
- package/dist/chunk-UTKQTZHL.js +0 -88
package/README.md
CHANGED
|
@@ -94,6 +94,23 @@ Orchestrates auth → session hooks → supervised daemon → capture-health gat
|
|
|
94
94
|
re-running each underlying command so every step behaves exactly as if run by
|
|
95
95
|
hand (including the browser login). Idempotent — safe to re-run.
|
|
96
96
|
|
|
97
|
+
### Uninstall
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
prim uninstall
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Stops the companion daemon, removes the current repository's Prim-owned agent
|
|
104
|
+
and Git-hook surfaces when run inside a Git repository, removes the user-scoped
|
|
105
|
+
Claude, Codex, Hermes, and Git-hook surfaces, then deletes only schema-valid
|
|
106
|
+
Prim runtime bytes. Foreign configuration is retained; ambiguous ownership
|
|
107
|
+
makes the command fail closed and retain the runtimes. If a removal races or
|
|
108
|
+
fails after one runtime changes, its JSON result reports each runtime's exact
|
|
109
|
+
state so rerunning can safely finish cleanup. Authentication,
|
|
110
|
+
undelivered journals, repository bindings, and agent skill guidance are kept.
|
|
111
|
+
Use `prim skill uninstall --agent <claude|codex|hermes> --scope <project|user>`
|
|
112
|
+
when you also want to remove a known skill target.
|
|
113
|
+
|
|
97
114
|
### Auth
|
|
98
115
|
|
|
99
116
|
```bash
|
|
@@ -101,8 +118,23 @@ prim auth login # Authenticate via browser
|
|
|
101
118
|
prim auth set-token <token> # Save a bearer token (e.g. for CI)
|
|
102
119
|
prim auth clear # Remove saved tokens
|
|
103
120
|
prim auth status # Check authentication status
|
|
121
|
+
prim auth api-keys mint --name <name> [--expires-at <epoch-ms>] # Mint a user key; secret prints once
|
|
122
|
+
prim auth api-keys list [--limit <count>] [--after <api-key-id>] # List user-key metadata
|
|
123
|
+
prim auth api-keys revoke <api-key-id> # Revoke a user key
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### GitHub
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
prim github connect # Install or reuse the Primitive GitHub App for this checkout
|
|
130
|
+
prim github connect --no-browser # Print the installation URL without opening it
|
|
104
131
|
```
|
|
105
132
|
|
|
133
|
+
The command first reuses existing repository access. When access is missing, it creates a
|
|
134
|
+
server-owned GitHub App installation intent, opens its installation URL unless browser opening is
|
|
135
|
+
suppressed, polls the same intent, then verifies and persists this checkout's binding. A completed
|
|
136
|
+
installation without admin access to this repository remains truthfully unbound.
|
|
137
|
+
|
|
106
138
|
### Session integration
|
|
107
139
|
|
|
108
140
|
Wires the agent's session hooks so the decisions you make are captured into the
|
|
@@ -145,13 +177,19 @@ unless Prim's global `--yes` is present.
|
|
|
145
177
|
#### Claude decision feedback
|
|
146
178
|
|
|
147
179
|
Claude Code receives eventual, human-visible feedback when automatic capture
|
|
148
|
-
creates
|
|
180
|
+
creates a `change` Decision. A later Claude `Stop` or fresh
|
|
149
181
|
`SessionStart` in the same Git worktree can display:
|
|
150
182
|
|
|
151
183
|
```text
|
|
152
184
|
[prim] response → created Decision (dec_a1b2c3d4): Use the stable API (https://app.getprimitive.ai/decisions/r571n1dqjdrtyxxpf0fnzee4gn8aed6q)
|
|
153
185
|
```
|
|
154
186
|
|
|
187
|
+
An author-private draft uses an actionable publish prompt instead:
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
[prim] publish this Decision draft (dec_a1b2c3d4)? Use the stable API (https://app.getprimitive.ai/decisions/r571n1dqjdrtyxxpf0fnzee4gn8aed6q) Run `prim decisions publish decision-full-1` to share it with your team.
|
|
191
|
+
```
|
|
192
|
+
|
|
155
193
|
This is a hook `systemMessage` for the person using Claude Code; it is not
|
|
156
194
|
injected into the model's context. Delivery is at-least-once at the stdout
|
|
157
195
|
handoff boundary: prim acknowledges only after writing the hook response, so a
|
package/SKILL.md
CHANGED
|
@@ -20,7 +20,7 @@ Run `npx --yes @primitive.ai/prim auth status` first. It exits **0 if authentica
|
|
|
20
20
|
Three ways to authenticate, in priority order:
|
|
21
21
|
|
|
22
22
|
1. **`PRIM_TOKEN` environment variable** -- preferred for agents and CI. Set it before invoking prim and you're done; no interactive flow, no token files.
|
|
23
|
-
2. **`npx --yes @primitive.ai/prim auth set-token <token>`** -- saves a bearer token
|
|
23
|
+
2. **`npx --yes @primitive.ai/prim auth set-token <token>`** -- saves a bearer token in Primitive's config directory (`PRIM_CONFIG_DIR`, then `$XDG_CONFIG_HOME/prim`, default `~/.config/prim`). Use when the user has a long-lived token in hand.
|
|
24
24
|
3. **`npx --yes @primitive.ai/prim auth login`** -- opens a browser via WorkOS OAuth. **Drive this yourself; do not hand it to the user.** It blocks up to 2 minutes waiting for approval -- that wait is expected, not a failure. The user's only action is clicking "Authorize". Run it in the background, surface the authorize URL it prints on STDERR so the user can click it if the browser didn't open, then poll `auth status` until it exits 0. If it times out before they click, run it again -- never fall back to asking them to run it.
|
|
25
25
|
|
|
26
26
|
The CLI auto-refreshes a still-valid session from the stored refresh token (proactively ~60s before expiry, and again on a 401), so a short access-token "expires in 2m" is normal -- not a reason to re-authenticate or warn the user. Only an absent refresh token, or an explicit `Authentication expired. Run prim auth login to re-authenticate.`, warrants a re-login -- which you then drive yourself, per the above. Relay that message if it appears.
|
|
@@ -1,34 +1,68 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
binFile,
|
|
3
|
+
compareSemver,
|
|
4
|
+
pinnedNpxCommand
|
|
5
|
+
} from "./chunk-4A2XB2PC.js";
|
|
6
|
+
import {
|
|
3
7
|
gitToplevel,
|
|
4
8
|
githubRepositoryFullName,
|
|
5
9
|
isValidRepoSyncId,
|
|
6
|
-
setRepoSyncId
|
|
7
|
-
|
|
10
|
+
setRepoSyncId,
|
|
11
|
+
setRepositoryBindingState
|
|
12
|
+
} from "./chunk-TJTVHDJ5.js";
|
|
8
13
|
import {
|
|
9
14
|
HttpError,
|
|
15
|
+
atomicWriteFile,
|
|
10
16
|
getClient,
|
|
11
17
|
withFileLock
|
|
12
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-Z7YPFWAZ.js";
|
|
19
|
+
|
|
20
|
+
// src/lib/bin-cache.ts
|
|
21
|
+
import { chmodSync, mkdirSync } from "fs";
|
|
22
|
+
import { homedir } from "os";
|
|
23
|
+
import { join } from "path";
|
|
24
|
+
var GIT_HOOK_CACHE_SHELL_DIR = "${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin";
|
|
25
|
+
var GIT_HOOK_CACHE_TTL_MINUTES = 1440;
|
|
26
|
+
var GIT_HOOK_CACHED_BINS = ["prim-post-commit", "prim-post-rewrite"];
|
|
27
|
+
function binCacheDir() {
|
|
28
|
+
const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
|
|
29
|
+
return join(base, "prim", "bin");
|
|
30
|
+
}
|
|
31
|
+
function writeAtomic(path, content) {
|
|
32
|
+
atomicWriteFile(path, content, { mode: 384 });
|
|
33
|
+
}
|
|
34
|
+
function warmBinCache() {
|
|
35
|
+
try {
|
|
36
|
+
if (process.env.PRIM_BIN_CACHE === "0") {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const dir = binCacheDir();
|
|
40
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
41
|
+
chmodSync(dir, 448);
|
|
42
|
+
writeAtomic(join(dir, "node"), process.execPath);
|
|
43
|
+
for (const bin of GIT_HOOK_CACHED_BINS) {
|
|
44
|
+
const file = binFile(bin);
|
|
45
|
+
if (file) {
|
|
46
|
+
writeAtomic(join(dir, bin), file);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
13
52
|
|
|
14
53
|
// src/commands/skill.ts
|
|
15
|
-
import { randomUUID } from "crypto";
|
|
16
54
|
import {
|
|
17
55
|
constants as constants2,
|
|
18
56
|
closeSync as closeSync2,
|
|
19
57
|
existsSync as existsSync2,
|
|
20
58
|
fstatSync as fstatSync2,
|
|
21
|
-
fsyncSync,
|
|
22
59
|
lstatSync as lstatSync2,
|
|
23
60
|
openSync as openSync2,
|
|
24
61
|
readFileSync as readFileSync2,
|
|
25
|
-
readSync as readSync2
|
|
26
|
-
renameSync,
|
|
27
|
-
rmSync as rmSync2,
|
|
28
|
-
writeFileSync
|
|
62
|
+
readSync as readSync2
|
|
29
63
|
} from "fs";
|
|
30
|
-
import { homedir as
|
|
31
|
-
import { dirname as dirname2, join as
|
|
64
|
+
import { homedir as homedir3 } from "os";
|
|
65
|
+
import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
|
|
32
66
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
33
67
|
import { createPatch } from "diff";
|
|
34
68
|
import { parse as parseYaml2 } from "yaml";
|
|
@@ -46,7 +80,7 @@ import {
|
|
|
46
80
|
existsSync,
|
|
47
81
|
fstatSync,
|
|
48
82
|
lstatSync,
|
|
49
|
-
mkdirSync,
|
|
83
|
+
mkdirSync as mkdirSync2,
|
|
50
84
|
openSync,
|
|
51
85
|
readFileSync,
|
|
52
86
|
readSync,
|
|
@@ -55,49 +89,10 @@ import {
|
|
|
55
89
|
rmSync,
|
|
56
90
|
rmdirSync
|
|
57
91
|
} from "fs";
|
|
58
|
-
import { homedir } from "os";
|
|
59
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
92
|
+
import { homedir as homedir2 } from "os";
|
|
93
|
+
import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
60
94
|
import { fileURLToPath } from "url";
|
|
61
95
|
import { parse as parseYaml } from "yaml";
|
|
62
|
-
|
|
63
|
-
// src/lib/semver.ts
|
|
64
|
-
function parseSemver(value) {
|
|
65
|
-
if (typeof value !== "string" || value.length > 256) return;
|
|
66
|
-
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(
|
|
67
|
-
value
|
|
68
|
-
);
|
|
69
|
-
if (!match) return;
|
|
70
|
-
const prerelease = match[4]?.split(".") ?? [];
|
|
71
|
-
if (prerelease.some((part) => /^\d+$/u.test(part) && part.length > 1 && part.startsWith("0"))) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
return { core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])], prerelease };
|
|
75
|
-
}
|
|
76
|
-
function compareSemver(left, right) {
|
|
77
|
-
const a = parseSemver(left);
|
|
78
|
-
const b = parseSemver(right);
|
|
79
|
-
if (!a || !b) return;
|
|
80
|
-
for (let i = 0; i < a.core.length; i += 1) {
|
|
81
|
-
if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
|
|
82
|
-
}
|
|
83
|
-
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
84
|
-
return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
|
|
85
|
-
}
|
|
86
|
-
for (let i = 0; i < Math.max(a.prerelease.length, b.prerelease.length); i += 1) {
|
|
87
|
-
const x = a.prerelease[i];
|
|
88
|
-
const y = b.prerelease[i];
|
|
89
|
-
if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
|
|
90
|
-
if (x === y) continue;
|
|
91
|
-
const xNumeric = /^\d+$/u.test(x);
|
|
92
|
-
const yNumeric = /^\d+$/u.test(y);
|
|
93
|
-
if (xNumeric && yNumeric) return BigInt(x) < BigInt(y) ? -1 : 1;
|
|
94
|
-
if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
|
|
95
|
-
return x < y ? -1 : 1;
|
|
96
|
-
}
|
|
97
|
-
return 0;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// src/commands/claude-plugin.ts
|
|
101
96
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
102
97
|
var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
|
|
103
98
|
var MAX_MANIFEST_BYTES = 64 * 1024;
|
|
@@ -105,8 +100,8 @@ var MAX_SKILL_BYTES = 1024 * 1024;
|
|
|
105
100
|
var REFRESH_LOCK_TIMEOUT_MS = 150;
|
|
106
101
|
var REFRESH_LOCK_POLL_MS = 10;
|
|
107
102
|
function pluginDirFor(cwd, scope) {
|
|
108
|
-
const base = scope === "user" ?
|
|
109
|
-
return
|
|
103
|
+
const base = scope === "user" ? join2(homedir2(), ".claude") : join2(gitToplevel(cwd) ?? cwd, ".claude");
|
|
104
|
+
return join2(base, "skills", "prim");
|
|
110
105
|
}
|
|
111
106
|
function resolvePluginDir(cwd, scope) {
|
|
112
107
|
if (scope && scope !== "user" && scope !== "project") {
|
|
@@ -138,8 +133,8 @@ function removeDirIfEmpty(dir) {
|
|
|
138
133
|
}
|
|
139
134
|
function pluginPaths(dir) {
|
|
140
135
|
return {
|
|
141
|
-
manifestPath:
|
|
142
|
-
skillPath:
|
|
136
|
+
manifestPath: join2(dir, ".claude-plugin", "plugin.json"),
|
|
137
|
+
skillPath: join2(dir, "SKILL.md")
|
|
143
138
|
};
|
|
144
139
|
}
|
|
145
140
|
function pathIsInside(root, path) {
|
|
@@ -204,9 +199,9 @@ function inspectRecognizedInstallation(paths, projectRoot) {
|
|
|
204
199
|
};
|
|
205
200
|
}
|
|
206
201
|
function refreshLockDir(physicalPluginDir) {
|
|
207
|
-
const cacheRoot = process.env.XDG_CACHE_HOME ||
|
|
202
|
+
const cacheRoot = process.env.XDG_CACHE_HOME || join2(homedir2(), ".cache");
|
|
208
203
|
const identity = createHash("sha256").update(physicalPluginDir).digest("hex");
|
|
209
|
-
return
|
|
204
|
+
return join2(cacheRoot, "prim", "skill-refresh-locks", `${identity}.lock`);
|
|
210
205
|
}
|
|
211
206
|
async function refreshClaudePlugins(cwd, options = {}) {
|
|
212
207
|
const result = { installed: 0, refreshed: 0 };
|
|
@@ -214,7 +209,7 @@ async function refreshClaudePlugins(cwd, options = {}) {
|
|
|
214
209
|
if (options.includeProject !== false) {
|
|
215
210
|
const projectRoot = options.projectRoot ?? gitToplevel(cwd);
|
|
216
211
|
if (projectRoot !== null && isAbsolute(projectRoot)) {
|
|
217
|
-
candidates.push({ dir:
|
|
212
|
+
candidates.push({ dir: join2(projectRoot, ".claude", "skills", "prim"), projectRoot });
|
|
218
213
|
}
|
|
219
214
|
}
|
|
220
215
|
candidates.push({ dir: pluginDirFor(cwd, "user") });
|
|
@@ -290,7 +285,7 @@ function installClaudePlugin(cwd, opts) {
|
|
|
290
285
|
console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
|
|
291
286
|
return 0;
|
|
292
287
|
}
|
|
293
|
-
|
|
288
|
+
mkdirSync2(join2(dir, ".claude-plugin"), { recursive: true });
|
|
294
289
|
atomicWrite(manifestPath, manifest);
|
|
295
290
|
atomicWrite(skillPath, skill);
|
|
296
291
|
console.log(`Installed prim skill plugin at ${dir}`);
|
|
@@ -310,7 +305,7 @@ function uninstallClaudePlugin(cwd, opts) {
|
|
|
310
305
|
}
|
|
311
306
|
rmSync(manifestPath, { force: true });
|
|
312
307
|
rmSync(skillPath, { force: true });
|
|
313
|
-
removeDirIfEmpty(
|
|
308
|
+
removeDirIfEmpty(join2(dir, ".claude-plugin"));
|
|
314
309
|
removeDirIfEmpty(dir);
|
|
315
310
|
console.log(`Removed prim skill plugin from ${dir}`);
|
|
316
311
|
return 0;
|
|
@@ -365,10 +360,10 @@ function readCodexGuidance(path) {
|
|
|
365
360
|
}
|
|
366
361
|
}
|
|
367
362
|
function effectiveCodexGuidance(scope) {
|
|
368
|
-
const override = readCodexGuidance(
|
|
363
|
+
const override = readCodexGuidance(join3(scope, "AGENTS.override.md"));
|
|
369
364
|
if (override === null) return null;
|
|
370
365
|
if (override !== void 0 && override.toString("utf8").trim().length > 0) return override;
|
|
371
|
-
const base = readCodexGuidance(
|
|
366
|
+
const base = readCodexGuidance(join3(scope, "AGENTS.md"));
|
|
372
367
|
return base ?? null;
|
|
373
368
|
}
|
|
374
369
|
function hasUsablePrimBlock(content, visibleBytes) {
|
|
@@ -391,7 +386,7 @@ function hasUsablePrimBlock(content, visibleBytes) {
|
|
|
391
386
|
}
|
|
392
387
|
function hasUsableCodexGuidance(projectRoot) {
|
|
393
388
|
try {
|
|
394
|
-
const codexHome = process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) :
|
|
389
|
+
const codexHome = process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) : join3(homedir3(), ".codex");
|
|
395
390
|
const scopes = [
|
|
396
391
|
{ path: codexHome, visibleBytes: CODEX_GUIDANCE_LIMIT_BYTES },
|
|
397
392
|
{ path: projectRoot, visibleBytes: CODEX_DEFAULT_PROJECT_GUIDANCE_BYTES }
|
|
@@ -419,10 +414,10 @@ var AGENT_TARGET = {
|
|
|
419
414
|
hermes: ".hermes.md"
|
|
420
415
|
};
|
|
421
416
|
function userTargetFor(agent) {
|
|
422
|
-
if (agent === "claude") return
|
|
423
|
-
if (agent === "codex") return
|
|
417
|
+
if (agent === "claude") return join3(homedir3(), ".claude", "CLAUDE.md");
|
|
418
|
+
if (agent === "codex") return join3(homedir3(), ".codex", "AGENTS.md");
|
|
424
419
|
if (agent === "hermes") {
|
|
425
|
-
return
|
|
420
|
+
return join3(process.env.HERMES_HOME ?? join3(homedir3(), ".hermes"), ".hermes.md");
|
|
426
421
|
}
|
|
427
422
|
return null;
|
|
428
423
|
}
|
|
@@ -463,27 +458,7 @@ function removeBlock(existing) {
|
|
|
463
458
|
return out.replace(/(\r?\n){2,}$/, "$1");
|
|
464
459
|
}
|
|
465
460
|
function atomicWrite(target, content) {
|
|
466
|
-
|
|
467
|
-
let temporaryCreated = false;
|
|
468
|
-
try {
|
|
469
|
-
const fd = openSync2(tmp, "wx");
|
|
470
|
-
temporaryCreated = true;
|
|
471
|
-
try {
|
|
472
|
-
writeFileSync(fd, content);
|
|
473
|
-
fsyncSync(fd);
|
|
474
|
-
} finally {
|
|
475
|
-
closeSync2(fd);
|
|
476
|
-
}
|
|
477
|
-
renameSync(tmp, target);
|
|
478
|
-
} catch (error) {
|
|
479
|
-
if (temporaryCreated) {
|
|
480
|
-
try {
|
|
481
|
-
rmSync2(tmp, { force: true });
|
|
482
|
-
} catch {
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
throw error;
|
|
486
|
-
}
|
|
461
|
+
atomicWriteFile(target, content);
|
|
487
462
|
}
|
|
488
463
|
function resolveTarget(cwd, opts) {
|
|
489
464
|
if (opts.scope && opts.scope !== "user" && opts.scope !== "project") {
|
|
@@ -597,20 +572,20 @@ function registerSkillCommands(program) {
|
|
|
597
572
|
|
|
598
573
|
// src/lib/post-commit-hook.ts
|
|
599
574
|
import { execFileSync } from "child_process";
|
|
600
|
-
import { createHash as createHash2, randomUUID
|
|
575
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
601
576
|
import {
|
|
602
577
|
constants as constants3,
|
|
603
578
|
closeSync as closeSync3,
|
|
604
579
|
existsSync as existsSync3,
|
|
605
580
|
fchmodSync,
|
|
606
|
-
fsyncSync
|
|
581
|
+
fsyncSync,
|
|
607
582
|
lstatSync as lstatSync3,
|
|
608
|
-
mkdirSync as
|
|
583
|
+
mkdirSync as mkdirSync3,
|
|
609
584
|
openSync as openSync3,
|
|
610
585
|
readFileSync as readFileSync3,
|
|
611
|
-
renameSync
|
|
586
|
+
renameSync,
|
|
612
587
|
unlinkSync,
|
|
613
|
-
writeFileSync
|
|
588
|
+
writeFileSync
|
|
614
589
|
} from "fs";
|
|
615
590
|
import { basename, dirname as dirname3, isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
|
|
616
591
|
var PRIM_POST_COMMIT_BLOCK_START = "# >>> prim post-commit hook >>>";
|
|
@@ -656,9 +631,9 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
|
656
631
|
if [ -n "$prim_commit_sha" ]; then
|
|
657
632
|
prim_commit_observed_file=$(mktemp "\${TMPDIR:-/tmp}/prim-post-commit-observed.XXXXXXXX" 2>/dev/null) || prim_commit_observed_file=
|
|
658
633
|
prim_commit_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || prim_commit_branch=
|
|
659
|
-
prim_cache_dir="
|
|
634
|
+
prim_cache_dir="${GIT_HOOK_CACHE_SHELL_DIR}"
|
|
660
635
|
prim_post_commit_ran=0
|
|
661
|
-
if [ "\${PRIM_BIN_CACHE:-1}" != "0" ] && [ -f "$prim_cache_dir/prim-post-commit" ] && [ -f "$prim_cache_dir/node" ] && [ -n "$(find "$prim_cache_dir/prim-post-commit" -mmin "-\${PRIM_BIN_CACHE_TTL_MIN
|
|
636
|
+
if [ "\${PRIM_BIN_CACHE:-1}" != "0" ] && [ -f "$prim_cache_dir/prim-post-commit" ] && [ -f "$prim_cache_dir/node" ] && [ -n "$(find "$prim_cache_dir/prim-post-commit" -mmin "-\${PRIM_BIN_CACHE_TTL_MIN:-${GIT_HOOK_CACHE_TTL_MINUTES}}" 2>/dev/null)" ]; then
|
|
662
637
|
prim_node=$(cat "$prim_cache_dir/node")
|
|
663
638
|
prim_entry=$(cat "$prim_cache_dir/prim-post-commit")
|
|
664
639
|
if [ -x "$prim_node" ] && [ -f "$prim_entry" ]; then
|
|
@@ -666,14 +641,9 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
|
666
641
|
prim_post_commit_ran=1
|
|
667
642
|
fi
|
|
668
643
|
fi
|
|
669
|
-
if [ "$prim_post_commit_ran" -eq 0 ]; then
|
|
670
|
-
if [ -
|
|
671
|
-
|
|
672
|
-
prim_post_commit_ran=1
|
|
673
|
-
elif command -v npx >/dev/null 2>&1; then
|
|
674
|
-
( trap '' HUP; trap 'if [ -n "$prim_commit_observed_file" ]; then rm -f "$prim_commit_observed_file"; fi' 0; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch" PRIM_COMMIT_OBSERVED_FILE="$prim_commit_observed_file"; npx --yes -p @primitive.ai/prim@latest prim-post-commit ) </dev/null >/dev/null 2>&1 &
|
|
675
|
-
prim_post_commit_ran=1
|
|
676
|
-
fi
|
|
644
|
+
if [ "$prim_post_commit_ran" -eq 0 ] && command -v npx >/dev/null 2>&1; then
|
|
645
|
+
( trap '' HUP; trap 'if [ -n "$prim_commit_observed_file" ]; then rm -f "$prim_commit_observed_file"; fi' 0; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch" PRIM_COMMIT_OBSERVED_FILE="$prim_commit_observed_file"; ${pinnedNpxCommand("prim-post-commit")} ) </dev/null >/dev/null 2>&1 &
|
|
646
|
+
prim_post_commit_ran=1
|
|
677
647
|
fi
|
|
678
648
|
if [ "$prim_post_commit_ran" -eq 0 ] && [ -n "$prim_commit_observed_file" ]; then
|
|
679
649
|
rm -f "$prim_commit_observed_file"
|
|
@@ -701,9 +671,9 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
|
701
671
|
if cat > "$prim_rewrite_pairs_file"; then
|
|
702
672
|
exec < "$prim_rewrite_pairs_file"
|
|
703
673
|
prim_rewrite_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || prim_rewrite_branch=
|
|
704
|
-
prim_cache_dir="
|
|
674
|
+
prim_cache_dir="${GIT_HOOK_CACHE_SHELL_DIR}"
|
|
705
675
|
prim_post_rewrite_ran=0
|
|
706
|
-
if [ "\${PRIM_BIN_CACHE:-1}" != "0" ] && [ -f "$prim_cache_dir/prim-post-rewrite" ] && [ -f "$prim_cache_dir/node" ] && [ -n "$(find "$prim_cache_dir/prim-post-rewrite" -mmin "-\${PRIM_BIN_CACHE_TTL_MIN
|
|
676
|
+
if [ "\${PRIM_BIN_CACHE:-1}" != "0" ] && [ -f "$prim_cache_dir/prim-post-rewrite" ] && [ -f "$prim_cache_dir/node" ] && [ -n "$(find "$prim_cache_dir/prim-post-rewrite" -mmin "-\${PRIM_BIN_CACHE_TTL_MIN:-${GIT_HOOK_CACHE_TTL_MINUTES}}" 2>/dev/null)" ]; then
|
|
707
677
|
prim_node=$(cat "$prim_cache_dir/node")
|
|
708
678
|
prim_entry=$(cat "$prim_cache_dir/prim-post-rewrite")
|
|
709
679
|
if [ -x "$prim_node" ] && [ -f "$prim_entry" ]; then
|
|
@@ -711,14 +681,9 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
|
711
681
|
prim_post_rewrite_ran=1
|
|
712
682
|
fi
|
|
713
683
|
fi
|
|
714
|
-
if [ "$prim_post_rewrite_ran" -eq 0 ]; then
|
|
715
|
-
if [ -
|
|
716
|
-
|
|
717
|
-
prim_post_rewrite_ran=1
|
|
718
|
-
elif command -v npx >/dev/null 2>&1; then
|
|
719
|
-
( trap '' HUP; trap 'if [ -n "$prim_rewrite_pairs_file" ]; then rm -f "$prim_rewrite_pairs_file"; fi' 0; export PRIM_REWRITE_SOURCE="$prim_rewrite_source" PRIM_REWRITE_BRANCH="$prim_rewrite_branch" PRIM_REWRITE_PAIRS_FILE="$prim_rewrite_pairs_file"; npx --yes -p @primitive.ai/prim@latest prim-post-rewrite ) </dev/null >/dev/null 2>&1 &
|
|
720
|
-
prim_post_rewrite_ran=1
|
|
721
|
-
fi
|
|
684
|
+
if [ "$prim_post_rewrite_ran" -eq 0 ] && command -v npx >/dev/null 2>&1; then
|
|
685
|
+
( trap '' HUP; trap 'if [ -n "$prim_rewrite_pairs_file" ]; then rm -f "$prim_rewrite_pairs_file"; fi' 0; export PRIM_REWRITE_SOURCE="$prim_rewrite_source" PRIM_REWRITE_BRANCH="$prim_rewrite_branch" PRIM_REWRITE_PAIRS_FILE="$prim_rewrite_pairs_file"; ${pinnedNpxCommand("prim-post-rewrite")} ) </dev/null >/dev/null 2>&1 &
|
|
686
|
+
prim_post_rewrite_ran=1
|
|
722
687
|
fi
|
|
723
688
|
if [ "$prim_post_rewrite_ran" -eq 0 ]; then
|
|
724
689
|
rm -f "$prim_rewrite_pairs_file"
|
|
@@ -1026,10 +991,10 @@ function assertTargetUnchanged(path, expected, spec, stat) {
|
|
|
1026
991
|
}
|
|
1027
992
|
function rewriteHookAtomically(path, next, expected, spec, stat) {
|
|
1028
993
|
const parent = dirname3(path);
|
|
1029
|
-
|
|
994
|
+
mkdirSync3(parent, { recursive: true });
|
|
1030
995
|
const temporary = resolve3(
|
|
1031
996
|
parent,
|
|
1032
|
-
`.${basename(path)}.prim-${String(process.pid)}-${
|
|
997
|
+
`.${basename(path)}.prim-${String(process.pid)}-${randomUUID()}.tmp`
|
|
1033
998
|
);
|
|
1034
999
|
let fd;
|
|
1035
1000
|
try {
|
|
@@ -1038,13 +1003,13 @@ function rewriteHookAtomically(path, next, expected, spec, stat) {
|
|
|
1038
1003
|
constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY | constants3.O_NOFOLLOW,
|
|
1039
1004
|
stat?.mode ?? 493
|
|
1040
1005
|
);
|
|
1041
|
-
|
|
1006
|
+
writeFileSync(fd, next);
|
|
1042
1007
|
fchmodSync(fd, (stat?.mode ?? 493) & 4095);
|
|
1043
|
-
|
|
1008
|
+
fsyncSync(fd);
|
|
1044
1009
|
closeSync3(fd);
|
|
1045
1010
|
fd = void 0;
|
|
1046
1011
|
assertTargetUnchanged(path, expected, spec, stat);
|
|
1047
|
-
|
|
1012
|
+
renameSync(temporary, path);
|
|
1048
1013
|
} finally {
|
|
1049
1014
|
if (fd !== void 0) {
|
|
1050
1015
|
try {
|
|
@@ -1353,40 +1318,66 @@ function uninstallPostRewriteHookAtPath(hookPath) {
|
|
|
1353
1318
|
}
|
|
1354
1319
|
|
|
1355
1320
|
// src/lib/repository-binding.ts
|
|
1356
|
-
|
|
1321
|
+
var InvalidRepositoryBindingResponseError = class extends Error {
|
|
1322
|
+
};
|
|
1323
|
+
async function resolveRepositoryBindingWithClient(root, client, options) {
|
|
1357
1324
|
const repositoryFullName = githubRepositoryFullName(root);
|
|
1358
1325
|
if (!repositoryFullName) {
|
|
1359
1326
|
throw new Error("origin must be a GitHub HTTPS/SSH remote in owner/name form");
|
|
1360
1327
|
}
|
|
1361
1328
|
let response;
|
|
1362
1329
|
try {
|
|
1363
|
-
response = await
|
|
1330
|
+
response = await client.post(
|
|
1364
1331
|
"/api/cli/repositories/bind",
|
|
1365
1332
|
{ repositoryFullName },
|
|
1366
1333
|
options
|
|
1367
1334
|
);
|
|
1368
1335
|
} catch (error) {
|
|
1369
1336
|
if (error instanceof HttpError && error.status === 404) {
|
|
1370
|
-
return { status: "
|
|
1337
|
+
return { status: "unbound", repositoryFullName };
|
|
1371
1338
|
}
|
|
1372
1339
|
throw error;
|
|
1373
1340
|
}
|
|
1374
1341
|
if (!isValidRepoSyncId(response.repoSyncId)) {
|
|
1375
|
-
throw new
|
|
1342
|
+
throw new InvalidRepositoryBindingResponseError(
|
|
1343
|
+
"server returned an invalid repository binding"
|
|
1344
|
+
);
|
|
1376
1345
|
}
|
|
1377
1346
|
return { status: "connected", repoSyncId: response.repoSyncId, repositoryFullName };
|
|
1378
1347
|
}
|
|
1379
|
-
async function
|
|
1380
|
-
|
|
1348
|
+
async function resolveRepositoryBinding(root, options) {
|
|
1349
|
+
return resolveRepositoryBindingWithClient(root, getClient(), options);
|
|
1350
|
+
}
|
|
1351
|
+
function persistRepositoryBinding(root, binding) {
|
|
1381
1352
|
if (binding.status === "connected") {
|
|
1382
|
-
setRepoSyncId(root, binding.repoSyncId);
|
|
1383
|
-
|
|
1384
|
-
|
|
1353
|
+
setRepoSyncId(root, binding.repoSyncId, binding.repositoryFullName);
|
|
1354
|
+
setRepositoryBindingState(root, "connected");
|
|
1355
|
+
return;
|
|
1385
1356
|
}
|
|
1357
|
+
setRepositoryBindingState(root, "unbound");
|
|
1358
|
+
}
|
|
1359
|
+
async function bindRepositoryWithClient(root, client, options) {
|
|
1360
|
+
let binding;
|
|
1361
|
+
try {
|
|
1362
|
+
binding = await resolveRepositoryBindingWithClient(root, client, options);
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
if (error instanceof InvalidRepositoryBindingResponseError) {
|
|
1365
|
+
try {
|
|
1366
|
+
setRepositoryBindingState(root, "invalid");
|
|
1367
|
+
} catch {
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
throw error;
|
|
1371
|
+
}
|
|
1372
|
+
persistRepositoryBinding(root, binding);
|
|
1386
1373
|
return binding;
|
|
1387
1374
|
}
|
|
1375
|
+
async function bindRepository(root, options) {
|
|
1376
|
+
return bindRepositoryWithClient(root, getClient(), options);
|
|
1377
|
+
}
|
|
1388
1378
|
|
|
1389
1379
|
export {
|
|
1380
|
+
warmBinCache,
|
|
1390
1381
|
postCommitHookBlock,
|
|
1391
1382
|
postRewriteHookBlock,
|
|
1392
1383
|
ensureEffectivePostCommitHook,
|
|
@@ -1400,9 +1391,9 @@ export {
|
|
|
1400
1391
|
uninstallPostCommitHookAtPath,
|
|
1401
1392
|
uninstallPostRewriteHookAtPath,
|
|
1402
1393
|
resolveRepositoryBinding,
|
|
1394
|
+
bindRepositoryWithClient,
|
|
1403
1395
|
bindRepository,
|
|
1404
1396
|
printJson,
|
|
1405
|
-
compareSemver,
|
|
1406
1397
|
refreshClaudePlugins,
|
|
1407
1398
|
hasUsableCodexGuidance,
|
|
1408
1399
|
registerSkillCommands
|