@primitive.ai/prim 0.1.0-alpha.17 → 0.1.0-alpha.18
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 +80 -54
- 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
|
@@ -1577,30 +1577,39 @@ import { execSync } from "child_process";
|
|
|
1577
1577
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1578
1578
|
import { resolve } from "path";
|
|
1579
1579
|
import { Option } from "commander";
|
|
1580
|
-
var
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1580
|
+
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
1581
|
+
var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
|
|
1582
|
+
var HOOKS = [PRE_COMMIT, POST_COMMIT];
|
|
1583
|
+
function blockMarkers(spec) {
|
|
1584
|
+
return {
|
|
1585
|
+
start: `# >>> prim ${spec.hookName} hook >>>`,
|
|
1586
|
+
end: `# <<< prim ${spec.hookName} hook <<<`
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
var PRIM_BLOCK_START = blockMarkers(PRE_COMMIT).start;
|
|
1590
|
+
var PRIM_BLOCK_END = blockMarkers(PRE_COMMIT).end;
|
|
1591
|
+
function hookShim(binName) {
|
|
1592
|
+
return `if command -v ${binName} >/dev/null 2>&1; then
|
|
1593
|
+
${binName}
|
|
1594
|
+
elif [ -f "./node_modules/.bin/${binName}" ]; then
|
|
1595
|
+
./node_modules/.bin/${binName}
|
|
1589
1596
|
else
|
|
1590
|
-
npx --yes -p @primitive.ai/prim
|
|
1591
|
-
fi
|
|
1597
|
+
npx --yes -p @primitive.ai/prim ${binName} 2>/dev/null || true
|
|
1598
|
+
fi`;
|
|
1599
|
+
}
|
|
1600
|
+
function dotGitScript(spec) {
|
|
1601
|
+
return `#!/bin/sh
|
|
1602
|
+
# prim ${spec.hookName} hook \u2014 installed by: prim hooks install
|
|
1603
|
+
|
|
1604
|
+
${hookShim(spec.binName)}
|
|
1592
1605
|
`;
|
|
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}`;
|
|
1606
|
+
}
|
|
1607
|
+
function huskyBlock(spec) {
|
|
1608
|
+
const { start, end } = blockMarkers(spec);
|
|
1609
|
+
return `${start}
|
|
1610
|
+
${hookShim(spec.binName)}
|
|
1611
|
+
${end}`;
|
|
1612
|
+
}
|
|
1604
1613
|
function getGitRoot() {
|
|
1605
1614
|
return execSync("git rev-parse --show-toplevel", {
|
|
1606
1615
|
encoding: "utf-8"
|
|
@@ -1624,8 +1633,8 @@ function detectHusky(gitRoot) {
|
|
|
1624
1633
|
}
|
|
1625
1634
|
return false;
|
|
1626
1635
|
}
|
|
1627
|
-
function containsPrimHook(content) {
|
|
1628
|
-
return content.includes(
|
|
1636
|
+
function containsPrimHook(content, binName = PRE_COMMIT.binName) {
|
|
1637
|
+
return content.includes(binName);
|
|
1629
1638
|
}
|
|
1630
1639
|
async function askConfirmation(question) {
|
|
1631
1640
|
if (!process.stdin.isTTY) return false;
|
|
@@ -1639,52 +1648,63 @@ async function askConfirmation(question) {
|
|
|
1639
1648
|
rl.close();
|
|
1640
1649
|
}
|
|
1641
1650
|
}
|
|
1642
|
-
function installToHusky(gitRoot) {
|
|
1643
|
-
const hookPath = resolve(gitRoot, ".husky",
|
|
1651
|
+
function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
1652
|
+
const hookPath = resolve(gitRoot, ".husky", spec.hookName);
|
|
1644
1653
|
if (existsSync4(hookPath)) {
|
|
1645
1654
|
const existing = readFileSync5(hookPath, "utf-8");
|
|
1646
|
-
if (containsPrimHook(existing)) {
|
|
1647
|
-
console.log(
|
|
1655
|
+
if (containsPrimHook(existing, spec.binName)) {
|
|
1656
|
+
console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
|
|
1648
1657
|
return;
|
|
1649
1658
|
}
|
|
1650
1659
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1651
|
-
writeFileSync3(hookPath, `${existing}${separator}${
|
|
1660
|
+
writeFileSync3(hookPath, `${existing}${separator}${huskyBlock(spec)}
|
|
1652
1661
|
`, {
|
|
1653
1662
|
mode: 493
|
|
1654
1663
|
});
|
|
1655
|
-
console.log(
|
|
1664
|
+
console.log(`Appended prim hook block to .husky/${spec.hookName}.`);
|
|
1656
1665
|
} else {
|
|
1657
1666
|
writeFileSync3(hookPath, `#!/bin/sh
|
|
1658
1667
|
|
|
1659
|
-
${
|
|
1668
|
+
${huskyBlock(spec)}
|
|
1660
1669
|
`, {
|
|
1661
1670
|
mode: 493
|
|
1662
1671
|
});
|
|
1663
|
-
console.log(
|
|
1672
|
+
console.log(`Created .husky/${spec.hookName} with prim hook block.`);
|
|
1664
1673
|
}
|
|
1665
1674
|
}
|
|
1666
|
-
function installToDotGit(gitRoot) {
|
|
1675
|
+
function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
1667
1676
|
const hooksDir = resolve(gitRoot, ".git", "hooks");
|
|
1668
|
-
const hookPath = resolve(hooksDir,
|
|
1677
|
+
const hookPath = resolve(hooksDir, spec.hookName);
|
|
1669
1678
|
if (!existsSync4(hooksDir)) {
|
|
1670
1679
|
mkdirSync3(hooksDir, { recursive: true });
|
|
1671
1680
|
}
|
|
1672
1681
|
if (existsSync4(hookPath)) {
|
|
1673
1682
|
const existing = readFileSync5(hookPath, "utf-8");
|
|
1674
|
-
if (containsPrimHook(existing)) {
|
|
1675
|
-
console.log(
|
|
1683
|
+
if (containsPrimHook(existing, spec.binName)) {
|
|
1684
|
+
console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
|
|
1676
1685
|
return;
|
|
1677
1686
|
}
|
|
1678
|
-
console.log(`A
|
|
1687
|
+
console.log(`A ${spec.hookName} hook already exists at ${hookPath}.`);
|
|
1679
1688
|
console.log("To replace it, run: prim hooks uninstall && prim hooks install");
|
|
1680
1689
|
return;
|
|
1681
1690
|
}
|
|
1682
|
-
writeFileSync3(hookPath,
|
|
1683
|
-
console.log(`Installed
|
|
1691
|
+
writeFileSync3(hookPath, dotGitScript(spec), { mode: 493 });
|
|
1692
|
+
console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
|
|
1693
|
+
}
|
|
1694
|
+
function installHooks(gitRoot, target) {
|
|
1695
|
+
for (const spec of HOOKS) {
|
|
1696
|
+
if (target === "husky") {
|
|
1697
|
+
installToHusky(gitRoot, spec);
|
|
1698
|
+
} else {
|
|
1699
|
+
installToDotGit(gitRoot, spec);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1684
1702
|
}
|
|
1685
1703
|
function registerHooksCommands(program2) {
|
|
1686
1704
|
const hooks = program2.command("hooks").description("Manage git hooks");
|
|
1687
|
-
hooks.command("install").description(
|
|
1705
|
+
hooks.command("install").description(
|
|
1706
|
+
"Install the prim git hooks \u2014 pre-commit + post-commit (auto-detects Husky; use --target to override)"
|
|
1707
|
+
).addOption(
|
|
1688
1708
|
new Option("--target <where>", "install destination; bypasses Husky detection").choices([
|
|
1689
1709
|
"husky",
|
|
1690
1710
|
"git-hooks"
|
|
@@ -1695,10 +1715,10 @@ function registerHooksCommands(program2) {
|
|
|
1695
1715
|
globals.nonInteractive || process.env.CI || process.env.PRIM_NON_INTERACTIVE
|
|
1696
1716
|
);
|
|
1697
1717
|
const gitRoot = getGitRoot();
|
|
1698
|
-
if (opts.target === "husky") return
|
|
1699
|
-
if (opts.target === "git-hooks") return
|
|
1718
|
+
if (opts.target === "husky") return installHooks(gitRoot, "husky");
|
|
1719
|
+
if (opts.target === "git-hooks") return installHooks(gitRoot, "git-hooks");
|
|
1700
1720
|
if (detectHusky(gitRoot)) {
|
|
1701
|
-
if (globals.yes) return
|
|
1721
|
+
if (globals.yes) return installHooks(gitRoot, "husky");
|
|
1702
1722
|
if (nonInteractive) {
|
|
1703
1723
|
throw new Error(
|
|
1704
1724
|
"--non-interactive set, refusing to prompt for Husky-hook installation. Pass --yes to confirm or --target=git-hooks to choose."
|
|
@@ -1709,24 +1729,30 @@ function registerHooksCommands(program2) {
|
|
|
1709
1729
|
"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
1730
|
);
|
|
1711
1731
|
} else if (await askConfirmation(
|
|
1712
|
-
"Husky detected. Install prim
|
|
1732
|
+
"Husky detected. Install prim hooks into .husky/ instead of .git/hooks/?"
|
|
1713
1733
|
)) {
|
|
1714
|
-
return
|
|
1734
|
+
return installHooks(gitRoot, "husky");
|
|
1715
1735
|
} else {
|
|
1716
|
-
console.log("Falling back to .git/hooks
|
|
1736
|
+
console.log("Falling back to .git/hooks install.");
|
|
1717
1737
|
}
|
|
1718
1738
|
}
|
|
1719
|
-
|
|
1739
|
+
installHooks(gitRoot, "git-hooks");
|
|
1720
1740
|
});
|
|
1721
|
-
hooks.command("uninstall").description("Remove the prim
|
|
1741
|
+
hooks.command("uninstall").description("Remove the prim git hooks (.git/hooks)").action(() => {
|
|
1722
1742
|
const gitRoot = getGitRoot();
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1743
|
+
for (const spec of HOOKS) {
|
|
1744
|
+
const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
|
|
1745
|
+
if (!existsSync4(hookPath)) {
|
|
1746
|
+
console.log(`No ${spec.hookName} hook found.`);
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
if (containsPrimHook(readFileSync5(hookPath, "utf-8"), spec.binName)) {
|
|
1750
|
+
unlinkSync2(hookPath);
|
|
1751
|
+
console.log(`Removed ${spec.hookName} hook at ${hookPath}`);
|
|
1752
|
+
} else {
|
|
1753
|
+
console.log(`Left ${spec.hookName} hook at ${hookPath} untouched (not a prim hook).`);
|
|
1754
|
+
}
|
|
1727
1755
|
}
|
|
1728
|
-
unlinkSync2(hookPath);
|
|
1729
|
-
console.log(`Removed pre-commit hook at ${hookPath}`);
|
|
1730
1756
|
});
|
|
1731
1757
|
}
|
|
1732
1758
|
|
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.18",
|
|
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/",
|