@primitive.ai/prim 0.1.0-alpha.17 → 0.1.0-alpha.19
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/SKILL.md +31 -1
- package/dist/{chunk-LCC66K45.js → chunk-6LAQVM26.js} +0 -33
- package/dist/chunk-7GHOFNJ2.js +57 -0
- package/dist/hooks/post-commit.js +71 -0
- package/dist/hooks/post-tool-use.js +4 -2
- package/dist/hooks/prim-hook.js +4 -2
- package/dist/index.js +341 -221
- package/package.json +5 -4
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prim
|
|
3
|
-
description: Use the prim CLI for
|
|
3
|
+
description: Use the prim CLI for Primitive specs, contexts, projects, pre-commit hooks, and the decision graph (passive decision capture, the conflict gate, reconcile, and team presence). TRIGGER when the user mentions Primitive, prim, "specs" or "contexts" (in the Primitive sense), or decisions / the decision graph / a conflict gate / reconcile; when the repo's package.json depends on @primitive.ai/prim; when the user asks to sync, map, update, or auto-map a spec; when an edit is denied or warned by a prior decision; when configuring Primitive hooks. SKIP when "spec" means test specs (vitest, jest, rspec), when "context" means React context or an LLM context window, or for unrelated CLIs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Working with the prim CLI
|
|
@@ -35,6 +35,36 @@ The CLI auto-refreshes expired tokens. On unrecoverable expiry it throws `Authen
|
|
|
35
35
|
2. Every command accepts `--help`. When unsure of flags, run `npx --yes @primitive.ai/prim <cmd> --help` rather than guessing.
|
|
36
36
|
3. The CLI prints API errors as one-liners to stderr and exits non-zero. Treat any non-zero exit as actionable. If a command fails with an unrecognized error, re-run with `--help` to check your flags. If auth-related, re-check `auth status`.
|
|
37
37
|
|
|
38
|
+
## Working with the decision graph
|
|
39
|
+
|
|
40
|
+
Separate from specs, prim passively captures the decisions you make during a coding session -- which library, which pattern, which config value -- into a queryable decision graph, and actively **gates** edits that would conflict with a load-bearing prior decision. Capture and the gate run automatically through the session hooks installed by `npx --yes @primitive.ai/prim claude install` (Claude Code) or `npx --yes @primitive.ai/prim codex install` (Codex). You never invoke capture; you *respond* to the gate and *read* the graph.
|
|
41
|
+
|
|
42
|
+
### Heed the conflict gate
|
|
43
|
+
Before an edit (Claude Code: Edit/Write/MultiEdit; Codex: apply_patch) a PreToolUse hook scores the target file against the graph:
|
|
44
|
+
|
|
45
|
+
- **deny** -- the edit is blocked: it conflicts with a load-bearing prior decision. Don't fight it. Read the reason line; it names the decision id. If you genuinely intend to override that decision, run `npx --yes @primitive.ai/prim reconcile dec_<shortId>`, then retry the edit once. Otherwise choose an approach that respects the decision.
|
|
46
|
+
- **warn / additional context** -- the edit proceeds, but a relevant prior decision is surfaced. Read it. On Codex a would-be `ask` is delivered as allow-plus-context (Codex can't pause mid-tool), so that context is your only signal -- read it before continuing.
|
|
47
|
+
- **"decision check skipped / not verified" or "... partial / truncated"** -- the check could not fully run. Treat constraints as UNKNOWN, not clear; never read silence as approval.
|
|
48
|
+
|
|
49
|
+
The gate fail-opens on its *own* infrastructure errors (no daemon, network blip, org-unbound token) -- a setup problem never blocks your edit. That is exactly why an "unavailable" note matters: it is the honest signal that the check, not your edit, is what failed.
|
|
50
|
+
|
|
51
|
+
### Read the graph before large or load-bearing edits
|
|
52
|
+
- `npx --yes @primitive.ai/prim decisions check --files "src/a.ts,src/b.ts"` -- which active decisions reference the files you're about to touch (comma-separated paths, one `--files` value). Run it before a big change.
|
|
53
|
+
- `npx --yes @primitive.ai/prim decisions recent` -- the team's recent decisions, each row badged by author and agent (`Your Claude Code` / `Your Codex`); `--limit <n>` and `--since <dur>` narrow it.
|
|
54
|
+
- `npx --yes @primitive.ai/prim decisions show <idOrShortId>` and `npx --yes @primitive.ai/prim decisions cascade <idOrShortId>` -- full detail, and the downstream blast radius a change would disturb.
|
|
55
|
+
|
|
56
|
+
### Reconcile and the verdict footer
|
|
57
|
+
`npx --yes @primitive.ai/prim reconcile <idOrShortId>` mints a single-use bypass for the named decision -- it prints `[prim] reconcile bypass issued for dec_<short> (expires in ...)` to STDERR, with the bypass JSON on STDOUT. Your *next* edit to the governed file then goes through, and on that edit prim prints a verdict footer to STDERR -- confirmation the override was recorded, not silently dropped:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
✓ Conflict caught before merge · N decisions saved · <author>'s intent preserved
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`N` is the reconciled decision's downstream live-dependent count, shown as `N+` when the server caps it.
|
|
64
|
+
|
|
65
|
+
### Presence
|
|
66
|
+
With the daemon running (`npx --yes @primitive.ai/prim daemon start`), `npx --yes @primitive.ai/prim daemon status` includes the live online count in its STDOUT JSON (when presence is fresh); Claude Code surfaces it in the statusline as `team: N online`. Your captured decisions are attributed to your agent automatically -- no flag required.
|
|
67
|
+
|
|
38
68
|
## Common workflows
|
|
39
69
|
|
|
40
70
|
### Read a spec's current text (do this before any partial edit)
|
|
@@ -1,34 +1,3 @@
|
|
|
1
|
-
// src/hooks/prim-hook-core.ts
|
|
2
|
-
import { randomUUID } from "crypto";
|
|
3
|
-
import { platform } from "os";
|
|
4
|
-
|
|
5
|
-
// src/protocol/move.ts
|
|
6
|
-
var ENVELOPE_VERSION = 1;
|
|
7
|
-
|
|
8
|
-
// src/hooks/prim-hook-core.ts
|
|
9
|
-
function toMove(parsed, cliVersion, agent = "claude_code") {
|
|
10
|
-
return {
|
|
11
|
-
moveId: randomUUID(),
|
|
12
|
-
capturedAt: Date.now(),
|
|
13
|
-
sessionId: parsed.session_id ?? "",
|
|
14
|
-
eventType: parsed.hook_event_name ?? "unknown",
|
|
15
|
-
payload: parsed,
|
|
16
|
-
env: {
|
|
17
|
-
cwd: parsed.cwd ?? process.cwd(),
|
|
18
|
-
cliVersion,
|
|
19
|
-
osPlatform: platform()
|
|
20
|
-
},
|
|
21
|
-
envelopeVersion: ENVELOPE_VERSION,
|
|
22
|
-
// Stamp the producer only for Codex; Claude Code moves omit it (the
|
|
23
|
-
// backend defaults an absent value to "claude_code"), keeping the
|
|
24
|
-
// Claude wire shape byte-identical.
|
|
25
|
-
...agent === "codex" ? { producer: "codex" } : {}
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
function shouldFlushAfter(eventType) {
|
|
29
|
-
return eventType === "SessionEnd";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
1
|
// src/hooks/redact.ts
|
|
33
2
|
import { existsSync, readFileSync } from "fs";
|
|
34
3
|
import { join } from "path";
|
|
@@ -109,7 +78,5 @@ function scrubFromCwd(value, cwd) {
|
|
|
109
78
|
}
|
|
110
79
|
|
|
111
80
|
export {
|
|
112
|
-
toMove,
|
|
113
|
-
shouldFlushAfter,
|
|
114
81
|
scrubFromCwd
|
|
115
82
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/hooks/prim-hook-core.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { platform } from "os";
|
|
4
|
+
|
|
5
|
+
// src/protocol/move.ts
|
|
6
|
+
var ENVELOPE_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
// src/hooks/prim-hook-core.ts
|
|
9
|
+
function toMove(parsed, cliVersion, agent = "claude_code") {
|
|
10
|
+
return {
|
|
11
|
+
moveId: randomUUID(),
|
|
12
|
+
capturedAt: Date.now(),
|
|
13
|
+
sessionId: parsed.session_id ?? "",
|
|
14
|
+
eventType: parsed.hook_event_name ?? "unknown",
|
|
15
|
+
payload: parsed,
|
|
16
|
+
env: {
|
|
17
|
+
cwd: parsed.cwd ?? process.cwd(),
|
|
18
|
+
cliVersion,
|
|
19
|
+
osPlatform: platform()
|
|
20
|
+
},
|
|
21
|
+
envelopeVersion: ENVELOPE_VERSION,
|
|
22
|
+
// Stamp the producer only for Codex; Claude Code moves omit it (the
|
|
23
|
+
// backend defaults an absent value to "claude_code"), keeping the
|
|
24
|
+
// Claude wire shape byte-identical.
|
|
25
|
+
...agent === "codex" ? { producer: "codex" } : {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function toCommitMove(commit, cliVersion, cwd) {
|
|
29
|
+
return {
|
|
30
|
+
moveId: `commit:${commit.sha}`,
|
|
31
|
+
capturedAt: Date.now(),
|
|
32
|
+
sessionId: "",
|
|
33
|
+
eventType: "git.commit",
|
|
34
|
+
payload: {
|
|
35
|
+
kind: "git.commit",
|
|
36
|
+
sha: commit.sha,
|
|
37
|
+
parentSha: commit.parentSha,
|
|
38
|
+
branch: commit.branch,
|
|
39
|
+
files: commit.files
|
|
40
|
+
},
|
|
41
|
+
env: {
|
|
42
|
+
cwd,
|
|
43
|
+
cliVersion,
|
|
44
|
+
osPlatform: platform()
|
|
45
|
+
},
|
|
46
|
+
envelopeVersion: ENVELOPE_VERSION
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function shouldFlushAfter(eventType) {
|
|
50
|
+
return eventType === "SessionEnd";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
toMove,
|
|
55
|
+
toCommitMove,
|
|
56
|
+
shouldFlushAfter
|
|
57
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
appendMove,
|
|
4
|
+
resolveOrg
|
|
5
|
+
} from "../chunk-JZGWQDM5.js";
|
|
6
|
+
import {
|
|
7
|
+
toCommitMove
|
|
8
|
+
} from "../chunk-7GHOFNJ2.js";
|
|
9
|
+
|
|
10
|
+
// src/hooks/post-commit.ts
|
|
11
|
+
import { execSync, spawn } from "child_process";
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
function git(args) {
|
|
17
|
+
try {
|
|
18
|
+
return execSync(`git ${args}`, {
|
|
19
|
+
encoding: "utf-8",
|
|
20
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
21
|
+
}).trim();
|
|
22
|
+
} catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function readCommit() {
|
|
27
|
+
const sha = git("rev-parse HEAD");
|
|
28
|
+
if (!sha) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const branch = git("rev-parse --abbrev-ref HEAD");
|
|
32
|
+
const files = (git("diff-tree --no-commit-id --name-only -r -m --root HEAD") ?? "").split("\n").filter((f) => f.length > 0);
|
|
33
|
+
return {
|
|
34
|
+
sha,
|
|
35
|
+
parentSha: git("rev-parse --verify --quiet HEAD^") || void 0,
|
|
36
|
+
branch: branch && branch !== "HEAD" ? branch : void 0,
|
|
37
|
+
files
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function resolveCliVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const pkg = JSON.parse(readFileSync(join(here, "..", "..", "package.json"), "utf-8"));
|
|
43
|
+
return pkg.version ?? "unknown";
|
|
44
|
+
} catch {
|
|
45
|
+
return "unknown";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function spawnBackgroundFlush() {
|
|
49
|
+
const entry = join(here, "..", "index.js");
|
|
50
|
+
spawn(process.execPath, [entry, "moves", "flush"], {
|
|
51
|
+
detached: true,
|
|
52
|
+
stdio: "ignore"
|
|
53
|
+
}).unref();
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const commit = readCommit();
|
|
57
|
+
if (commit) {
|
|
58
|
+
const cwd = git("rev-parse --show-toplevel") ?? process.cwd();
|
|
59
|
+
const move = toCommitMove(commit, resolveCliVersion(), cwd);
|
|
60
|
+
const { orgId } = resolveOrg({ sessionId: "", cwd });
|
|
61
|
+
appendMove(move, orgId);
|
|
62
|
+
spawnBackgroundFlush();
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (process.env.PRIM_HOOK_DEBUG) {
|
|
66
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
67
|
+
process.stderr.write(`[prim-post-commit] capture failed: ${detail}
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
process.exit(0);
|
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
getClient
|
|
8
8
|
} from "../chunk-6SIEWWUL.js";
|
|
9
9
|
import {
|
|
10
|
-
scrubFromCwd
|
|
10
|
+
scrubFromCwd
|
|
11
|
+
} from "../chunk-6LAQVM26.js";
|
|
12
|
+
import {
|
|
11
13
|
toMove
|
|
12
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-7GHOFNJ2.js";
|
|
13
15
|
import {
|
|
14
16
|
parseAgent
|
|
15
17
|
} from "../chunk-7YRBACIE.js";
|
package/dist/hooks/prim-hook.js
CHANGED
|
@@ -4,10 +4,12 @@ import {
|
|
|
4
4
|
resolveOrg
|
|
5
5
|
} from "../chunk-JZGWQDM5.js";
|
|
6
6
|
import {
|
|
7
|
-
scrubFromCwd
|
|
7
|
+
scrubFromCwd
|
|
8
|
+
} from "../chunk-6LAQVM26.js";
|
|
9
|
+
import {
|
|
8
10
|
shouldFlushAfter,
|
|
9
11
|
toMove
|
|
10
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-7GHOFNJ2.js";
|
|
11
13
|
import {
|
|
12
14
|
parseAgent
|
|
13
15
|
} from "../chunk-7YRBACIE.js";
|
package/dist/index.js
CHANGED
|
@@ -34,9 +34,9 @@ import {
|
|
|
34
34
|
} from "./chunk-UTKQTZHL.js";
|
|
35
35
|
|
|
36
36
|
// src/index.ts
|
|
37
|
-
import { readFileSync as
|
|
38
|
-
import { dirname as
|
|
39
|
-
import { fileURLToPath as
|
|
37
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
38
|
+
import { dirname as dirname6, resolve as resolve4 } from "path";
|
|
39
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
40
40
|
import { Command } from "commander";
|
|
41
41
|
import updateNotifier from "update-notifier";
|
|
42
42
|
|
|
@@ -125,7 +125,8 @@ function registerAuthCommands(program2) {
|
|
|
125
125
|
res.end("<h1>Authentication successful!</h1><p>You can close this tab.</p>");
|
|
126
126
|
exchangeCode(siteUrl, code, verifier, `http://${LOCALHOST}:${port}/callback`).then((token) => {
|
|
127
127
|
saveToken(token);
|
|
128
|
-
console.
|
|
128
|
+
console.error(`Authenticated! Token saved to ${TOKEN_FILE_PATH}`);
|
|
129
|
+
console.log(JSON.stringify({ authenticated: true, tokenFile: TOKEN_FILE_PATH }));
|
|
129
130
|
server.close();
|
|
130
131
|
process.exit(0);
|
|
131
132
|
}).catch((err) => {
|
|
@@ -151,12 +152,12 @@ function registerAuthCommands(program2) {
|
|
|
151
152
|
authUrl.searchParams.set("state", state);
|
|
152
153
|
authUrl.searchParams.set("code_challenge", challenge);
|
|
153
154
|
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
154
|
-
console.
|
|
155
|
+
console.error("Opening browser for authentication...");
|
|
155
156
|
openBrowser(authUrl.toString());
|
|
156
|
-
console.
|
|
157
|
+
console.error(`If the browser doesn't open, visit:
|
|
157
158
|
${authUrl.toString()}
|
|
158
159
|
`);
|
|
159
|
-
console.
|
|
160
|
+
console.error("Waiting for callback...");
|
|
160
161
|
setTimeout(() => {
|
|
161
162
|
console.error("Authentication timed out.");
|
|
162
163
|
server.close();
|
|
@@ -281,33 +282,94 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
|
|
|
281
282
|
// src/commands/claude-install.ts
|
|
282
283
|
import {
|
|
283
284
|
closeSync,
|
|
284
|
-
existsSync as
|
|
285
|
+
existsSync as existsSync3,
|
|
285
286
|
fsyncSync,
|
|
286
287
|
mkdirSync as mkdirSync2,
|
|
287
288
|
openSync,
|
|
288
|
-
readFileSync as
|
|
289
|
+
readFileSync as readFileSync3,
|
|
289
290
|
renameSync,
|
|
290
291
|
writeFileSync as writeFileSync2
|
|
291
292
|
} from "fs";
|
|
292
293
|
import { homedir } from "os";
|
|
293
|
-
import { dirname as
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
var
|
|
294
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
295
|
+
|
|
296
|
+
// src/lib/bin-path.ts
|
|
297
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
298
|
+
import { dirname as dirname2, isAbsolute, join } from "path";
|
|
299
|
+
import { fileURLToPath } from "url";
|
|
300
|
+
var PKG_NAME = "@primitive.ai/prim";
|
|
301
|
+
var ROOT_WALK_LIMIT = 6;
|
|
302
|
+
var NPX_FALLBACK = `npx --yes -p ${PKG_NAME}@latest`;
|
|
303
|
+
var resolvedRoot;
|
|
304
|
+
function locateRoot() {
|
|
305
|
+
if (resolvedRoot !== void 0) {
|
|
306
|
+
return resolvedRoot;
|
|
307
|
+
}
|
|
308
|
+
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
309
|
+
for (let depth = 0; depth < ROOT_WALK_LIMIT; depth++) {
|
|
310
|
+
const manifestPath = join(dir, "package.json");
|
|
311
|
+
if (existsSync2(manifestPath)) {
|
|
312
|
+
try {
|
|
313
|
+
const manifest = JSON.parse(readFileSync2(manifestPath, "utf-8"));
|
|
314
|
+
if (manifest.name === PKG_NAME && manifest.bin) {
|
|
315
|
+
resolvedRoot = { dir, bin: manifest.bin };
|
|
316
|
+
return resolvedRoot;
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const parent = dirname2(dir);
|
|
322
|
+
if (parent === dir) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
dir = parent;
|
|
326
|
+
}
|
|
327
|
+
resolvedRoot = null;
|
|
328
|
+
return resolvedRoot;
|
|
329
|
+
}
|
|
330
|
+
function binFile(bin) {
|
|
331
|
+
const root = locateRoot();
|
|
332
|
+
const rel = root?.bin[bin];
|
|
333
|
+
if (!root || !rel) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
return isAbsolute(rel) ? rel : join(root.dir, rel);
|
|
337
|
+
}
|
|
338
|
+
function hookShimCommand(bin, args = "") {
|
|
339
|
+
const invoke = (cmd) => args ? `${cmd} ${args}` : cmd;
|
|
340
|
+
return `if command -v ${bin} >/dev/null 2>&1; then ${invoke(bin)}; elif [ -f "./node_modules/.bin/${bin}" ]; then ${invoke(`./node_modules/.bin/${bin}`)}; else ${invoke(`${NPX_FALLBACK} ${bin}`)}; fi`;
|
|
341
|
+
}
|
|
342
|
+
function commandMatchesBin(command, bin) {
|
|
343
|
+
if (!command) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
const c = command.trim();
|
|
347
|
+
if (c === bin || c.startsWith(`${bin} `)) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
return c.includes(`command -v ${bin} `);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/commands/claude-install.ts
|
|
354
|
+
var CAPTURE_BIN = "prim-hook";
|
|
355
|
+
var GATE_BIN = "prim-pre-tool-use";
|
|
356
|
+
var POST_TOOL_USE_BIN = "prim-post-tool-use";
|
|
357
|
+
var SESSION_START_BIN = "prim-session-start";
|
|
358
|
+
var SESSION_END_BIN = "prim-session-end";
|
|
359
|
+
var STATUSLINE_BIN = "prim";
|
|
360
|
+
var STATUSLINE_ARGS = "statusline";
|
|
361
|
+
var STATUSLINE_COMMAND = hookShimCommand(STATUSLINE_BIN, STATUSLINE_ARGS);
|
|
300
362
|
var STATUSLINE_REFRESH_SECONDS = 5;
|
|
301
|
-
var
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
]
|
|
363
|
+
var PRIM_BINS = [
|
|
364
|
+
CAPTURE_BIN,
|
|
365
|
+
GATE_BIN,
|
|
366
|
+
POST_TOOL_USE_BIN,
|
|
367
|
+
SESSION_START_BIN,
|
|
368
|
+
SESSION_END_BIN
|
|
369
|
+
];
|
|
308
370
|
var JSON_INDENT = 2;
|
|
309
|
-
var USER_SCOPE_PATH =
|
|
310
|
-
var PROJECT_SCOPE_PATH =
|
|
371
|
+
var USER_SCOPE_PATH = join2(homedir(), ".claude", "settings.json");
|
|
372
|
+
var PROJECT_SCOPE_PATH = join2(process.cwd(), ".claude", "settings.json");
|
|
311
373
|
var CAPTURE_EVENTS = [
|
|
312
374
|
"SessionStart",
|
|
313
375
|
"UserPromptSubmit",
|
|
@@ -317,21 +379,24 @@ var CAPTURE_EVENTS = [
|
|
|
317
379
|
"SessionEnd",
|
|
318
380
|
"SubagentStop"
|
|
319
381
|
];
|
|
382
|
+
function makeRegistration(event, matcher, bin, args = "") {
|
|
383
|
+
return { event, matcher, bin, command: hookShimCommand(bin, args) };
|
|
384
|
+
}
|
|
320
385
|
var REGISTRATIONS = [
|
|
321
|
-
...CAPTURE_EVENTS.map((event) => (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
386
|
+
...CAPTURE_EVENTS.map((event) => makeRegistration(event, "*", CAPTURE_BIN)),
|
|
387
|
+
makeRegistration("PreToolUse", "Edit|Write|MultiEdit", GATE_BIN),
|
|
388
|
+
makeRegistration("PostToolUse", "Edit|Write|MultiEdit", POST_TOOL_USE_BIN),
|
|
389
|
+
makeRegistration("SessionStart", "*", SESSION_START_BIN),
|
|
390
|
+
makeRegistration("SessionEnd", "*", SESSION_END_BIN)
|
|
326
391
|
];
|
|
327
392
|
function settingsPathFor(scope) {
|
|
328
393
|
return scope === "user" ? USER_SCOPE_PATH : PROJECT_SCOPE_PATH;
|
|
329
394
|
}
|
|
330
395
|
function readSettings(path) {
|
|
331
|
-
if (!
|
|
396
|
+
if (!existsSync3(path)) {
|
|
332
397
|
return {};
|
|
333
398
|
}
|
|
334
|
-
const raw =
|
|
399
|
+
const raw = readFileSync3(path, "utf-8");
|
|
335
400
|
try {
|
|
336
401
|
return JSON.parse(raw);
|
|
337
402
|
} catch (err) {
|
|
@@ -339,16 +404,16 @@ function readSettings(path) {
|
|
|
339
404
|
throw new Error(`${path} is not valid JSON: ${detail}`);
|
|
340
405
|
}
|
|
341
406
|
}
|
|
342
|
-
function entryHasCommand(entry,
|
|
343
|
-
return entry.hooks?.some((h) => h.command
|
|
407
|
+
function entryHasCommand(entry, bin) {
|
|
408
|
+
return entry.hooks?.some((h) => commandMatchesBin(h.command, bin)) ?? false;
|
|
344
409
|
}
|
|
345
410
|
function canonicalEntry(reg) {
|
|
346
411
|
return { matcher: reg.matcher, hooks: [{ type: "command", command: reg.command }] };
|
|
347
412
|
}
|
|
348
|
-
function stripCommand(list,
|
|
413
|
+
function stripCommand(list, bin) {
|
|
349
414
|
const out = [];
|
|
350
415
|
for (const e of list) {
|
|
351
|
-
const hooks = (e.hooks ?? []).filter((h) => h.command
|
|
416
|
+
const hooks = (e.hooks ?? []).filter((h) => !commandMatchesBin(h.command, bin));
|
|
352
417
|
if (hooks.length > 0) {
|
|
353
418
|
out.push({ ...e, hooks });
|
|
354
419
|
}
|
|
@@ -362,11 +427,16 @@ function ensureRegistration(list, reg, force) {
|
|
|
362
427
|
if (hasCanonical && !force) {
|
|
363
428
|
return list;
|
|
364
429
|
}
|
|
365
|
-
return [...stripCommand(list, reg.
|
|
430
|
+
return [...stripCommand(list, reg.bin), canonicalEntry(reg)];
|
|
366
431
|
}
|
|
432
|
+
var LEGACY_STATUSLINE_COMMAND = "prim statusline";
|
|
367
433
|
function isPrimStatusLine(settings) {
|
|
368
434
|
const s = settings.statusLine;
|
|
369
|
-
|
|
435
|
+
if (s?.type !== "command") {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
const c = (s.command ?? "").trim();
|
|
439
|
+
return c === LEGACY_STATUSLINE_COMMAND || c.includes("@primitive.ai/prim") && c.includes("statusline");
|
|
370
440
|
}
|
|
371
441
|
function applyStatusLine(settings) {
|
|
372
442
|
if (settings.statusLine && !isPrimStatusLine(settings)) {
|
|
@@ -393,8 +463,8 @@ function applyUninstall(settings) {
|
|
|
393
463
|
const hooks = {};
|
|
394
464
|
for (const event of Object.keys(source)) {
|
|
395
465
|
let list = source[event] ?? [];
|
|
396
|
-
for (const
|
|
397
|
-
list = stripCommand(list,
|
|
466
|
+
for (const bin of PRIM_BINS) {
|
|
467
|
+
list = stripCommand(list, bin);
|
|
398
468
|
}
|
|
399
469
|
if (list.length > 0) {
|
|
400
470
|
hooks[event] = list;
|
|
@@ -408,18 +478,18 @@ function applyUninstall(settings) {
|
|
|
408
478
|
}
|
|
409
479
|
function captureInstalled(settings) {
|
|
410
480
|
return CAPTURE_EVENTS.some(
|
|
411
|
-
(event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e,
|
|
481
|
+
(event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_BIN))
|
|
412
482
|
);
|
|
413
483
|
}
|
|
414
484
|
function statuslineInstalled(settings) {
|
|
415
485
|
return isPrimStatusLine(settings);
|
|
416
486
|
}
|
|
417
487
|
function isGateInstalled(settings) {
|
|
418
|
-
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e,
|
|
488
|
+
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN));
|
|
419
489
|
}
|
|
420
490
|
function atomicWrite(path, content) {
|
|
421
|
-
const dir =
|
|
422
|
-
if (!
|
|
491
|
+
const dir = dirname3(path);
|
|
492
|
+
if (!existsSync3(dir)) {
|
|
423
493
|
mkdirSync2(dir, { recursive: true });
|
|
424
494
|
}
|
|
425
495
|
const tmp = `${path}.tmp.${String(Date.now())}`;
|
|
@@ -535,11 +605,12 @@ ${line("project", result.project)}`);
|
|
|
535
605
|
|
|
536
606
|
// src/commands/codex-install.ts
|
|
537
607
|
import { homedir as homedir2 } from "os";
|
|
538
|
-
import { join as
|
|
539
|
-
var
|
|
540
|
-
var
|
|
541
|
-
var
|
|
542
|
-
var
|
|
608
|
+
import { join as join3 } from "path";
|
|
609
|
+
var CAPTURE_BIN2 = "prim-hook";
|
|
610
|
+
var GATE_BIN2 = "prim-pre-tool-use";
|
|
611
|
+
var POST_TOOL_USE_BIN2 = "prim-post-tool-use";
|
|
612
|
+
var SESSION_START_BIN2 = "prim-session-start";
|
|
613
|
+
var CODEX_ARGS = "--agent codex";
|
|
543
614
|
var JSON_INDENT2 = 2;
|
|
544
615
|
var CODEX_CAPTURE_EVENTS = [
|
|
545
616
|
"SessionStart",
|
|
@@ -549,20 +620,15 @@ var CODEX_CAPTURE_EVENTS = [
|
|
|
549
620
|
"Stop",
|
|
550
621
|
"SubagentStop"
|
|
551
622
|
];
|
|
552
|
-
var
|
|
553
|
-
CAPTURE_COMMAND2,
|
|
554
|
-
GATE_COMMAND2,
|
|
555
|
-
POST_TOOL_USE_COMMAND2,
|
|
556
|
-
SESSION_START_COMMAND2
|
|
557
|
-
]);
|
|
623
|
+
var PRIM_BINS2 = [CAPTURE_BIN2, GATE_BIN2, POST_TOOL_USE_BIN2, SESSION_START_BIN2];
|
|
558
624
|
var CODEX_REGISTRATIONS = [
|
|
559
|
-
...CODEX_CAPTURE_EVENTS.map((event) => (
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
625
|
+
...CODEX_CAPTURE_EVENTS.map((event) => makeRegistration(event, "*", CAPTURE_BIN2, CODEX_ARGS)),
|
|
626
|
+
makeRegistration("PreToolUse", "apply_patch", GATE_BIN2, CODEX_ARGS),
|
|
627
|
+
makeRegistration("PostToolUse", "apply_patch", POST_TOOL_USE_BIN2, CODEX_ARGS),
|
|
628
|
+
makeRegistration("SessionStart", "*", SESSION_START_BIN2, CODEX_ARGS)
|
|
563
629
|
];
|
|
564
|
-
var USER_SCOPE_PATH2 =
|
|
565
|
-
var PROJECT_SCOPE_PATH2 =
|
|
630
|
+
var USER_SCOPE_PATH2 = join3(homedir2(), ".codex", "hooks.json");
|
|
631
|
+
var PROJECT_SCOPE_PATH2 = join3(process.cwd(), ".codex", "hooks.json");
|
|
566
632
|
function settingsPathFor2(scope) {
|
|
567
633
|
return scope === "user" ? USER_SCOPE_PATH2 : PROJECT_SCOPE_PATH2;
|
|
568
634
|
}
|
|
@@ -578,8 +644,8 @@ function applyUninstall2(settings) {
|
|
|
578
644
|
const hooks = {};
|
|
579
645
|
for (const event of Object.keys(source)) {
|
|
580
646
|
let list = source[event] ?? [];
|
|
581
|
-
for (const
|
|
582
|
-
list = stripCommand(list,
|
|
647
|
+
for (const bin of PRIM_BINS2) {
|
|
648
|
+
list = stripCommand(list, bin);
|
|
583
649
|
}
|
|
584
650
|
if (list.length > 0) {
|
|
585
651
|
hooks[event] = list;
|
|
@@ -589,11 +655,11 @@ function applyUninstall2(settings) {
|
|
|
589
655
|
}
|
|
590
656
|
function captureInstalled2(settings) {
|
|
591
657
|
return CODEX_CAPTURE_EVENTS.some(
|
|
592
|
-
(event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e,
|
|
658
|
+
(event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_BIN2))
|
|
593
659
|
);
|
|
594
660
|
}
|
|
595
661
|
function isGateInstalled2(settings) {
|
|
596
|
-
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e,
|
|
662
|
+
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN2));
|
|
597
663
|
}
|
|
598
664
|
function resultFor(scope, path, after, changed) {
|
|
599
665
|
return {
|
|
@@ -682,7 +748,7 @@ ${line("project", result.project)}`);
|
|
|
682
748
|
}
|
|
683
749
|
|
|
684
750
|
// src/commands/context.ts
|
|
685
|
-
import { readFileSync as
|
|
751
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
686
752
|
function registerContextCommands(program2) {
|
|
687
753
|
const context = program2.command("context").description("Manage contexts");
|
|
688
754
|
context.command("list").description("List contexts").option("-s, --scope <scope>", "Filter by scope: project, global, external").option("-t, --project-id <projectId>", "List contexts linked to a specific project").option("--json", "Output as JSON").action(async (opts) => {
|
|
@@ -711,7 +777,7 @@ function registerContextCommands(program2) {
|
|
|
711
777
|
const client = getClient();
|
|
712
778
|
let text = opts.text;
|
|
713
779
|
if (opts.file) {
|
|
714
|
-
text =
|
|
780
|
+
text = readFileSync4(opts.file, "utf-8");
|
|
715
781
|
}
|
|
716
782
|
const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
|
|
717
783
|
const result = await client.post("/api/cli/contexts", {
|
|
@@ -734,7 +800,7 @@ function registerContextCommands(program2) {
|
|
|
734
800
|
const client = getClient();
|
|
735
801
|
let text = opts.text;
|
|
736
802
|
if (opts.file) {
|
|
737
|
-
text =
|
|
803
|
+
text = readFileSync4(opts.file, "utf-8");
|
|
738
804
|
}
|
|
739
805
|
await client.patch(`/api/cli/contexts/${contextId}`, {
|
|
740
806
|
name: opts.name,
|
|
@@ -800,22 +866,26 @@ ${contexts.length} context(s)`);
|
|
|
800
866
|
|
|
801
867
|
// src/commands/daemon.ts
|
|
802
868
|
import { spawn } from "child_process";
|
|
803
|
-
import { existsSync as
|
|
869
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, unlinkSync } from "fs";
|
|
804
870
|
import { homedir as homedir3 } from "os";
|
|
805
|
-
import { join as
|
|
871
|
+
import { join as join4 } from "path";
|
|
806
872
|
var DAEMON_BIN = "prim-daemon-server";
|
|
807
|
-
var PID_PATH =
|
|
808
|
-
var SOCK_PATH =
|
|
873
|
+
var PID_PATH = join4(homedir3(), ".config", "prim", "daemon.pid");
|
|
874
|
+
var SOCK_PATH = join4(homedir3(), ".config", "prim", "sock");
|
|
809
875
|
var STOP_TIMEOUT_MS = 5e3;
|
|
810
876
|
var STOP_POLL_MS = 100;
|
|
811
877
|
var STATUS_PROBE_TIMEOUT_MS = 500;
|
|
812
|
-
var
|
|
878
|
+
var READY_TIMEOUT_MS = 5e3;
|
|
879
|
+
var READY_POLL_MS = 100;
|
|
880
|
+
var READY_PROBE_TIMEOUT_MS = 250;
|
|
881
|
+
var EXIT_OK = 0;
|
|
813
882
|
var EXIT_NOT_RUNNING = 2;
|
|
883
|
+
var EXIT_BOOTING = 3;
|
|
814
884
|
function readPidfile() {
|
|
815
|
-
if (!
|
|
885
|
+
if (!existsSync4(PID_PATH)) {
|
|
816
886
|
return null;
|
|
817
887
|
}
|
|
818
|
-
const raw =
|
|
888
|
+
const raw = readFileSync5(PID_PATH, "utf-8").trim();
|
|
819
889
|
const pid = Number(raw);
|
|
820
890
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
821
891
|
return null;
|
|
@@ -846,6 +916,20 @@ function sleep(ms) {
|
|
|
846
916
|
timer.unref();
|
|
847
917
|
});
|
|
848
918
|
}
|
|
919
|
+
function spawnDaemon(options) {
|
|
920
|
+
const file = binFile(DAEMON_BIN);
|
|
921
|
+
return file ? spawn(process.execPath, [file], options) : spawn(DAEMON_BIN, [], options);
|
|
922
|
+
}
|
|
923
|
+
async function waitForReady() {
|
|
924
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
925
|
+
while (Date.now() < deadline) {
|
|
926
|
+
if (await daemonIsLive(READY_PROBE_TIMEOUT_MS)) {
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
await sleep(READY_POLL_MS);
|
|
930
|
+
}
|
|
931
|
+
return daemonIsLive(READY_PROBE_TIMEOUT_MS);
|
|
932
|
+
}
|
|
849
933
|
async function daemonStart(opts) {
|
|
850
934
|
const existing = readPidfile();
|
|
851
935
|
if (existing?.alive) {
|
|
@@ -858,29 +942,32 @@ async function daemonStart(opts) {
|
|
|
858
942
|
clearStaleArtifacts();
|
|
859
943
|
}
|
|
860
944
|
if (opts.foreground) {
|
|
861
|
-
const child2 =
|
|
945
|
+
const child2 = spawnDaemon({ stdio: "inherit" });
|
|
862
946
|
child2.on("exit", (code) => {
|
|
863
947
|
process.exit(code ?? 0);
|
|
864
948
|
});
|
|
865
949
|
return;
|
|
866
950
|
}
|
|
867
|
-
const child =
|
|
868
|
-
detached: true,
|
|
869
|
-
stdio: ["ignore", "ignore", "ignore"]
|
|
870
|
-
});
|
|
951
|
+
const child = spawnDaemon({ detached: true, stdio: ["ignore", "ignore", "ignore"] });
|
|
871
952
|
child.unref();
|
|
872
|
-
await
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
process.stderr.write(
|
|
876
|
-
`)
|
|
877
|
-
|
|
953
|
+
const live = await waitForReady();
|
|
954
|
+
if (live) {
|
|
955
|
+
const after = readPidfile();
|
|
956
|
+
process.stderr.write(
|
|
957
|
+
`[prim] \u2713 daemon started (pid=${after?.pid ?? "?"}, socket=${SOCK_PATH})
|
|
958
|
+
`
|
|
959
|
+
);
|
|
960
|
+
console.log(JSON.stringify({ started: true, pid: after?.pid }, null, 2));
|
|
878
961
|
return;
|
|
879
962
|
}
|
|
880
963
|
process.stderr.write(
|
|
881
|
-
|
|
964
|
+
`[prim] \u2717 daemon start: spawned but the socket did not respond within ${READY_TIMEOUT_MS}ms (check that \`${DAEMON_BIN}\` resolves, and see its log)
|
|
965
|
+
`
|
|
882
966
|
);
|
|
883
967
|
console.log(JSON.stringify({ started: false }, null, 2));
|
|
968
|
+
if (!process.exitCode) {
|
|
969
|
+
process.exitCode = EXIT_NOT_RUNNING;
|
|
970
|
+
}
|
|
884
971
|
}
|
|
885
972
|
async function daemonStop() {
|
|
886
973
|
const existing = readPidfile();
|
|
@@ -922,41 +1009,48 @@ async function daemonStop() {
|
|
|
922
1009
|
);
|
|
923
1010
|
console.log(JSON.stringify({ stopped: false, pid: existing.pid }, null, 2));
|
|
924
1011
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
process.stderr.write("[prim] \u2717 daemon down\n");
|
|
929
|
-
console.log(JSON.stringify({ running: false }, null, 2));
|
|
930
|
-
if (!process.exitCode) {
|
|
931
|
-
process.exitCode = EXIT_NOT_RUNNING;
|
|
932
|
-
}
|
|
933
|
-
return;
|
|
1012
|
+
function classifyStatus(pidAlive, responding, snapshot, pid) {
|
|
1013
|
+
if (!pidAlive) {
|
|
1014
|
+
return { json: { running: false }, exitCode: EXIT_NOT_RUNNING };
|
|
934
1015
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
if (!process.exitCode) {
|
|
941
|
-
process.exitCode = EXIT_NOT_RUNNING;
|
|
942
|
-
}
|
|
943
|
-
return;
|
|
1016
|
+
if (!responding) {
|
|
1017
|
+
return {
|
|
1018
|
+
json: { running: true, responding: false, state: "starting", pid },
|
|
1019
|
+
exitCode: EXIT_BOOTING
|
|
1020
|
+
};
|
|
944
1021
|
}
|
|
945
|
-
|
|
1022
|
+
if (!snapshot) {
|
|
1023
|
+
return { json: { running: true, responding: true }, exitCode: EXIT_OK };
|
|
1024
|
+
}
|
|
1025
|
+
return { json: { running: true, responding: true, ...snapshot }, exitCode: EXIT_OK };
|
|
1026
|
+
}
|
|
1027
|
+
async function daemonStatus() {
|
|
1028
|
+
const pid = readPidfile();
|
|
1029
|
+
const pidAlive = pid?.alive ?? false;
|
|
1030
|
+
const responding = pidAlive ? await daemonIsLive(STATUS_PROBE_TIMEOUT_MS) : false;
|
|
1031
|
+
const snapshot = responding ? await daemonRequest(
|
|
946
1032
|
"status_snapshot",
|
|
947
1033
|
{},
|
|
948
1034
|
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
949
|
-
);
|
|
950
|
-
|
|
1035
|
+
) : null;
|
|
1036
|
+
const { json, exitCode } = classifyStatus(pidAlive, responding, snapshot, pid?.pid);
|
|
1037
|
+
if (!pidAlive) {
|
|
1038
|
+
process.stderr.write("[prim] \u2717 daemon down\n");
|
|
1039
|
+
} else if (!responding) {
|
|
1040
|
+
process.stderr.write(`[prim] \u25CC daemon pid=${pid?.pid} starting (socket not responding yet)
|
|
1041
|
+
`);
|
|
1042
|
+
} else if (!snapshot) {
|
|
951
1043
|
process.stderr.write("[prim] \u2713 daemon live (no snapshot)\n");
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
process.stderr.write(
|
|
956
|
-
`[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}
|
|
1044
|
+
} else {
|
|
1045
|
+
process.stderr.write(
|
|
1046
|
+
`[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}
|
|
957
1047
|
`
|
|
958
|
-
|
|
959
|
-
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
console.log(JSON.stringify(json, null, 2));
|
|
1051
|
+
if (exitCode !== EXIT_OK && !process.exitCode) {
|
|
1052
|
+
process.exitCode = exitCode;
|
|
1053
|
+
}
|
|
960
1054
|
}
|
|
961
1055
|
async function daemonRestart(opts) {
|
|
962
1056
|
await daemonStop();
|
|
@@ -1574,33 +1668,42 @@ function registerDecisionsCommands(program2) {
|
|
|
1574
1668
|
|
|
1575
1669
|
// src/commands/hooks.ts
|
|
1576
1670
|
import { execSync } from "child_process";
|
|
1577
|
-
import { existsSync as
|
|
1671
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1578
1672
|
import { resolve } from "path";
|
|
1579
1673
|
import { Option } from "commander";
|
|
1580
|
-
var
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1674
|
+
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
1675
|
+
var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
|
|
1676
|
+
var HOOKS = [PRE_COMMIT, POST_COMMIT];
|
|
1677
|
+
function blockMarkers(spec) {
|
|
1678
|
+
return {
|
|
1679
|
+
start: `# >>> prim ${spec.hookName} hook >>>`,
|
|
1680
|
+
end: `# <<< prim ${spec.hookName} hook <<<`
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
var PRIM_BLOCK_START = blockMarkers(PRE_COMMIT).start;
|
|
1684
|
+
var PRIM_BLOCK_END = blockMarkers(PRE_COMMIT).end;
|
|
1685
|
+
function hookShim(binName) {
|
|
1686
|
+
return `if command -v ${binName} >/dev/null 2>&1; then
|
|
1687
|
+
${binName}
|
|
1688
|
+
elif [ -f "./node_modules/.bin/${binName}" ]; then
|
|
1689
|
+
./node_modules/.bin/${binName}
|
|
1589
1690
|
else
|
|
1590
|
-
npx --yes -p @primitive.ai/prim
|
|
1591
|
-
fi
|
|
1691
|
+
npx --yes -p @primitive.ai/prim ${binName} 2>/dev/null || true
|
|
1692
|
+
fi`;
|
|
1693
|
+
}
|
|
1694
|
+
function dotGitScript(spec) {
|
|
1695
|
+
return `#!/bin/sh
|
|
1696
|
+
# prim ${spec.hookName} hook \u2014 installed by: prim hooks install
|
|
1697
|
+
|
|
1698
|
+
${hookShim(spec.binName)}
|
|
1592
1699
|
`;
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
else
|
|
1601
|
-
npx --yes -p @primitive.ai/prim prim-pre-commit 2>/dev/null || true
|
|
1602
|
-
fi
|
|
1603
|
-
${PRIM_BLOCK_END}`;
|
|
1700
|
+
}
|
|
1701
|
+
function huskyBlock(spec) {
|
|
1702
|
+
const { start, end } = blockMarkers(spec);
|
|
1703
|
+
return `${start}
|
|
1704
|
+
${hookShim(spec.binName)}
|
|
1705
|
+
${end}`;
|
|
1706
|
+
}
|
|
1604
1707
|
function getGitRoot() {
|
|
1605
1708
|
return execSync("git rev-parse --show-toplevel", {
|
|
1606
1709
|
encoding: "utf-8"
|
|
@@ -1608,13 +1711,13 @@ function getGitRoot() {
|
|
|
1608
1711
|
}
|
|
1609
1712
|
function detectHusky(gitRoot) {
|
|
1610
1713
|
const huskyDir = resolve(gitRoot, ".husky");
|
|
1611
|
-
if (!
|
|
1612
|
-
if (
|
|
1613
|
-
if (
|
|
1714
|
+
if (!existsSync5(huskyDir)) return false;
|
|
1715
|
+
if (existsSync5(resolve(huskyDir, "_"))) return true;
|
|
1716
|
+
if (existsSync5(resolve(huskyDir, "pre-commit"))) return true;
|
|
1614
1717
|
const pkgPath = resolve(gitRoot, "package.json");
|
|
1615
|
-
if (
|
|
1718
|
+
if (existsSync5(pkgPath)) {
|
|
1616
1719
|
try {
|
|
1617
|
-
const pkg2 = JSON.parse(
|
|
1720
|
+
const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
1618
1721
|
const scripts = pkg2.scripts ?? {};
|
|
1619
1722
|
if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
|
|
1620
1723
|
return true;
|
|
@@ -1624,8 +1727,8 @@ function detectHusky(gitRoot) {
|
|
|
1624
1727
|
}
|
|
1625
1728
|
return false;
|
|
1626
1729
|
}
|
|
1627
|
-
function containsPrimHook(content) {
|
|
1628
|
-
return content.includes(
|
|
1730
|
+
function containsPrimHook(content, binName = PRE_COMMIT.binName) {
|
|
1731
|
+
return content.includes(binName);
|
|
1629
1732
|
}
|
|
1630
1733
|
async function askConfirmation(question) {
|
|
1631
1734
|
if (!process.stdin.isTTY) return false;
|
|
@@ -1639,52 +1742,63 @@ async function askConfirmation(question) {
|
|
|
1639
1742
|
rl.close();
|
|
1640
1743
|
}
|
|
1641
1744
|
}
|
|
1642
|
-
function installToHusky(gitRoot) {
|
|
1643
|
-
const hookPath = resolve(gitRoot, ".husky",
|
|
1644
|
-
if (
|
|
1645
|
-
const existing =
|
|
1646
|
-
if (containsPrimHook(existing)) {
|
|
1647
|
-
console.log(
|
|
1745
|
+
function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
1746
|
+
const hookPath = resolve(gitRoot, ".husky", spec.hookName);
|
|
1747
|
+
if (existsSync5(hookPath)) {
|
|
1748
|
+
const existing = readFileSync6(hookPath, "utf-8");
|
|
1749
|
+
if (containsPrimHook(existing, spec.binName)) {
|
|
1750
|
+
console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
|
|
1648
1751
|
return;
|
|
1649
1752
|
}
|
|
1650
1753
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1651
|
-
writeFileSync3(hookPath, `${existing}${separator}${
|
|
1754
|
+
writeFileSync3(hookPath, `${existing}${separator}${huskyBlock(spec)}
|
|
1652
1755
|
`, {
|
|
1653
1756
|
mode: 493
|
|
1654
1757
|
});
|
|
1655
|
-
console.log(
|
|
1758
|
+
console.log(`Appended prim hook block to .husky/${spec.hookName}.`);
|
|
1656
1759
|
} else {
|
|
1657
1760
|
writeFileSync3(hookPath, `#!/bin/sh
|
|
1658
1761
|
|
|
1659
|
-
${
|
|
1762
|
+
${huskyBlock(spec)}
|
|
1660
1763
|
`, {
|
|
1661
1764
|
mode: 493
|
|
1662
1765
|
});
|
|
1663
|
-
console.log(
|
|
1766
|
+
console.log(`Created .husky/${spec.hookName} with prim hook block.`);
|
|
1664
1767
|
}
|
|
1665
1768
|
}
|
|
1666
|
-
function installToDotGit(gitRoot) {
|
|
1769
|
+
function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
1667
1770
|
const hooksDir = resolve(gitRoot, ".git", "hooks");
|
|
1668
|
-
const hookPath = resolve(hooksDir,
|
|
1669
|
-
if (!
|
|
1771
|
+
const hookPath = resolve(hooksDir, spec.hookName);
|
|
1772
|
+
if (!existsSync5(hooksDir)) {
|
|
1670
1773
|
mkdirSync3(hooksDir, { recursive: true });
|
|
1671
1774
|
}
|
|
1672
|
-
if (
|
|
1673
|
-
const existing =
|
|
1674
|
-
if (containsPrimHook(existing)) {
|
|
1675
|
-
console.log(
|
|
1775
|
+
if (existsSync5(hookPath)) {
|
|
1776
|
+
const existing = readFileSync6(hookPath, "utf-8");
|
|
1777
|
+
if (containsPrimHook(existing, spec.binName)) {
|
|
1778
|
+
console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
|
|
1676
1779
|
return;
|
|
1677
1780
|
}
|
|
1678
|
-
console.log(`A
|
|
1781
|
+
console.log(`A ${spec.hookName} hook already exists at ${hookPath}.`);
|
|
1679
1782
|
console.log("To replace it, run: prim hooks uninstall && prim hooks install");
|
|
1680
1783
|
return;
|
|
1681
1784
|
}
|
|
1682
|
-
writeFileSync3(hookPath,
|
|
1683
|
-
console.log(`Installed
|
|
1785
|
+
writeFileSync3(hookPath, dotGitScript(spec), { mode: 493 });
|
|
1786
|
+
console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
|
|
1787
|
+
}
|
|
1788
|
+
function installHooks(gitRoot, target) {
|
|
1789
|
+
for (const spec of HOOKS) {
|
|
1790
|
+
if (target === "husky") {
|
|
1791
|
+
installToHusky(gitRoot, spec);
|
|
1792
|
+
} else {
|
|
1793
|
+
installToDotGit(gitRoot, spec);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1684
1796
|
}
|
|
1685
1797
|
function registerHooksCommands(program2) {
|
|
1686
1798
|
const hooks = program2.command("hooks").description("Manage git hooks");
|
|
1687
|
-
hooks.command("install").description(
|
|
1799
|
+
hooks.command("install").description(
|
|
1800
|
+
"Install the prim git hooks \u2014 pre-commit + post-commit (auto-detects Husky; use --target to override)"
|
|
1801
|
+
).addOption(
|
|
1688
1802
|
new Option("--target <where>", "install destination; bypasses Husky detection").choices([
|
|
1689
1803
|
"husky",
|
|
1690
1804
|
"git-hooks"
|
|
@@ -1695,10 +1809,10 @@ function registerHooksCommands(program2) {
|
|
|
1695
1809
|
globals.nonInteractive || process.env.CI || process.env.PRIM_NON_INTERACTIVE
|
|
1696
1810
|
);
|
|
1697
1811
|
const gitRoot = getGitRoot();
|
|
1698
|
-
if (opts.target === "husky") return
|
|
1699
|
-
if (opts.target === "git-hooks") return
|
|
1812
|
+
if (opts.target === "husky") return installHooks(gitRoot, "husky");
|
|
1813
|
+
if (opts.target === "git-hooks") return installHooks(gitRoot, "git-hooks");
|
|
1700
1814
|
if (detectHusky(gitRoot)) {
|
|
1701
|
-
if (globals.yes) return
|
|
1815
|
+
if (globals.yes) return installHooks(gitRoot, "husky");
|
|
1702
1816
|
if (nonInteractive) {
|
|
1703
1817
|
throw new Error(
|
|
1704
1818
|
"--non-interactive set, refusing to prompt for Husky-hook installation. Pass --yes to confirm or --target=git-hooks to choose."
|
|
@@ -1709,30 +1823,36 @@ function registerHooksCommands(program2) {
|
|
|
1709
1823
|
"Note: Husky detected but stdin is not a TTY \u2014 falling back to .git/hooks. Pass --yes for Husky or --non-interactive to fail fast."
|
|
1710
1824
|
);
|
|
1711
1825
|
} else if (await askConfirmation(
|
|
1712
|
-
"Husky detected. Install prim
|
|
1826
|
+
"Husky detected. Install prim hooks into .husky/ instead of .git/hooks/?"
|
|
1713
1827
|
)) {
|
|
1714
|
-
return
|
|
1828
|
+
return installHooks(gitRoot, "husky");
|
|
1715
1829
|
} else {
|
|
1716
|
-
console.log("Falling back to .git/hooks
|
|
1830
|
+
console.log("Falling back to .git/hooks install.");
|
|
1717
1831
|
}
|
|
1718
1832
|
}
|
|
1719
|
-
|
|
1833
|
+
installHooks(gitRoot, "git-hooks");
|
|
1720
1834
|
});
|
|
1721
|
-
hooks.command("uninstall").description("Remove the prim
|
|
1835
|
+
hooks.command("uninstall").description("Remove the prim git hooks (.git/hooks)").action(() => {
|
|
1722
1836
|
const gitRoot = getGitRoot();
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1837
|
+
for (const spec of HOOKS) {
|
|
1838
|
+
const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
|
|
1839
|
+
if (!existsSync5(hookPath)) {
|
|
1840
|
+
console.log(`No ${spec.hookName} hook found.`);
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
if (containsPrimHook(readFileSync6(hookPath, "utf-8"), spec.binName)) {
|
|
1844
|
+
unlinkSync2(hookPath);
|
|
1845
|
+
console.log(`Removed ${spec.hookName} hook at ${hookPath}`);
|
|
1846
|
+
} else {
|
|
1847
|
+
console.log(`Left ${spec.hookName} hook at ${hookPath} untouched (not a prim hook).`);
|
|
1848
|
+
}
|
|
1727
1849
|
}
|
|
1728
|
-
unlinkSync2(hookPath);
|
|
1729
|
-
console.log(`Removed pre-commit hook at ${hookPath}`);
|
|
1730
1850
|
});
|
|
1731
1851
|
}
|
|
1732
1852
|
|
|
1733
1853
|
// src/commands/moves.ts
|
|
1734
|
-
import { existsSync as
|
|
1735
|
-
import { join as
|
|
1854
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1855
|
+
import { join as join5 } from "path";
|
|
1736
1856
|
|
|
1737
1857
|
// src/flusher.ts
|
|
1738
1858
|
import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
@@ -1841,19 +1961,19 @@ function registerMovesCommands(program2) {
|
|
|
1841
1961
|
}
|
|
1842
1962
|
});
|
|
1843
1963
|
moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
|
|
1844
|
-
const dir =
|
|
1845
|
-
if (!
|
|
1964
|
+
const dir = join5(process.cwd(), ".prim");
|
|
1965
|
+
if (!existsSync6(dir)) {
|
|
1846
1966
|
mkdirSync4(dir, { recursive: true, mode: DIR_MODE });
|
|
1847
1967
|
}
|
|
1848
|
-
const file =
|
|
1968
|
+
const file = join5(process.cwd(), WORKSPACE_FILE);
|
|
1849
1969
|
writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
|
|
1850
1970
|
mode: FILE_MODE2
|
|
1851
1971
|
});
|
|
1852
1972
|
console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
|
|
1853
1973
|
});
|
|
1854
1974
|
moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
|
|
1855
|
-
const file =
|
|
1856
|
-
if (!
|
|
1975
|
+
const file = join5(process.cwd(), WORKSPACE_FILE);
|
|
1976
|
+
if (!existsSync6(file)) {
|
|
1857
1977
|
console.log("[prim] no workspace binding in cwd");
|
|
1858
1978
|
return;
|
|
1859
1979
|
}
|
|
@@ -1885,7 +2005,7 @@ function registerProjectCommands(program2) {
|
|
|
1885
2005
|
}
|
|
1886
2006
|
|
|
1887
2007
|
// src/commands/reconcile.ts
|
|
1888
|
-
var
|
|
2008
|
+
var EXIT_OK2 = 0;
|
|
1889
2009
|
var EXIT_USAGE = 2;
|
|
1890
2010
|
var EXIT_SERVER = 3;
|
|
1891
2011
|
var HTTP_CLIENT_ERROR_MIN = 400;
|
|
@@ -1948,7 +2068,7 @@ async function performReconcile(idOrShortId, opts = {}) {
|
|
|
1948
2068
|
`
|
|
1949
2069
|
);
|
|
1950
2070
|
console.log(JSON.stringify(response, null, 2));
|
|
1951
|
-
process.exitCode =
|
|
2071
|
+
process.exitCode = EXIT_OK2;
|
|
1952
2072
|
return;
|
|
1953
2073
|
}
|
|
1954
2074
|
process.stderr.write("[prim] reconcile: malformed server response\n");
|
|
@@ -1968,23 +2088,23 @@ function registerReconcileCommands(program2) {
|
|
|
1968
2088
|
|
|
1969
2089
|
// src/commands/session.ts
|
|
1970
2090
|
import {
|
|
1971
|
-
existsSync as
|
|
2091
|
+
existsSync as existsSync7,
|
|
1972
2092
|
mkdirSync as mkdirSync5,
|
|
1973
|
-
readFileSync as
|
|
2093
|
+
readFileSync as readFileSync7,
|
|
1974
2094
|
readdirSync,
|
|
1975
2095
|
unlinkSync as unlinkSync5,
|
|
1976
2096
|
writeFileSync as writeFileSync5
|
|
1977
2097
|
} from "fs";
|
|
1978
|
-
import { join as
|
|
2098
|
+
import { join as join6 } from "path";
|
|
1979
2099
|
var DIR_MODE2 = 448;
|
|
1980
2100
|
var FILE_MODE3 = 384;
|
|
1981
2101
|
function ensureDir() {
|
|
1982
|
-
if (!
|
|
2102
|
+
if (!existsSync7(SESSIONS_DIR)) {
|
|
1983
2103
|
mkdirSync5(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
|
|
1984
2104
|
}
|
|
1985
2105
|
}
|
|
1986
2106
|
function markerPath(sessionId) {
|
|
1987
|
-
return
|
|
2107
|
+
return join6(SESSIONS_DIR, `${sessionId}.json`);
|
|
1988
2108
|
}
|
|
1989
2109
|
function registerSessionCommands(program2) {
|
|
1990
2110
|
const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
|
|
@@ -2000,7 +2120,7 @@ function registerSessionCommands(program2) {
|
|
|
2000
2120
|
console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
|
|
2001
2121
|
});
|
|
2002
2122
|
session.command("list").description("List active session markers").action(() => {
|
|
2003
|
-
if (!
|
|
2123
|
+
if (!existsSync7(SESSIONS_DIR)) {
|
|
2004
2124
|
console.log("[prim] no session markers");
|
|
2005
2125
|
return;
|
|
2006
2126
|
}
|
|
@@ -2012,7 +2132,7 @@ function registerSessionCommands(program2) {
|
|
|
2012
2132
|
for (const f of files) {
|
|
2013
2133
|
const sessionId = f.replace(/\.json$/, "");
|
|
2014
2134
|
try {
|
|
2015
|
-
const m = JSON.parse(
|
|
2135
|
+
const m = JSON.parse(readFileSync7(join6(SESSIONS_DIR, f), "utf-8"));
|
|
2016
2136
|
console.log(`${sessionId} org=${m.orgId}`);
|
|
2017
2137
|
} catch {
|
|
2018
2138
|
}
|
|
@@ -2020,7 +2140,7 @@ function registerSessionCommands(program2) {
|
|
|
2020
2140
|
});
|
|
2021
2141
|
session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
|
|
2022
2142
|
const p = markerPath(sessionId);
|
|
2023
|
-
if (!
|
|
2143
|
+
if (!existsSync7(p)) {
|
|
2024
2144
|
console.log(`[prim] no marker for session ${sessionId}`);
|
|
2025
2145
|
return;
|
|
2026
2146
|
}
|
|
@@ -2032,17 +2152,17 @@ function registerSessionCommands(program2) {
|
|
|
2032
2152
|
// src/commands/skill.ts
|
|
2033
2153
|
import {
|
|
2034
2154
|
closeSync as closeSync2,
|
|
2035
|
-
existsSync as
|
|
2155
|
+
existsSync as existsSync8,
|
|
2036
2156
|
fsyncSync as fsyncSync2,
|
|
2037
2157
|
openSync as openSync2,
|
|
2038
|
-
readFileSync as
|
|
2158
|
+
readFileSync as readFileSync8,
|
|
2039
2159
|
renameSync as renameSync3,
|
|
2040
2160
|
writeFileSync as writeFileSync6
|
|
2041
2161
|
} from "fs";
|
|
2042
|
-
import { dirname as
|
|
2043
|
-
import { fileURLToPath } from "url";
|
|
2162
|
+
import { dirname as dirname4, resolve as resolve2 } from "path";
|
|
2163
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2044
2164
|
import { createPatch } from "diff";
|
|
2045
|
-
var __dirname =
|
|
2165
|
+
var __dirname = dirname4(fileURLToPath2(import.meta.url));
|
|
2046
2166
|
var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
|
|
2047
2167
|
var SKILL_END = "<!-- END PRIM SKILL v1 -->";
|
|
2048
2168
|
var TARGET_CANDIDATES = [
|
|
@@ -2055,15 +2175,15 @@ var TARGET_CANDIDATES = [
|
|
|
2055
2175
|
var DEFAULT_TARGET = "CLAUDE.md";
|
|
2056
2176
|
function loadSkill() {
|
|
2057
2177
|
let dir = __dirname;
|
|
2058
|
-
while (dir !==
|
|
2178
|
+
while (dir !== dirname4(dir)) {
|
|
2059
2179
|
const p = resolve2(dir, "SKILL.md");
|
|
2060
|
-
if (
|
|
2061
|
-
dir =
|
|
2180
|
+
if (existsSync8(p)) return readFileSync8(p, "utf-8");
|
|
2181
|
+
dir = dirname4(dir);
|
|
2062
2182
|
}
|
|
2063
2183
|
throw new Error("SKILL.md not found in package");
|
|
2064
2184
|
}
|
|
2065
2185
|
function detectTargets(cwd) {
|
|
2066
|
-
return TARGET_CANDIDATES.filter((p) =>
|
|
2186
|
+
return TARGET_CANDIDATES.filter((p) => existsSync8(resolve2(cwd, p)));
|
|
2067
2187
|
}
|
|
2068
2188
|
function detectNewline(content) {
|
|
2069
2189
|
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -2112,7 +2232,7 @@ function resolveTarget(cwd, override) {
|
|
|
2112
2232
|
function runInstall(cwd, opts) {
|
|
2113
2233
|
const target = resolveTarget(cwd, opts.target);
|
|
2114
2234
|
if (target === null) return 1;
|
|
2115
|
-
const existing =
|
|
2235
|
+
const existing = existsSync8(target) ? readFileSync8(target, "utf-8") : "";
|
|
2116
2236
|
const eol = existing ? detectNewline(existing) : "\n";
|
|
2117
2237
|
const block = composeBlock(loadSkill(), eol);
|
|
2118
2238
|
const next = applyBlock(existing, block, eol);
|
|
@@ -2131,11 +2251,11 @@ function runInstall(cwd, opts) {
|
|
|
2131
2251
|
function runUninstall(cwd, opts) {
|
|
2132
2252
|
const target = resolveTarget(cwd, opts.target);
|
|
2133
2253
|
if (target === null) return 1;
|
|
2134
|
-
if (!
|
|
2254
|
+
if (!existsSync8(target)) {
|
|
2135
2255
|
console.log(`Skill block not present at ${target}`);
|
|
2136
2256
|
return 0;
|
|
2137
2257
|
}
|
|
2138
|
-
const existing =
|
|
2258
|
+
const existing = readFileSync8(target, "utf-8");
|
|
2139
2259
|
const next = removeBlock(existing);
|
|
2140
2260
|
if (next === null) {
|
|
2141
2261
|
console.log(`Skill block not present at ${target}`);
|
|
@@ -2148,10 +2268,10 @@ function runUninstall(cwd, opts) {
|
|
|
2148
2268
|
function runStatus(cwd, opts) {
|
|
2149
2269
|
const target = resolveTarget(cwd, opts.target);
|
|
2150
2270
|
if (target === null) return 1;
|
|
2151
|
-
const fileExists =
|
|
2271
|
+
const fileExists = existsSync8(target);
|
|
2152
2272
|
let installed = false;
|
|
2153
2273
|
if (fileExists) {
|
|
2154
|
-
const content =
|
|
2274
|
+
const content = readFileSync8(target, "utf-8");
|
|
2155
2275
|
installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
|
|
2156
2276
|
}
|
|
2157
2277
|
if (opts.json) {
|
|
@@ -2188,7 +2308,7 @@ function registerSkillCommands(program2) {
|
|
|
2188
2308
|
}
|
|
2189
2309
|
|
|
2190
2310
|
// src/commands/spec.ts
|
|
2191
|
-
import { readFileSync as
|
|
2311
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
2192
2312
|
function registerSpecCommands(program2) {
|
|
2193
2313
|
const spec = program2.command("spec").description("Manage spec documents");
|
|
2194
2314
|
spec.command("list").description("List spec documents").option("-t, --project-id <projectId>", "List spec for a specific root project").option("--json", "Output as JSON").action(async (opts) => {
|
|
@@ -2242,7 +2362,7 @@ ${contexts.length} spec(s)`);
|
|
|
2242
2362
|
const client = getClient();
|
|
2243
2363
|
let text = opts.text;
|
|
2244
2364
|
if (opts.file) {
|
|
2245
|
-
text =
|
|
2365
|
+
text = readFileSync9(opts.file, "utf-8");
|
|
2246
2366
|
}
|
|
2247
2367
|
const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
|
|
2248
2368
|
let linkedBranch;
|
|
@@ -2283,7 +2403,7 @@ ${contexts.length} spec(s)`);
|
|
|
2283
2403
|
const client = getClient();
|
|
2284
2404
|
let text = opts.text;
|
|
2285
2405
|
if (opts.file) {
|
|
2286
|
-
text =
|
|
2406
|
+
text = readFileSync9(opts.file, "utf-8");
|
|
2287
2407
|
}
|
|
2288
2408
|
if (!(text || opts.name)) {
|
|
2289
2409
|
console.error("Provide --text, --file, or --name to update.");
|
|
@@ -2454,17 +2574,17 @@ ${preview}`);
|
|
|
2454
2574
|
}
|
|
2455
2575
|
|
|
2456
2576
|
// src/commands/statusline.ts
|
|
2457
|
-
import { readFileSync as
|
|
2458
|
-
import { dirname as
|
|
2459
|
-
import { fileURLToPath as
|
|
2577
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2578
|
+
import { dirname as dirname5, resolve as resolve3 } from "path";
|
|
2579
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2460
2580
|
var STATUSLINE_TIMEOUT_MS = 200;
|
|
2461
2581
|
function readPackageVersion() {
|
|
2462
2582
|
try {
|
|
2463
|
-
const here =
|
|
2583
|
+
const here = dirname5(fileURLToPath3(import.meta.url));
|
|
2464
2584
|
const candidates = [resolve3(here, "../../package.json"), resolve3(here, "../package.json")];
|
|
2465
2585
|
for (const path of candidates) {
|
|
2466
2586
|
try {
|
|
2467
|
-
const pkg2 = JSON.parse(
|
|
2587
|
+
const pkg2 = JSON.parse(readFileSync10(path, "utf-8"));
|
|
2468
2588
|
if (pkg2.version) {
|
|
2469
2589
|
return pkg2.version;
|
|
2470
2590
|
}
|
|
@@ -2509,8 +2629,8 @@ function registerStatuslineCommands(program2) {
|
|
|
2509
2629
|
}
|
|
2510
2630
|
|
|
2511
2631
|
// src/index.ts
|
|
2512
|
-
var __dirname2 =
|
|
2513
|
-
var pkg = JSON.parse(
|
|
2632
|
+
var __dirname2 = dirname6(fileURLToPath4(import.meta.url));
|
|
2633
|
+
var pkg = JSON.parse(readFileSync11(resolve4(__dirname2, "../package.json"), "utf-8"));
|
|
2514
2634
|
updateNotifier({ pkg }).notify();
|
|
2515
2635
|
var program = new Command();
|
|
2516
2636
|
program.name("prim").description("CLI for managing Primitive specs and contexts").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitive.ai/prim",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.19",
|
|
4
4
|
"description": "CLI for managing Primitive specs, contexts, and git hooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"bin": {
|
|
31
31
|
"prim": "dist/index.js",
|
|
32
32
|
"prim-pre-commit": "dist/hooks/pre-commit.js",
|
|
33
|
+
"prim-post-commit": "dist/hooks/post-commit.js",
|
|
33
34
|
"prim-hook": "dist/hooks/prim-hook.js",
|
|
34
35
|
"prim-pre-tool-use": "dist/hooks/pre-tool-use.js",
|
|
35
36
|
"prim-post-tool-use": "dist/hooks/post-tool-use.js",
|
|
@@ -45,9 +46,9 @@
|
|
|
45
46
|
"SKILL.md"
|
|
46
47
|
],
|
|
47
48
|
"scripts": {
|
|
48
|
-
"build": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --clean",
|
|
49
|
-
"postbuild": "chmod +x ./dist/index.js ./dist/hooks/pre-commit.js ./dist/hooks/prim-hook.js ./dist/hooks/pre-tool-use.js ./dist/hooks/post-tool-use.js ./dist/hooks/session-start.js ./dist/hooks/session-end.js ./dist/daemon/server.js",
|
|
50
|
-
"dev": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --watch --clean",
|
|
49
|
+
"build": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/post-commit.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --clean",
|
|
50
|
+
"postbuild": "chmod +x ./dist/index.js ./dist/hooks/pre-commit.js ./dist/hooks/post-commit.js ./dist/hooks/prim-hook.js ./dist/hooks/pre-tool-use.js ./dist/hooks/post-tool-use.js ./dist/hooks/session-start.js ./dist/hooks/session-end.js ./dist/daemon/server.js",
|
|
51
|
+
"dev": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/post-commit.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --watch --clean",
|
|
51
52
|
"clean": "rm -rf dist coverage",
|
|
52
53
|
"lint": "biome check src/",
|
|
53
54
|
"format": "biome check --fix src/",
|