@primitive.ai/prim 0.1.0-alpha.29 → 0.1.0-alpha.30
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/dist/{chunk-JZGWQDM5.js → chunk-C4UYMR7X.js} +59 -8
- package/dist/hooks/post-commit.js +2 -1
- package/dist/hooks/post-tool-use.js +3 -3
- package/dist/hooks/pre-tool-use.js +3 -3
- package/dist/hooks/prim-hook.js +2 -1
- package/dist/hooks/session-start.js +3 -3
- package/dist/index.js +214 -47
- package/package.json +1 -1
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSiteUrl
|
|
3
|
+
} from "./chunk-26VA3ADF.js";
|
|
4
|
+
|
|
1
5
|
// src/journal.ts
|
|
2
6
|
import {
|
|
3
7
|
appendFileSync,
|
|
@@ -10,9 +14,15 @@ import {
|
|
|
10
14
|
import { homedir } from "os";
|
|
11
15
|
import { dirname, join } from "path";
|
|
12
16
|
var JOURNAL_DIR = join(homedir(), ".config", "prim", "moves");
|
|
13
|
-
var LEGACY_JOURNAL_PATH = join(JOURNAL_DIR, "journal.ndjson");
|
|
14
17
|
var UNBOUND_BUCKET = "_unbound";
|
|
15
18
|
var JOURNAL_BASENAME = "journal.ndjson";
|
|
19
|
+
function envSlug(apiUrl) {
|
|
20
|
+
const slug = apiUrl.replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
21
|
+
return slug.length > 0 ? slug : "default";
|
|
22
|
+
}
|
|
23
|
+
function currentEnvDir() {
|
|
24
|
+
return join(JOURNAL_DIR, envSlug(getSiteUrl()));
|
|
25
|
+
}
|
|
16
26
|
var DIR_MODE = 448;
|
|
17
27
|
var FILE_MODE = 384;
|
|
18
28
|
var SAFE_BUCKET = /^[A-Za-z0-9_-]+$/;
|
|
@@ -27,7 +37,7 @@ function appendMoveToPath(path, move) {
|
|
|
27
37
|
}
|
|
28
38
|
function bucketDir(orgId) {
|
|
29
39
|
const safe = orgId !== void 0 && SAFE_BUCKET.test(orgId) && !RESERVED_BUCKETS.has(orgId);
|
|
30
|
-
return join(
|
|
40
|
+
return join(currentEnvDir(), safe ? orgId : UNBOUND_BUCKET);
|
|
31
41
|
}
|
|
32
42
|
function journalPath(orgId) {
|
|
33
43
|
return join(bucketDir(orgId), JOURNAL_BASENAME);
|
|
@@ -54,23 +64,63 @@ function readMovesFromPath(path) {
|
|
|
54
64
|
}
|
|
55
65
|
function listBuckets() {
|
|
56
66
|
const out = [];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
if (!existsSync(JOURNAL_DIR)) {
|
|
67
|
+
const envDir = currentEnvDir();
|
|
68
|
+
if (!existsSync(envDir)) {
|
|
61
69
|
return out;
|
|
62
70
|
}
|
|
63
|
-
for (const entry of readdirSync(
|
|
71
|
+
for (const entry of readdirSync(envDir, { withFileTypes: true })) {
|
|
64
72
|
if (!entry.isDirectory()) {
|
|
65
73
|
continue;
|
|
66
74
|
}
|
|
67
|
-
const path = join(
|
|
75
|
+
const path = join(envDir, entry.name, JOURNAL_BASENAME);
|
|
68
76
|
if (existsSync(path)) {
|
|
69
77
|
out.push({ bucket: entry.name, path });
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
return out;
|
|
73
81
|
}
|
|
82
|
+
var FLUSHING_PREFIX = `${JOURNAL_BASENAME}.flushing.`;
|
|
83
|
+
function parseFlushingPid(name) {
|
|
84
|
+
const segments = name.slice(FLUSHING_PREFIX.length).split(".");
|
|
85
|
+
const last = segments.length >= 2 ? segments[segments.length - 1] : void 0;
|
|
86
|
+
return last !== void 0 && /^[0-9]+$/.test(last) ? Number(last) : void 0;
|
|
87
|
+
}
|
|
88
|
+
function listFlushingInDir(dir, bucket) {
|
|
89
|
+
if (!existsSync(dir)) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
94
|
+
if (!entry.isFile() || !entry.name.startsWith(FLUSHING_PREFIX)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const path = join(dir, entry.name);
|
|
98
|
+
const stat = statSync(path);
|
|
99
|
+
const lineCount = readFileSync(path, "utf-8").split("\n").filter((l) => l.length > 0).length;
|
|
100
|
+
out.push({
|
|
101
|
+
bucket,
|
|
102
|
+
path,
|
|
103
|
+
pid: parseFlushingPid(entry.name),
|
|
104
|
+
sizeBytes: stat.size,
|
|
105
|
+
mtimeMs: stat.mtimeMs,
|
|
106
|
+
lineCount
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
function listFlushing() {
|
|
112
|
+
const envDir = currentEnvDir();
|
|
113
|
+
if (!existsSync(envDir)) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const entry of readdirSync(envDir, { withFileTypes: true })) {
|
|
118
|
+
if (entry.isDirectory()) {
|
|
119
|
+
out.push(...listFlushingInDir(join(envDir, entry.name), entry.name));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
74
124
|
function bucketStats() {
|
|
75
125
|
return listBuckets().map(({ bucket, path }) => {
|
|
76
126
|
const stat = statSync(path);
|
|
@@ -193,6 +243,7 @@ export {
|
|
|
193
243
|
appendMove,
|
|
194
244
|
readMovesFromPath,
|
|
195
245
|
listBuckets,
|
|
246
|
+
listFlushing,
|
|
196
247
|
bucketStats,
|
|
197
248
|
SESSIONS_DIR,
|
|
198
249
|
resolveOrg
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
appendMove,
|
|
4
4
|
resolveOrg
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-C4UYMR7X.js";
|
|
6
6
|
import {
|
|
7
7
|
toCommitMove
|
|
8
8
|
} from "../chunk-7GHOFNJ2.js";
|
|
9
|
+
import "../chunk-26VA3ADF.js";
|
|
9
10
|
|
|
10
11
|
// src/hooks/post-commit.ts
|
|
11
12
|
import { execSync, spawn } from "child_process";
|
|
@@ -3,9 +3,6 @@ import {
|
|
|
3
3
|
bold,
|
|
4
4
|
color
|
|
5
5
|
} from "../chunk-4QJOQIY6.js";
|
|
6
|
-
import {
|
|
7
|
-
getClient
|
|
8
|
-
} from "../chunk-26VA3ADF.js";
|
|
9
6
|
import {
|
|
10
7
|
scrubFromCwd
|
|
11
8
|
} from "../chunk-6LAQVM26.js";
|
|
@@ -15,6 +12,9 @@ import {
|
|
|
15
12
|
import {
|
|
16
13
|
parseAgent
|
|
17
14
|
} from "../chunk-7YRBACIE.js";
|
|
15
|
+
import {
|
|
16
|
+
getClient
|
|
17
|
+
} from "../chunk-26VA3ADF.js";
|
|
18
18
|
|
|
19
19
|
// src/hooks/post-tool-use.ts
|
|
20
20
|
import { readFileSync } from "fs";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
parseAgent
|
|
4
|
+
} from "../chunk-7YRBACIE.js";
|
|
2
5
|
import {
|
|
3
6
|
getClient,
|
|
4
7
|
getSiteUrl
|
|
5
8
|
} from "../chunk-26VA3ADF.js";
|
|
6
|
-
import {
|
|
7
|
-
parseAgent
|
|
8
|
-
} from "../chunk-7YRBACIE.js";
|
|
9
9
|
import {
|
|
10
10
|
daemonRequest
|
|
11
11
|
} from "../chunk-UTKQTZHL.js";
|
package/dist/hooks/prim-hook.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
appendMove,
|
|
4
4
|
resolveOrg
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-C4UYMR7X.js";
|
|
6
6
|
import {
|
|
7
7
|
scrubFromCwd
|
|
8
8
|
} from "../chunk-6LAQVM26.js";
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
parseAgent
|
|
15
15
|
} from "../chunk-7YRBACIE.js";
|
|
16
|
+
import "../chunk-26VA3ADF.js";
|
|
16
17
|
|
|
17
18
|
// src/hooks/prim-hook.ts
|
|
18
19
|
import { spawn } from "child_process";
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,14 @@ import {
|
|
|
11
11
|
daemonOrDirectGet,
|
|
12
12
|
formatDecisionsWarning
|
|
13
13
|
} from "./chunk-UWAJCF7P.js";
|
|
14
|
+
import {
|
|
15
|
+
JOURNAL_DIR,
|
|
16
|
+
SESSIONS_DIR,
|
|
17
|
+
bucketStats,
|
|
18
|
+
listBuckets,
|
|
19
|
+
listFlushing,
|
|
20
|
+
readMovesFromPath
|
|
21
|
+
} from "./chunk-C4UYMR7X.js";
|
|
14
22
|
import {
|
|
15
23
|
HttpError,
|
|
16
24
|
REFRESH_TOKEN_PATH,
|
|
@@ -22,13 +30,6 @@ import {
|
|
|
22
30
|
getTokenExpiresAt,
|
|
23
31
|
saveTokenExpiry
|
|
24
32
|
} from "./chunk-26VA3ADF.js";
|
|
25
|
-
import {
|
|
26
|
-
JOURNAL_DIR,
|
|
27
|
-
SESSIONS_DIR,
|
|
28
|
-
bucketStats,
|
|
29
|
-
listBuckets,
|
|
30
|
-
readMovesFromPath
|
|
31
|
-
} from "./chunk-JZGWQDM5.js";
|
|
32
33
|
import {
|
|
33
34
|
daemonIsLive,
|
|
34
35
|
daemonRequest
|
|
@@ -1882,9 +1883,124 @@ function registerDecisionsCommands(program2) {
|
|
|
1882
1883
|
});
|
|
1883
1884
|
}
|
|
1884
1885
|
|
|
1886
|
+
// src/commands/doctor.ts
|
|
1887
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1888
|
+
var DAEMON_PROBE_TIMEOUT_MS = 500;
|
|
1889
|
+
var CONNECTIVITY_TIMEOUT_MS = 3e3;
|
|
1890
|
+
var MS_PER_SECOND = 1e3;
|
|
1891
|
+
var STALE_PENDING_MS = 6e4;
|
|
1892
|
+
var EXIT_UNHEALTHY = 1;
|
|
1893
|
+
function classifyDoctor(checks) {
|
|
1894
|
+
const status = checks.some((c) => c.status === "fail") ? "fail" : checks.some((c) => c.status === "warn") ? "warn" : "ok";
|
|
1895
|
+
return {
|
|
1896
|
+
json: { ok: status !== "fail", status, checks },
|
|
1897
|
+
exitCode: status === "fail" ? EXIT_UNHEALTHY : 0
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function checkAuth() {
|
|
1901
|
+
if (!getAuthToken()) {
|
|
1902
|
+
return { name: "auth", status: "fail", detail: "no token \u2014 run `prim auth login`" };
|
|
1903
|
+
}
|
|
1904
|
+
const expiresAt = getTokenExpiresAt();
|
|
1905
|
+
const hasRefresh = existsSync5(REFRESH_TOKEN_PATH);
|
|
1906
|
+
if (expiresAt !== void 0 && Date.now() >= expiresAt) {
|
|
1907
|
+
return hasRefresh ? { name: "auth", status: "warn", detail: "access token expired (refresh available)" } : {
|
|
1908
|
+
name: "auth",
|
|
1909
|
+
status: "fail",
|
|
1910
|
+
detail: "token expired, no refresh \u2014 run `prim auth login`"
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
if (!hasRefresh) {
|
|
1914
|
+
return { name: "auth", status: "warn", detail: "no refresh token \u2014 capture stops at expiry" };
|
|
1915
|
+
}
|
|
1916
|
+
const detail = expiresAt !== void 0 ? `valid (${String(Math.round((expiresAt - Date.now()) / MS_PER_SECOND))}s left)` : "valid";
|
|
1917
|
+
return { name: "auth", status: "ok", detail };
|
|
1918
|
+
}
|
|
1919
|
+
async function checkDaemon() {
|
|
1920
|
+
const live = await daemonIsLive(DAEMON_PROBE_TIMEOUT_MS);
|
|
1921
|
+
return live ? { name: "daemon", status: "ok", detail: "live" } : {
|
|
1922
|
+
name: "daemon",
|
|
1923
|
+
status: "warn",
|
|
1924
|
+
detail: "down \u2014 capture still journals; drains on next `prim` invocation"
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
function checkJournal() {
|
|
1928
|
+
const stats = bucketStats();
|
|
1929
|
+
const pending = stats.reduce((n, s) => n + s.lineCount, 0);
|
|
1930
|
+
if (pending === 0) {
|
|
1931
|
+
return { name: "journal", status: "ok", detail: "no pending moves" };
|
|
1932
|
+
}
|
|
1933
|
+
const oldestMs = Math.max(...stats.map((s) => Date.now() - s.mtimeMs));
|
|
1934
|
+
const oldestS = Math.round(oldestMs / MS_PER_SECOND);
|
|
1935
|
+
if (oldestMs > STALE_PENDING_MS) {
|
|
1936
|
+
return {
|
|
1937
|
+
name: "journal",
|
|
1938
|
+
status: "warn",
|
|
1939
|
+
detail: `${String(pending)} pending, oldest ${String(oldestS)}s \u2014 drain may be stalled`
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
return { name: "journal", status: "ok", detail: `${String(pending)} pending, draining` };
|
|
1943
|
+
}
|
|
1944
|
+
function checkStranded() {
|
|
1945
|
+
const stranded = listFlushing();
|
|
1946
|
+
if (stranded.length === 0) {
|
|
1947
|
+
return { name: "stranded", status: "ok", detail: "none" };
|
|
1948
|
+
}
|
|
1949
|
+
const moves = stranded.reduce((n, f) => n + f.lineCount, 0);
|
|
1950
|
+
return {
|
|
1951
|
+
name: "stranded",
|
|
1952
|
+
status: "warn",
|
|
1953
|
+
detail: `${String(moves)} move(s) in ${String(stranded.length)} file(s) \u2014 run \`prim moves flush\``
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
async function checkConnectivity() {
|
|
1957
|
+
try {
|
|
1958
|
+
await getClient().get("/api/cli/decisions/recent?limit=1", {
|
|
1959
|
+
signal: AbortSignal.timeout(CONNECTIVITY_TIMEOUT_MS)
|
|
1960
|
+
});
|
|
1961
|
+
return { name: "connectivity", status: "ok", detail: "server reachable" };
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1964
|
+
const status = message.includes("Authentication") ? "fail" : "warn";
|
|
1965
|
+
return { name: "connectivity", status, detail: message.slice(0, 80) };
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
async function collectChecks() {
|
|
1969
|
+
return [
|
|
1970
|
+
checkAuth(),
|
|
1971
|
+
await checkDaemon(),
|
|
1972
|
+
checkJournal(),
|
|
1973
|
+
checkStranded(),
|
|
1974
|
+
await checkConnectivity()
|
|
1975
|
+
];
|
|
1976
|
+
}
|
|
1977
|
+
function icon(status) {
|
|
1978
|
+
return status === "ok" ? "\u2713" : status === "warn" ? "\u26A0" : "\u2717";
|
|
1979
|
+
}
|
|
1980
|
+
async function runDoctor() {
|
|
1981
|
+
const checks = await collectChecks();
|
|
1982
|
+
const { json, exitCode } = classifyDoctor(checks);
|
|
1983
|
+
const headline = json.status === "ok" ? "\u2713 healthy" : json.status === "warn" ? "\u26A0 degraded" : "\u2717 unhealthy";
|
|
1984
|
+
process.stderr.write(`[prim] doctor: ${headline}
|
|
1985
|
+
`);
|
|
1986
|
+
for (const c of checks) {
|
|
1987
|
+
process.stderr.write(` ${icon(c.status)} ${c.name.padEnd(13)} ${c.detail}
|
|
1988
|
+
`);
|
|
1989
|
+
}
|
|
1990
|
+
console.log(JSON.stringify(json, null, 2));
|
|
1991
|
+
if (exitCode !== 0 && !process.exitCode) {
|
|
1992
|
+
process.exitCode = exitCode;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
function registerDoctorCommands(program2) {
|
|
1996
|
+
program2.command("doctor").description("Check capture-pipeline health end to end (auth, daemon, journal, server)").action(async () => {
|
|
1997
|
+
await runDoctor();
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1885
2001
|
// src/commands/hooks.ts
|
|
1886
2002
|
import { execSync as execSync2 } from "child_process";
|
|
1887
|
-
import { existsSync as
|
|
2003
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1888
2004
|
import { resolve } from "path";
|
|
1889
2005
|
import { Option } from "commander";
|
|
1890
2006
|
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
@@ -1927,11 +2043,11 @@ function getGitRoot() {
|
|
|
1927
2043
|
}
|
|
1928
2044
|
function detectHusky(gitRoot) {
|
|
1929
2045
|
const huskyDir = resolve(gitRoot, ".husky");
|
|
1930
|
-
if (!
|
|
1931
|
-
if (
|
|
1932
|
-
if (
|
|
2046
|
+
if (!existsSync6(huskyDir)) return false;
|
|
2047
|
+
if (existsSync6(resolve(huskyDir, "_"))) return true;
|
|
2048
|
+
if (existsSync6(resolve(huskyDir, "pre-commit"))) return true;
|
|
1933
2049
|
const pkgPath = resolve(gitRoot, "package.json");
|
|
1934
|
-
if (
|
|
2050
|
+
if (existsSync6(pkgPath)) {
|
|
1935
2051
|
try {
|
|
1936
2052
|
const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
1937
2053
|
const scripts = pkg2.scripts ?? {};
|
|
@@ -1960,7 +2076,7 @@ async function askConfirmation(question) {
|
|
|
1960
2076
|
}
|
|
1961
2077
|
function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
1962
2078
|
const hookPath = resolve(gitRoot, ".husky", spec.hookName);
|
|
1963
|
-
if (
|
|
2079
|
+
if (existsSync6(hookPath)) {
|
|
1964
2080
|
const existing = readFileSync5(hookPath, "utf-8");
|
|
1965
2081
|
if (containsPrimHook(existing, spec.binName)) {
|
|
1966
2082
|
console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
|
|
@@ -1985,10 +2101,10 @@ ${huskyBlock(spec)}
|
|
|
1985
2101
|
function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
1986
2102
|
const hooksDir = resolve(gitRoot, ".git", "hooks");
|
|
1987
2103
|
const hookPath = resolve(hooksDir, spec.hookName);
|
|
1988
|
-
if (!
|
|
2104
|
+
if (!existsSync6(hooksDir)) {
|
|
1989
2105
|
mkdirSync4(hooksDir, { recursive: true });
|
|
1990
2106
|
}
|
|
1991
|
-
if (
|
|
2107
|
+
if (existsSync6(hookPath)) {
|
|
1992
2108
|
const existing = readFileSync5(hookPath, "utf-8");
|
|
1993
2109
|
if (containsPrimHook(existing, spec.binName)) {
|
|
1994
2110
|
console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
|
|
@@ -2052,7 +2168,7 @@ function registerHooksCommands(program2) {
|
|
|
2052
2168
|
const gitRoot = getGitRoot();
|
|
2053
2169
|
for (const spec of HOOKS) {
|
|
2054
2170
|
const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
|
|
2055
|
-
if (!
|
|
2171
|
+
if (!existsSync6(hookPath)) {
|
|
2056
2172
|
console.log(`No ${spec.hookName} hook found.`);
|
|
2057
2173
|
continue;
|
|
2058
2174
|
}
|
|
@@ -2067,7 +2183,7 @@ function registerHooksCommands(program2) {
|
|
|
2067
2183
|
}
|
|
2068
2184
|
|
|
2069
2185
|
// src/commands/moves.ts
|
|
2070
|
-
import { existsSync as
|
|
2186
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2071
2187
|
import { join as join5 } from "path";
|
|
2072
2188
|
|
|
2073
2189
|
// src/flusher.ts
|
|
@@ -2075,35 +2191,70 @@ import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
|
2075
2191
|
var BATCH_SIZE = 500;
|
|
2076
2192
|
var HTTP_TIMEOUT_MS = 1e4;
|
|
2077
2193
|
var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
if (err.code === "ENOENT") {
|
|
2084
|
-
return 0;
|
|
2085
|
-
}
|
|
2086
|
-
throw err;
|
|
2194
|
+
var ORPHAN_QUARANTINE_MS = 6e4;
|
|
2195
|
+
function batchMoves(moves, size = BATCH_SIZE) {
|
|
2196
|
+
const batches = [];
|
|
2197
|
+
for (let i = 0; i < moves.length; i += size) {
|
|
2198
|
+
batches.push(moves.slice(i, i + size));
|
|
2087
2199
|
}
|
|
2088
|
-
|
|
2200
|
+
return batches;
|
|
2201
|
+
}
|
|
2202
|
+
async function drainFlushingPath(flushingPath) {
|
|
2203
|
+
const moves = readMovesFromPath(flushingPath);
|
|
2089
2204
|
if (moves.length === 0) {
|
|
2090
|
-
unlinkSync3(
|
|
2205
|
+
unlinkSync3(flushingPath);
|
|
2091
2206
|
return 0;
|
|
2092
2207
|
}
|
|
2093
2208
|
const client = getClient();
|
|
2094
|
-
for (
|
|
2095
|
-
const batch = moves.slice(i, i + BATCH_SIZE);
|
|
2209
|
+
for (const batch of batchMoves(moves)) {
|
|
2096
2210
|
await client.post(
|
|
2097
2211
|
"/api/cli/moves/ingest",
|
|
2098
2212
|
{ batch },
|
|
2099
2213
|
{ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) }
|
|
2100
2214
|
);
|
|
2101
2215
|
}
|
|
2102
|
-
unlinkSync3(
|
|
2216
|
+
unlinkSync3(flushingPath);
|
|
2103
2217
|
return moves.length;
|
|
2104
2218
|
}
|
|
2105
|
-
async function
|
|
2219
|
+
async function drainPath(path) {
|
|
2220
|
+
const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
|
|
2221
|
+
try {
|
|
2222
|
+
renameSync2(path, tmpPath);
|
|
2223
|
+
} catch (err) {
|
|
2224
|
+
if (err.code === "ENOENT") {
|
|
2225
|
+
return 0;
|
|
2226
|
+
}
|
|
2227
|
+
throw err;
|
|
2228
|
+
}
|
|
2229
|
+
return drainFlushingPath(tmpPath);
|
|
2230
|
+
}
|
|
2231
|
+
function processIsAlive2(pid) {
|
|
2232
|
+
try {
|
|
2233
|
+
process.kill(pid, 0);
|
|
2234
|
+
return true;
|
|
2235
|
+
} catch {
|
|
2236
|
+
return false;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
function selectRecoverable(files, now, opts = {}) {
|
|
2240
|
+
const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
|
|
2241
|
+
const isAlive = opts.isAlive ?? processIsAlive2;
|
|
2242
|
+
return files.filter(
|
|
2243
|
+
(f) => f.pid === void 0 ? now - f.mtimeMs > quarantineMs : !isAlive(f.pid)
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
async function recoverOrphans() {
|
|
2106
2247
|
let total = 0;
|
|
2248
|
+
for (const file of selectRecoverable(listFlushing(), Date.now())) {
|
|
2249
|
+
try {
|
|
2250
|
+
total += await drainFlushingPath(file.path);
|
|
2251
|
+
} catch {
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
return total;
|
|
2255
|
+
}
|
|
2256
|
+
async function flush() {
|
|
2257
|
+
let total = await recoverOrphans();
|
|
2107
2258
|
for (const { path } of listBuckets()) {
|
|
2108
2259
|
total += await drainPath(path);
|
|
2109
2260
|
}
|
|
@@ -2124,7 +2275,7 @@ async function flushIfNeeded() {
|
|
|
2124
2275
|
}
|
|
2125
2276
|
|
|
2126
2277
|
// src/commands/moves.ts
|
|
2127
|
-
var
|
|
2278
|
+
var MS_PER_SECOND2 = 1e3;
|
|
2128
2279
|
var DEFAULT_TAIL_LINES = "20";
|
|
2129
2280
|
var RADIX_DECIMAL = 10;
|
|
2130
2281
|
var ID_PREFIX_LEN = 8;
|
|
@@ -2142,17 +2293,32 @@ function registerMovesCommands(program2) {
|
|
|
2142
2293
|
});
|
|
2143
2294
|
moves.command("status").description("Show per-bucket pending stats").action(() => {
|
|
2144
2295
|
const stats = bucketStats();
|
|
2145
|
-
|
|
2296
|
+
const stranded = listFlushing();
|
|
2297
|
+
if (stats.length === 0 && stranded.length === 0) {
|
|
2146
2298
|
console.log("[prim] journal: empty");
|
|
2147
2299
|
return;
|
|
2148
2300
|
}
|
|
2149
2301
|
console.log(`[prim] root: ${JOURNAL_DIR}`);
|
|
2150
2302
|
for (const s of stats) {
|
|
2151
|
-
const ageS = Math.round((Date.now() - s.mtimeMs) /
|
|
2303
|
+
const ageS = Math.round((Date.now() - s.mtimeMs) / MS_PER_SECOND2);
|
|
2152
2304
|
console.log(
|
|
2153
2305
|
` ${s.bucket.padEnd(BUCKET_COL_WIDTH)} ${String(s.lineCount).padStart(5)} pending, ${String(s.sizeBytes).padStart(8)} bytes, last write ${String(ageS)}s ago`
|
|
2154
2306
|
);
|
|
2155
2307
|
}
|
|
2308
|
+
if (stranded.length > 0) {
|
|
2309
|
+
const moveCount = stranded.reduce((n, f) => n + f.lineCount, 0);
|
|
2310
|
+
const byteCount = stranded.reduce((n, f) => n + f.sizeBytes, 0);
|
|
2311
|
+
console.log(
|
|
2312
|
+
`[prim] \u26A0 ${String(stranded.length)} stranded flush file(s): ${String(moveCount)} move(s), ${String(byteCount)} bytes \u2014 recover with \`prim moves flush\``
|
|
2313
|
+
);
|
|
2314
|
+
for (const f of stranded) {
|
|
2315
|
+
const ageS = Math.round((Date.now() - f.mtimeMs) / MS_PER_SECOND2);
|
|
2316
|
+
const owner = f.pid === void 0 ? "no pid" : `pid ${String(f.pid)}`;
|
|
2317
|
+
console.log(
|
|
2318
|
+
` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${String(f.lineCount).padStart(5)} stranded, ${String(f.sizeBytes).padStart(8)} bytes, ${String(ageS)}s ago (${owner})`
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2156
2322
|
});
|
|
2157
2323
|
moves.command("tail").description("Pretty-print recent journal entries across all buckets").option("-n, --lines <n>", "number of lines to tail", DEFAULT_TAIL_LINES).action((opts) => {
|
|
2158
2324
|
const lines = Number.parseInt(opts.lines, RADIX_DECIMAL);
|
|
@@ -2178,7 +2344,7 @@ function registerMovesCommands(program2) {
|
|
|
2178
2344
|
});
|
|
2179
2345
|
moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
|
|
2180
2346
|
const dir = join5(process.cwd(), ".prim");
|
|
2181
|
-
if (!
|
|
2347
|
+
if (!existsSync7(dir)) {
|
|
2182
2348
|
mkdirSync5(dir, { recursive: true, mode: DIR_MODE });
|
|
2183
2349
|
}
|
|
2184
2350
|
const file = join5(process.cwd(), WORKSPACE_FILE);
|
|
@@ -2189,7 +2355,7 @@ function registerMovesCommands(program2) {
|
|
|
2189
2355
|
});
|
|
2190
2356
|
moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
|
|
2191
2357
|
const file = join5(process.cwd(), WORKSPACE_FILE);
|
|
2192
|
-
if (!
|
|
2358
|
+
if (!existsSync7(file)) {
|
|
2193
2359
|
console.log("[prim] no workspace binding in cwd");
|
|
2194
2360
|
return;
|
|
2195
2361
|
}
|
|
@@ -2282,7 +2448,7 @@ function registerReconcileCommands(program2) {
|
|
|
2282
2448
|
|
|
2283
2449
|
// src/commands/session.ts
|
|
2284
2450
|
import {
|
|
2285
|
-
existsSync as
|
|
2451
|
+
existsSync as existsSync8,
|
|
2286
2452
|
mkdirSync as mkdirSync6,
|
|
2287
2453
|
readFileSync as readFileSync6,
|
|
2288
2454
|
readdirSync,
|
|
@@ -2293,7 +2459,7 @@ import { join as join6 } from "path";
|
|
|
2293
2459
|
var DIR_MODE2 = 448;
|
|
2294
2460
|
var FILE_MODE3 = 384;
|
|
2295
2461
|
function ensureDir() {
|
|
2296
|
-
if (!
|
|
2462
|
+
if (!existsSync8(SESSIONS_DIR)) {
|
|
2297
2463
|
mkdirSync6(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
|
|
2298
2464
|
}
|
|
2299
2465
|
}
|
|
@@ -2314,7 +2480,7 @@ function registerSessionCommands(program2) {
|
|
|
2314
2480
|
console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
|
|
2315
2481
|
});
|
|
2316
2482
|
session.command("list").description("List active session markers").action(() => {
|
|
2317
|
-
if (!
|
|
2483
|
+
if (!existsSync8(SESSIONS_DIR)) {
|
|
2318
2484
|
console.log("[prim] no session markers");
|
|
2319
2485
|
return;
|
|
2320
2486
|
}
|
|
@@ -2334,7 +2500,7 @@ function registerSessionCommands(program2) {
|
|
|
2334
2500
|
});
|
|
2335
2501
|
session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
|
|
2336
2502
|
const p = markerPath(sessionId);
|
|
2337
|
-
if (!
|
|
2503
|
+
if (!existsSync8(p)) {
|
|
2338
2504
|
console.log(`[prim] no marker for session ${sessionId}`);
|
|
2339
2505
|
return;
|
|
2340
2506
|
}
|
|
@@ -2436,7 +2602,7 @@ function registerSetupCommand(program2) {
|
|
|
2436
2602
|
// src/commands/skill.ts
|
|
2437
2603
|
import {
|
|
2438
2604
|
closeSync as closeSync3,
|
|
2439
|
-
existsSync as
|
|
2605
|
+
existsSync as existsSync9,
|
|
2440
2606
|
fsyncSync as fsyncSync2,
|
|
2441
2607
|
openSync as openSync3,
|
|
2442
2608
|
readFileSync as readFileSync7,
|
|
@@ -2461,13 +2627,13 @@ function loadSkill() {
|
|
|
2461
2627
|
let dir = __dirname;
|
|
2462
2628
|
while (dir !== dirname4(dir)) {
|
|
2463
2629
|
const p = resolve2(dir, "SKILL.md");
|
|
2464
|
-
if (
|
|
2630
|
+
if (existsSync9(p)) return readFileSync7(p, "utf-8");
|
|
2465
2631
|
dir = dirname4(dir);
|
|
2466
2632
|
}
|
|
2467
2633
|
throw new Error("SKILL.md not found in package");
|
|
2468
2634
|
}
|
|
2469
2635
|
function detectTargets(cwd) {
|
|
2470
|
-
return TARGET_CANDIDATES.filter((p) =>
|
|
2636
|
+
return TARGET_CANDIDATES.filter((p) => existsSync9(resolve2(cwd, p)));
|
|
2471
2637
|
}
|
|
2472
2638
|
function detectNewline(content) {
|
|
2473
2639
|
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -2516,7 +2682,7 @@ function resolveTarget(cwd, override) {
|
|
|
2516
2682
|
function runInstall(cwd, opts) {
|
|
2517
2683
|
const target = resolveTarget(cwd, opts.target);
|
|
2518
2684
|
if (target === null) return 1;
|
|
2519
|
-
const existing =
|
|
2685
|
+
const existing = existsSync9(target) ? readFileSync7(target, "utf-8") : "";
|
|
2520
2686
|
const eol = existing ? detectNewline(existing) : "\n";
|
|
2521
2687
|
const block = composeBlock(loadSkill(), eol);
|
|
2522
2688
|
const next = applyBlock(existing, block, eol);
|
|
@@ -2535,7 +2701,7 @@ function runInstall(cwd, opts) {
|
|
|
2535
2701
|
function runUninstall(cwd, opts) {
|
|
2536
2702
|
const target = resolveTarget(cwd, opts.target);
|
|
2537
2703
|
if (target === null) return 1;
|
|
2538
|
-
if (!
|
|
2704
|
+
if (!existsSync9(target)) {
|
|
2539
2705
|
console.log(`Skill block not present at ${target}`);
|
|
2540
2706
|
return 0;
|
|
2541
2707
|
}
|
|
@@ -2552,7 +2718,7 @@ function runUninstall(cwd, opts) {
|
|
|
2552
2718
|
function runStatus(cwd, opts) {
|
|
2553
2719
|
const target = resolveTarget(cwd, opts.target);
|
|
2554
2720
|
if (target === null) return 1;
|
|
2555
|
-
const fileExists =
|
|
2721
|
+
const fileExists = existsSync9(target);
|
|
2556
2722
|
let installed = false;
|
|
2557
2723
|
if (fileExists) {
|
|
2558
2724
|
const content = readFileSync7(target, "utf-8");
|
|
@@ -2787,6 +2953,7 @@ registerDecisionsCommands(program);
|
|
|
2787
2953
|
registerClaudeCommands(program);
|
|
2788
2954
|
registerCodexCommands(program);
|
|
2789
2955
|
registerDaemonCommands(program);
|
|
2956
|
+
registerDoctorCommands(program);
|
|
2790
2957
|
registerReconcileCommands(program);
|
|
2791
2958
|
registerStatuslineCommands(program);
|
|
2792
2959
|
registerWelcomeCommand(program);
|
package/package.json
CHANGED