tenjin-cli 0.1.0-alpha.0 → 0.1.0-alpha.2
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/NOTICE.md +33 -0
- package/README.md +198 -18
- package/dist/{_esm-ZH2J4QEH.js → _esm-C5CRWCYU.js} +2 -2
- package/dist/_esm-FEASBWRP.js +881 -0
- package/dist/buy-U35DQ6XM.js +1598 -0
- package/dist/buy-U35DQ6XM.js.map +1 -0
- package/dist/candidate-RLYCQMF5.js +127 -0
- package/dist/candidate-RLYCQMF5.js.map +1 -0
- package/dist/{ccip-FXWZDDF5.js → ccip-NQXCEK2U.js} +6 -5
- package/dist/ccip-XR5NE5QK.js +19 -0
- package/dist/chunk-27ONARHV.js +171 -0
- package/dist/chunk-27ONARHV.js.map +1 -0
- package/dist/chunk-2I4QTOLB.js +11 -0
- package/dist/chunk-2I4QTOLB.js.map +1 -0
- package/dist/chunk-3OV7E5G5.js +18 -0
- package/dist/chunk-3OV7E5G5.js.map +1 -0
- package/dist/chunk-5CLSD5YN.js +209 -0
- package/dist/chunk-5CLSD5YN.js.map +1 -0
- package/dist/chunk-7KUW4MJD.js +59 -0
- package/dist/chunk-7KUW4MJD.js.map +1 -0
- package/dist/{doctor-MVLZRVPU.js → chunk-7PHSZKQS.js} +88 -93
- package/dist/chunk-7PHSZKQS.js.map +1 -0
- package/dist/chunk-CYOF7ROH.js +70 -0
- package/dist/chunk-CYOF7ROH.js.map +1 -0
- package/dist/chunk-HIVZG55D.js +204 -0
- package/dist/chunk-HIVZG55D.js.map +1 -0
- package/dist/chunk-HSDCI7OV.js +7806 -0
- package/dist/chunk-HSDCI7OV.js.map +1 -0
- package/dist/{chunk-LTTRND4A.js → chunk-IYLHQMT5.js} +85 -9
- package/dist/chunk-IYLHQMT5.js.map +1 -0
- package/dist/chunk-JPHG2JW3.js +96 -0
- package/dist/chunk-JPHG2JW3.js.map +1 -0
- package/dist/{chunk-ZPZFPVDN.js → chunk-MUBUQVC3.js} +1 -1
- package/dist/chunk-MUBUQVC3.js.map +1 -0
- package/dist/chunk-N62ROTMC.js +79 -0
- package/dist/chunk-N62ROTMC.js.map +1 -0
- package/dist/{chunk-SXQQ2ZSR.js → chunk-NCE5NSN6.js} +186 -18
- package/dist/chunk-NCE5NSN6.js.map +1 -0
- package/dist/{chunk-FIYZLITH.js → chunk-OYMYCW7J.js} +23 -4
- package/dist/{chunk-FIYZLITH.js.map → chunk-OYMYCW7J.js.map} +1 -1
- package/dist/chunk-QNP7H47L.js +152 -0
- package/dist/chunk-QNP7H47L.js.map +1 -0
- package/dist/{chunk-TX5T3LKJ.js → chunk-QVX5MWJZ.js} +21 -17
- package/dist/{chunk-TX5T3LKJ.js.map → chunk-QVX5MWJZ.js.map} +1 -1
- package/dist/{chunk-CPUZRZ2A.js → chunk-RBJPAZFI.js} +25 -170
- package/dist/chunk-RBJPAZFI.js.map +1 -0
- package/dist/chunk-RRN3YTK7.js +6199 -0
- package/dist/chunk-RRN3YTK7.js.map +1 -0
- package/dist/chunk-RY4XKNV3.js +140 -0
- package/dist/chunk-RY4XKNV3.js.map +1 -0
- package/dist/chunk-S3IGX57D.js +14682 -0
- package/dist/chunk-S3IGX57D.js.map +1 -0
- package/dist/{config-XVXZ3V2Q.js → chunk-TFGSOPWR.js} +117 -62
- package/dist/chunk-TFGSOPWR.js.map +1 -0
- package/dist/chunk-TLCMMYLY.js +126 -0
- package/dist/chunk-TLCMMYLY.js.map +1 -0
- package/dist/{chunk-PAAL45FJ.js → chunk-UGX7YPX2.js} +3446 -448
- package/dist/chunk-UGX7YPX2.js.map +1 -0
- package/dist/chunk-UO22YNY4.js +69 -0
- package/dist/chunk-UO22YNY4.js.map +1 -0
- package/dist/chunk-UPIEAMGH.js +160 -0
- package/dist/chunk-UPIEAMGH.js.map +1 -0
- package/dist/chunk-XJIW7USF.js +217 -0
- package/dist/chunk-XJIW7USF.js.map +1 -0
- package/dist/{chunk-F5DZJLU3.js → chunk-YALGASEU.js} +2065 -1258
- package/dist/chunk-YALGASEU.js.map +1 -0
- package/dist/chunk-YBMMV4Y4.js +51 -0
- package/dist/chunk-YBMMV4Y4.js.map +1 -0
- package/dist/chunk-YIYQ2ZNT.js +286 -0
- package/dist/chunk-YIYQ2ZNT.js.map +1 -0
- package/dist/{chunk-376LHMK2.js → chunk-YN5SHIXQ.js} +36 -10
- package/dist/chunk-YN5SHIXQ.js.map +1 -0
- package/dist/chunk-Z52RA3NC.js +137 -0
- package/dist/chunk-Z52RA3NC.js.map +1 -0
- package/dist/chunk-Z6MFJV5Z.js +1330 -0
- package/dist/chunk-Z6MFJV5Z.js.map +1 -0
- package/dist/chunk-ZABPQJ3M.js +83 -0
- package/dist/chunk-ZABPQJ3M.js.map +1 -0
- package/dist/{chunk-WOHCAMDQ.js → chunk-ZMHYXMO6.js} +1887 -1029
- package/dist/chunk-ZMHYXMO6.js.map +1 -0
- package/dist/{chunk-3IMLRUZE.js → chunk-ZXTAQYNA.js} +6 -2
- package/dist/{cli-LMHGCZ5S.js → cli-4NNBEQYH.js} +175 -179
- package/dist/cli-4NNBEQYH.js.map +1 -0
- package/dist/config-2VSXWWHK.js +21 -0
- package/dist/doctor-JJPNIBYH.js +20 -0
- package/dist/doctor-JJPNIBYH.js.map +1 -0
- package/dist/ethersCompat-Df4a6QIZ-3YZIAXDK.js +18 -0
- package/dist/ethersCompat-Df4a6QIZ-3YZIAXDK.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/inspect-JGYJWFDP.js +96 -0
- package/dist/inspect-JGYJWFDP.js.map +1 -0
- package/dist/install-ZQRMSRGW.js +378 -0
- package/dist/install-ZQRMSRGW.js.map +1 -0
- package/dist/lookup-WYVTOMVY.js +113 -0
- package/dist/lookup-WYVTOMVY.js.map +1 -0
- package/dist/{mine.wasm-GQECMSGN.js → mine.wasm-6PIT3DK6.js} +2 -2
- package/dist/outcome-HKKT6QIA.js +66 -0
- package/dist/outcome-HKKT6QIA.js.map +1 -0
- package/dist/publish-D76B3MBK.js +1480 -0
- package/dist/publish-D76B3MBK.js.map +1 -0
- package/dist/{secp256k1-35YNNB6F.js → secp256k1-N3ZHU7LB.js} +5 -4
- package/dist/secp256k1-N3ZHU7LB.js.map +1 -0
- package/dist/usdc-JIAJW4SV.js +22 -0
- package/dist/usdc-JIAJW4SV.js.map +1 -0
- package/dist/viemAdapter-By13KInL-ZZE6UV4Y.js +14 -0
- package/dist/viemAdapter-By13KInL-ZZE6UV4Y.js.map +1 -0
- package/dist/wallet-6QKS6UHW.js +179 -0
- package/dist/wallet-6QKS6UHW.js.map +1 -0
- package/dist/wallet-PA54CRBL.js +30 -0
- package/dist/wallet-PA54CRBL.js.map +1 -0
- package/package.json +20 -15
- package/skills/tenjin/SKILL.md +199 -0
- package/skills/tenjin-publish/SKILL.md +96 -0
- package/skills/tenjin-search/SKILL.md +122 -0
- package/dist/chunk-376LHMK2.js.map +0 -1
- package/dist/chunk-CPUZRZ2A.js.map +0 -1
- package/dist/chunk-F5DZJLU3.js.map +0 -1
- package/dist/chunk-LTTRND4A.js.map +0 -1
- package/dist/chunk-PAAL45FJ.js.map +0 -1
- package/dist/chunk-QHIEYBLV.js +0 -320
- package/dist/chunk-QHIEYBLV.js.map +0 -1
- package/dist/chunk-SXQQ2ZSR.js.map +0 -1
- package/dist/chunk-WOHCAMDQ.js.map +0 -1
- package/dist/chunk-ZPZFPVDN.js.map +0 -1
- package/dist/cli-LMHGCZ5S.js.map +0 -1
- package/dist/config-XVXZ3V2Q.js.map +0 -1
- package/dist/doctor-MVLZRVPU.js.map +0 -1
- package/dist/usdc-OQUOFF6R.js +0 -18
- package/dist/wallet-NRKWLKHX.js +0 -153
- package/dist/wallet-NRKWLKHX.js.map +0 -1
- package/dist/wallet-QUWK2PA4.js +0 -22
- /package/dist/{_esm-ZH2J4QEH.js.map → _esm-C5CRWCYU.js.map} +0 -0
- /package/dist/{ccip-FXWZDDF5.js.map → _esm-FEASBWRP.js.map} +0 -0
- /package/dist/{chunk-3IMLRUZE.js.map → ccip-NQXCEK2U.js.map} +0 -0
- /package/dist/{secp256k1-35YNNB6F.js.map → ccip-XR5NE5QK.js.map} +0 -0
- /package/dist/{usdc-OQUOFF6R.js.map → chunk-ZXTAQYNA.js.map} +0 -0
- /package/dist/{wallet-QUWK2PA4.js.map → config-2VSXWWHK.js.map} +0 -0
- /package/dist/{mine.wasm-GQECMSGN.js.map → mine.wasm-6PIT3DK6.js.map} +0 -0
|
@@ -1,72 +1,48 @@
|
|
|
1
1
|
import { createRequire as __tenjinCreateRequire } from 'node:module'; const require = __tenjinCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
loadProjectConfig
|
|
4
|
+
} from "./chunk-UPIEAMGH.js";
|
|
2
5
|
import {
|
|
3
6
|
CONFIG_KEYS,
|
|
7
|
+
PUBLISH_CONFIG_KEYS,
|
|
8
|
+
PublishModeSchema,
|
|
4
9
|
RawConfigSchema,
|
|
5
10
|
loadRawConfig,
|
|
6
11
|
parseUsdToAtomic,
|
|
7
12
|
resolveSettings,
|
|
8
13
|
toMoney
|
|
9
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-IYLHQMT5.js";
|
|
15
|
+
import {
|
|
16
|
+
LockTimeoutError,
|
|
17
|
+
withFileLock
|
|
18
|
+
} from "./chunk-YBMMV4Y4.js";
|
|
10
19
|
import {
|
|
11
20
|
writeFileAtomic
|
|
12
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-MUBUQVC3.js";
|
|
13
22
|
import {
|
|
14
23
|
CliError,
|
|
15
24
|
configPath
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-3IMLRUZE.js";
|
|
25
|
+
} from "./chunk-OYMYCW7J.js";
|
|
18
26
|
|
|
19
27
|
// src/commands/config.ts
|
|
20
|
-
import { mkdir
|
|
28
|
+
import { mkdir } from "fs/promises";
|
|
21
29
|
import { styleText } from "util";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.lockPath = lockPath;
|
|
35
|
-
}
|
|
30
|
+
var CONFIRM_ABOVE = "above:";
|
|
31
|
+
var KEY_WIDTH = Math.max(...[...CONFIG_KEYS, ...PUBLISH_CONFIG_KEYS].map((key) => key.length));
|
|
32
|
+
var KEY_DESCRIPTIONS = {
|
|
33
|
+
maxAutoSpend: "auto-approve a read up to this amount",
|
|
34
|
+
sessionBudget: "cap on total auto-spend per session",
|
|
35
|
+
confirm: "when to ask before paying",
|
|
36
|
+
allowlistCreators: "only auto-pay these creators (empty = any)",
|
|
37
|
+
baseUrl: "Tenjin API base URL",
|
|
38
|
+
rpcUrl: "Base RPC endpoint for balance reads",
|
|
39
|
+
evalCohort: "opt in to the lookup evaluation cohort",
|
|
40
|
+
"publish.mode": "review=always ask, auto=ask on findings, full-auto=only hard blocks stop it",
|
|
41
|
+
"publish.defaultPrice": "price used when none is given"
|
|
36
42
|
};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const metaPath = join(lockPath, "meta");
|
|
40
|
-
const deadline = Date.now() + timeoutMs;
|
|
41
|
-
for (; ; ) {
|
|
42
|
-
try {
|
|
43
|
-
await mkdir(lockPath);
|
|
44
|
-
break;
|
|
45
|
-
} catch (err) {
|
|
46
|
-
if (!isEexist(err)) throw err;
|
|
47
|
-
if (Date.now() >= deadline) throw new LockTimeoutError(lockPath, timeoutMs);
|
|
48
|
-
await delay(retryMs);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
await writeFile(metaPath, JSON.stringify({ pid: process.pid, acquiredAt: Date.now() })).catch(
|
|
52
|
-
() => void 0
|
|
53
|
-
);
|
|
54
|
-
try {
|
|
55
|
-
return await fn();
|
|
56
|
-
} finally {
|
|
57
|
-
await rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
58
|
-
}
|
|
43
|
+
function isPublishKey(key) {
|
|
44
|
+
return PUBLISH_CONFIG_KEYS.includes(key);
|
|
59
45
|
}
|
|
60
|
-
function isEexist(err) {
|
|
61
|
-
return typeof err === "object" && err !== null && err.code === "EEXIST";
|
|
62
|
-
}
|
|
63
|
-
function delay(ms) {
|
|
64
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// src/commands/config.ts
|
|
68
|
-
var CONFIRM_ABOVE = "above:";
|
|
69
|
-
var KEY_WIDTH = Math.max(...CONFIG_KEYS.map((key) => key.length));
|
|
70
46
|
async function runConfigList(ctx) {
|
|
71
47
|
const settings = await resolveFromContext(ctx);
|
|
72
48
|
const data = {};
|
|
@@ -74,38 +50,90 @@ async function runConfigList(ctx) {
|
|
|
74
50
|
for (const key of CONFIG_KEYS) {
|
|
75
51
|
const entry = renderSetting(key, settings[key].value, settings[key].source);
|
|
76
52
|
data[key] = entry;
|
|
77
|
-
humanLines.push(
|
|
53
|
+
humanLines.push(describedLine(key, entry));
|
|
54
|
+
}
|
|
55
|
+
for (const key of PUBLISH_CONFIG_KEYS) {
|
|
56
|
+
const entry = renderPublishSetting(key, settings);
|
|
57
|
+
data[key] = entry;
|
|
58
|
+
humanLines.push(describedLine(key, entry, downgradeNote(key, settings)));
|
|
78
59
|
}
|
|
79
60
|
return { data, humanLines };
|
|
80
61
|
}
|
|
81
62
|
async function runConfigGet({ key }, ctx) {
|
|
63
|
+
if (isPublishKey(key)) {
|
|
64
|
+
const settings2 = await resolveFromContext(ctx);
|
|
65
|
+
const entry2 = renderPublishSetting(key, settings2);
|
|
66
|
+
return {
|
|
67
|
+
data: { key, ...entry2 },
|
|
68
|
+
humanLines: [withNote(formatLine(key, entry2), downgradeNote(key, settings2))]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
82
71
|
const configKey = assertKey(key);
|
|
83
72
|
const settings = await resolveFromContext(ctx);
|
|
84
73
|
const entry = renderSetting(configKey, settings[configKey].value, settings[configKey].source);
|
|
85
74
|
return { data: { key: configKey, ...entry }, humanLines: [formatLine(configKey, entry)] };
|
|
86
75
|
}
|
|
87
76
|
async function runConfigSet({ key, value }, ctx) {
|
|
77
|
+
if (isPublishKey(key)) return setPublishKey(key, value, ctx);
|
|
88
78
|
const configKey = assertKey(key);
|
|
89
79
|
const stored = parseValue(configKey, value);
|
|
90
|
-
await persist(ctx.dataDir,
|
|
80
|
+
await persist(ctx.dataDir, (existing) => ({ ...existing, [configKey]: stored }));
|
|
91
81
|
const entry = renderSetting(configKey, stored, "file");
|
|
92
82
|
return { data: { key: configKey, ...entry }, humanLines: [formatLine(configKey, entry)] };
|
|
93
83
|
}
|
|
84
|
+
async function setPublishKey(key, value, ctx) {
|
|
85
|
+
const entry = key === "publish.mode" ? { value: parsePublishMode(value), source: "file" } : { value: toMoney(parseUsdToAtomic(value)), source: "file" };
|
|
86
|
+
const subkey = key === "publish.mode" ? "mode" : "defaultPrice";
|
|
87
|
+
const stored = key === "publish.mode" ? entry.value : entry.value.atomic;
|
|
88
|
+
await persist(ctx.dataDir, (existing) => ({
|
|
89
|
+
...existing,
|
|
90
|
+
publish: { ...existing.publish, [subkey]: stored }
|
|
91
|
+
}));
|
|
92
|
+
return { data: { key, ...entry }, humanLines: [formatLine(key, entry)] };
|
|
93
|
+
}
|
|
94
|
+
function parsePublishMode(value) {
|
|
95
|
+
const parsed = PublishModeSchema.safeParse(value);
|
|
96
|
+
if (parsed.success) return parsed.data;
|
|
97
|
+
throw new CliError("USAGE", `Invalid publish mode: ${JSON.stringify(value)}`, {
|
|
98
|
+
fix: 'Use "review", "auto", or "full-auto".'
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function persistPublishMode(dir, mode) {
|
|
102
|
+
await persist(dir, (existing) => ({
|
|
103
|
+
...existing,
|
|
104
|
+
publish: { ...existing.publish, mode }
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
94
107
|
async function resolveFromContext(ctx) {
|
|
95
108
|
const config = await loadRawConfig(ctx.dataDir);
|
|
96
|
-
|
|
109
|
+
const project = await loadProjectConfig(process.cwd());
|
|
110
|
+
return resolveSettings({
|
|
111
|
+
config,
|
|
112
|
+
flags: { baseUrl: ctx.flags.baseUrl },
|
|
113
|
+
env: process.env,
|
|
114
|
+
project: project?.layer
|
|
115
|
+
});
|
|
97
116
|
}
|
|
98
117
|
function assertKey(key) {
|
|
99
118
|
if (CONFIG_KEYS.includes(key)) return key;
|
|
100
119
|
throw new CliError("USAGE", `Unknown config key: ${JSON.stringify(key)}`, {
|
|
101
|
-
fix: `Valid keys: ${CONFIG_KEYS.join(", ")}.`
|
|
120
|
+
fix: `Valid keys: ${[...CONFIG_KEYS, ...PUBLISH_CONFIG_KEYS].join(", ")}.`
|
|
102
121
|
});
|
|
103
122
|
}
|
|
104
123
|
function renderSetting(key, stored, source) {
|
|
105
124
|
return { ...renderValue(key, stored), source };
|
|
106
125
|
}
|
|
126
|
+
function renderPublishSetting(key, settings) {
|
|
127
|
+
if (key === "publish.mode") {
|
|
128
|
+
return { value: settings.publishMode.value, source: settings.publishMode.source };
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
value: toMoney(settings.publishDefaultPrice.value),
|
|
132
|
+
source: settings.publishDefaultPrice.source
|
|
133
|
+
};
|
|
134
|
+
}
|
|
107
135
|
function renderValue(key, stored) {
|
|
108
|
-
if (Array.isArray(stored)) return { value: stored };
|
|
136
|
+
if (Array.isArray(stored) || typeof stored === "boolean") return { value: stored };
|
|
109
137
|
if (key === "maxAutoSpend" || key === "sessionBudget") return { value: toMoney(stored) };
|
|
110
138
|
if (key === "confirm" && stored.startsWith(CONFIRM_ABOVE)) {
|
|
111
139
|
return { value: stored, threshold: toMoney(stored.slice(CONFIRM_ABOVE.length)) };
|
|
@@ -125,8 +153,17 @@ function parseValue(key, value) {
|
|
|
125
153
|
case "baseUrl":
|
|
126
154
|
case "rpcUrl":
|
|
127
155
|
return parseHttpUrl(value);
|
|
156
|
+
case "evalCohort":
|
|
157
|
+
return parseBoolean(value);
|
|
128
158
|
}
|
|
129
159
|
}
|
|
160
|
+
function parseBoolean(value) {
|
|
161
|
+
if (value === "true") return true;
|
|
162
|
+
if (value === "false") return false;
|
|
163
|
+
throw new CliError("USAGE", `Invalid boolean value: ${JSON.stringify(value)}`, {
|
|
164
|
+
fix: 'Use "true" or "false".'
|
|
165
|
+
});
|
|
166
|
+
}
|
|
130
167
|
function parseConfirm(value) {
|
|
131
168
|
if (value === "always") return "always";
|
|
132
169
|
if (value.startsWith(CONFIRM_ABOVE)) {
|
|
@@ -163,13 +200,13 @@ function parseHttpUrl(value) {
|
|
|
163
200
|
}
|
|
164
201
|
return value;
|
|
165
202
|
}
|
|
166
|
-
async function persist(dir,
|
|
167
|
-
await
|
|
203
|
+
async function persist(dir, merge) {
|
|
204
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
168
205
|
const lockPath = `${configPath(dir)}.lock`;
|
|
169
206
|
try {
|
|
170
207
|
await withFileLock(lockPath, async () => {
|
|
171
208
|
const existing = await loadRawConfig(dir);
|
|
172
|
-
const merged =
|
|
209
|
+
const merged = merge(existing);
|
|
173
210
|
const validated = RawConfigSchema.parse(merged);
|
|
174
211
|
await writeFileAtomic(configPath(dir), `${JSON.stringify(validated, null, 2)}
|
|
175
212
|
`, {
|
|
@@ -191,16 +228,34 @@ function formatLine(key, entry) {
|
|
|
191
228
|
const label = key.padEnd(KEY_WIDTH);
|
|
192
229
|
return ` ${label} ${displayValue(entry)} ${styleText("dim", `(${entry.source})`)}`;
|
|
193
230
|
}
|
|
231
|
+
function describedLine(key, entry, note) {
|
|
232
|
+
const description = KEY_DESCRIPTIONS[key];
|
|
233
|
+
let line = formatLine(key, entry);
|
|
234
|
+
if (description !== void 0) line += ` ${styleText("dim", `- ${description}`)}`;
|
|
235
|
+
return withNote(line, note);
|
|
236
|
+
}
|
|
237
|
+
function withNote(line, note) {
|
|
238
|
+
return note !== void 0 ? `${line} ${styleText("dim", `(${note})`)}` : line;
|
|
239
|
+
}
|
|
240
|
+
function downgradeNote(key, settings) {
|
|
241
|
+
if (key !== "publish.mode" || settings.publishMode.downgradedWarning === void 0) {
|
|
242
|
+
return void 0;
|
|
243
|
+
}
|
|
244
|
+
return "downgraded from full-auto: committed .tenjin.json (gitignore it to opt in)";
|
|
245
|
+
}
|
|
194
246
|
function displayValue(entry) {
|
|
195
247
|
const { value } = entry;
|
|
196
248
|
if (Array.isArray(value)) return value.length > 0 ? value.join(", ") : "(empty)";
|
|
249
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
197
250
|
if (typeof value === "object") return `${value.usd} USD`;
|
|
198
251
|
if (entry.threshold !== void 0) return `above ${entry.threshold.usd} USD`;
|
|
199
252
|
return value;
|
|
200
253
|
}
|
|
254
|
+
|
|
201
255
|
export {
|
|
202
|
-
runConfigGet,
|
|
203
256
|
runConfigList,
|
|
204
|
-
|
|
257
|
+
runConfigGet,
|
|
258
|
+
runConfigSet,
|
|
259
|
+
persistPublishMode
|
|
205
260
|
};
|
|
206
|
-
//# sourceMappingURL=
|
|
261
|
+
//# sourceMappingURL=chunk-TFGSOPWR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/config.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { styleText } from 'node:util';\nimport { CliError } from '../lib/errors';\nimport {\n CONFIG_KEYS,\n PUBLISH_CONFIG_KEYS,\n PublishModeSchema,\n RawConfigSchema,\n loadRawConfig,\n resolveSettings,\n} from '../lib/config';\nimport type {\n EffectiveSettings,\n PartialConfig,\n Provenance,\n PublishConfigKey,\n PublishMode,\n ScalarConfigKey,\n} from '../lib/config';\nimport { loadProjectConfig } from '../lib/settings';\nimport { configPath } from '../lib/paths';\nimport { writeFileAtomic } from '../lib/atomic-json';\nimport { withFileLock, LockTimeoutError } from '../lib/lock';\nimport { parseUsdToAtomic, toMoney } from '../lib/money';\nimport type { Money } from '../schemas';\nimport type { CommandContext, CommandResult } from '../context';\n\n/**\n * How one config key is presented in `data`. `value` is the machine form: dual\n * Money for the spend keys, the stored string for `confirm`/URLs, the string[]\n * for the allowlist. `threshold` rides along only for `confirm: above:<atomic>`\n * so an agent reads the dollar amount without re-parsing the string.\n */\ninterface RenderedValue {\n value: Money | string | string[] | boolean;\n threshold?: Money;\n}\ninterface RenderedSetting extends RenderedValue {\n source: Provenance;\n}\n\nconst CONFIRM_ABOVE = 'above:';\nconst KEY_WIDTH = Math.max(...[...CONFIG_KEYS, ...PUBLISH_CONFIG_KEYS].map((key) => key.length));\n\n/**\n * A one-line human description per key, appended (dim) to the bare `config`\n * listing only. Machine `data` is unchanged; these are humanLines decoration.\n */\nconst KEY_DESCRIPTIONS: Record<string, string> = {\n maxAutoSpend: 'auto-approve a read up to this amount',\n sessionBudget: 'cap on total auto-spend per session',\n confirm: 'when to ask before paying',\n allowlistCreators: 'only auto-pay these creators (empty = any)',\n baseUrl: 'Tenjin API base URL',\n rpcUrl: 'Base RPC endpoint for balance reads',\n evalCohort: 'opt in to the lookup evaluation cohort',\n 'publish.mode': 'review=always ask, auto=ask on findings, full-auto=only hard blocks stop it',\n 'publish.defaultPrice': 'price used when none is given',\n};\n\nfunction isPublishKey(key: string): key is PublishConfigKey {\n return (PUBLISH_CONFIG_KEYS as readonly string[]).includes(key);\n}\n\n/**\n * Show every effective key with its value and provenance. `data` is keyed by\n * config key; provenance comes from resolveSettings over the *raw* file (not the\n * defaults-merged config), so a key the user never set reads `default`, not\n * `file`.\n */\nexport async function runConfigList(ctx: CommandContext): Promise<CommandResult> {\n const settings = await resolveFromContext(ctx);\n const data: Record<string, RenderedSetting> = {};\n const humanLines: string[] = [];\n for (const key of CONFIG_KEYS) {\n const entry = renderSetting(key, settings[key].value, settings[key].source);\n data[key] = entry;\n humanLines.push(describedLine(key, entry));\n }\n for (const key of PUBLISH_CONFIG_KEYS) {\n const entry = renderPublishSetting(key, settings);\n data[key] = entry;\n humanLines.push(describedLine(key, entry, downgradeNote(key, settings)));\n }\n return { data, humanLines };\n}\n\n/** Same per-key shape as list, for one key. Unknown key is a USAGE failure. */\nexport async function runConfigGet(\n { key }: { key: string },\n ctx: CommandContext,\n): Promise<CommandResult> {\n if (isPublishKey(key)) {\n const settings = await resolveFromContext(ctx);\n const entry = renderPublishSetting(key, settings);\n return {\n data: { key, ...entry },\n humanLines: [withNote(formatLine(key, entry), downgradeNote(key, settings))],\n };\n }\n const configKey = assertKey(key);\n const settings = await resolveFromContext(ctx);\n const entry = renderSetting(configKey, settings[configKey].value, settings[configKey].source);\n return { data: { key: configKey, ...entry }, humanLines: [formatLine(configKey, entry)] };\n}\n\n/**\n * Parse the value for this key, then persist it merged into the existing raw\n * file — never materializing defaults for keys the user did not set, so\n * provenance stays truthful. The written key now reads `file`.\n */\nexport async function runConfigSet(\n { key, value }: { key: string; value: string },\n ctx: CommandContext,\n): Promise<CommandResult> {\n if (isPublishKey(key)) return setPublishKey(key, value, ctx);\n const configKey = assertKey(key);\n const stored = parseValue(configKey, value);\n await persist(ctx.dataDir, (existing) => ({ ...existing, [configKey]: stored }));\n const entry = renderSetting(configKey, stored, 'file');\n return { data: { key: configKey, ...entry }, humanLines: [formatLine(configKey, entry)] };\n}\n\n/**\n * `config set publish.mode|publish.defaultPrice`. mode validates to the enum;\n * defaultPrice parses decimal USD at the edge into atomic (matching the spend\n * keys). The subkey is merged into the nested publish block, so a sibling subkey\n * (or an unknown one a newer CLI wrote) is preserved.\n */\nasync function setPublishKey(\n key: PublishConfigKey,\n value: string,\n ctx: CommandContext,\n): Promise<CommandResult> {\n const entry: RenderedSetting =\n key === 'publish.mode'\n ? { value: parsePublishMode(value), source: 'file' }\n : { value: toMoney(parseUsdToAtomic(value)), source: 'file' };\n const subkey = key === 'publish.mode' ? 'mode' : 'defaultPrice';\n const stored = key === 'publish.mode' ? (entry.value as string) : (entry.value as Money).atomic;\n await persist(ctx.dataDir, (existing) => ({\n ...existing,\n publish: { ...existing.publish, [subkey]: stored },\n }));\n return { data: { key, ...entry }, humanLines: [formatLine(key, entry)] };\n}\n\nfunction parsePublishMode(value: string): string {\n const parsed = PublishModeSchema.safeParse(value);\n if (parsed.success) return parsed.data;\n throw new CliError('USAGE', `Invalid publish mode: ${JSON.stringify(value)}`, {\n fix: 'Use \"review\", \"auto\", or \"full-auto\".',\n });\n}\n\n/**\n * Persist just `publish.mode` into the global config through the same locked\n * merge-write every `config set` uses (never a raw overwrite), so a sibling\n * subkey or an unknown block a newer CLI wrote is preserved. Used by `install`'s\n * setup prompt; the mode is a validated PublishMode.\n */\nexport async function persistPublishMode(dir: string, mode: PublishMode): Promise<void> {\n await persist(dir, (existing) => ({\n ...existing,\n publish: { ...existing.publish, mode },\n }));\n}\n\nasync function resolveFromContext(ctx: CommandContext): Promise<EffectiveSettings> {\n const config = await loadRawConfig(ctx.dataDir);\n // Feed the per-project `.tenjin.json` layer so the publish keys read out what a\n // real `publish` would resolve (project/env included), not a global-only guess.\n const project = await loadProjectConfig(process.cwd());\n return resolveSettings({\n config,\n flags: { baseUrl: ctx.flags.baseUrl },\n env: process.env,\n project: project?.layer,\n });\n}\n\nfunction assertKey(key: string): ScalarConfigKey {\n if ((CONFIG_KEYS as string[]).includes(key)) return key as ScalarConfigKey;\n throw new CliError('USAGE', `Unknown config key: ${JSON.stringify(key)}`, {\n fix: `Valid keys: ${[...CONFIG_KEYS, ...PUBLISH_CONFIG_KEYS].join(', ')}.`,\n });\n}\n\nfunction renderSetting(\n key: ScalarConfigKey,\n stored: string | string[] | boolean,\n source: Provenance,\n): RenderedSetting {\n return { ...renderValue(key, stored), source };\n}\n\n/**\n * The list/get shape for a publish key, read from the resolved effective\n * settings, which now include the per-project `.tenjin.json` layer (see\n * resolveFromContext) so the source reflects what a publish would actually use.\n */\nfunction renderPublishSetting(key: PublishConfigKey, settings: EffectiveSettings): RenderedSetting {\n if (key === 'publish.mode') {\n return { value: settings.publishMode.value, source: settings.publishMode.source };\n }\n return {\n value: toMoney(settings.publishDefaultPrice.value),\n source: settings.publishDefaultPrice.source,\n };\n}\n\nfunction renderValue(key: ScalarConfigKey, stored: string | string[] | boolean): RenderedValue {\n if (Array.isArray(stored) || typeof stored === 'boolean') return { value: stored };\n if (key === 'maxAutoSpend' || key === 'sessionBudget') return { value: toMoney(stored) };\n if (key === 'confirm' && stored.startsWith(CONFIRM_ABOVE)) {\n return { value: stored, threshold: toMoney(stored.slice(CONFIRM_ABOVE.length)) };\n }\n return { value: stored };\n}\n\n/** Per-key edge parsing. Returns the persisted form; throws USAGE on bad input. */\nfunction parseValue(key: ScalarConfigKey, value: string): string | string[] | boolean {\n switch (key) {\n case 'maxAutoSpend':\n case 'sessionBudget':\n return parseUsdToAtomic(value); // throws USAGE on a bad amount\n case 'confirm':\n return parseConfirm(value);\n case 'allowlistCreators':\n return parseAllowlist(value);\n case 'baseUrl':\n case 'rpcUrl':\n return parseHttpUrl(value);\n case 'evalCohort':\n return parseBoolean(value);\n }\n}\n\nfunction parseBoolean(value: string): boolean {\n if (value === 'true') return true;\n if (value === 'false') return false;\n throw new CliError('USAGE', `Invalid boolean value: ${JSON.stringify(value)}`, {\n fix: 'Use \"true\" or \"false\".',\n });\n}\n\nfunction parseConfirm(value: string): string {\n if (value === 'always') return 'always';\n if (value.startsWith(CONFIRM_ABOVE)) {\n return `${CONFIRM_ABOVE}${parseUsdToAtomic(value.slice(CONFIRM_ABOVE.length))}`;\n }\n throw new CliError('USAGE', `Invalid confirm value: ${JSON.stringify(value)}`, {\n fix: 'Use \"always\" or \"above:<usd>\", e.g. above:0.25.',\n });\n}\n\nfunction parseAllowlist(value: string): string[] {\n // \"\" clears to []; comma-split, trim, drop empties. Reject an entry with\n // internal whitespace — a creator handle/address is a single token.\n const entries = value\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0);\n for (const entry of entries) {\n if (/\\s/.test(entry)) {\n throw new CliError('USAGE', `Invalid creator entry: ${JSON.stringify(entry)}`, {\n fix: 'Creator entries cannot contain spaces; separate multiple with commas.',\n });\n }\n }\n return entries;\n}\n\nfunction parseHttpUrl(value: string): string {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new CliError('USAGE', `Invalid URL: ${JSON.stringify(value)}`, {\n fix: 'Pass an absolute http(s) URL like https://tenjin.blog.',\n });\n }\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new CliError('USAGE', `URL must be http or https: ${JSON.stringify(value)}`, {\n fix: 'Pass an absolute http(s) URL like https://tenjin.blog.',\n });\n }\n return value;\n}\n\n/**\n * Apply `merge` to the raw file and write it back atomically. Uses the partial\n * schema and writeFileAtomic (not writeConfig, which would materialize every\n * default into the file and corrupt provenance). loadRawConfig surfaces a\n * corrupt existing file as CONFIG_INVALID, which propagates.\n *\n * The whole read-merge-write runs under a cross-process file lock: without it two\n * concurrent `config set` on different keys race on the read, and the last writer\n * drops the other's update (worst case, a zeroed maxAutoSpend resurrected). The\n * data dir is ensured first so the lock's mkdir has a parent on a fresh install.\n */\nasync function persist(\n dir: string,\n merge: (existing: PartialConfig) => PartialConfig,\n): Promise<void> {\n await mkdir(dir, { recursive: true, mode: 0o700 });\n const lockPath = `${configPath(dir)}.lock`;\n try {\n await withFileLock(lockPath, async () => {\n const existing = await loadRawConfig(dir);\n const merged = merge(existing);\n const validated = RawConfigSchema.parse(merged);\n await writeFileAtomic(configPath(dir), `${JSON.stringify(validated, null, 2)}\\n`, {\n mode: 0o644,\n dirMode: 0o700,\n });\n });\n } catch (err) {\n // A lock timeout is not the user's malformed input; surface it as INTERNAL with\n // the one manual step (there is no auto-steal), keeping the JSON error contract.\n if (err instanceof LockTimeoutError) {\n throw new CliError('INTERNAL', err.message, {\n fix: `If no other tenjin process is running, remove ${lockPath} and retry.`,\n cause: err,\n });\n }\n throw err;\n }\n}\n\nfunction formatLine(key: string, entry: RenderedSetting): string {\n const label = key.padEnd(KEY_WIDTH);\n return ` ${label} ${displayValue(entry)} ${styleText('dim', `(${entry.source})`)}`;\n}\n\n/** The list variant: the value line, a dim description, and an optional dim note. */\nfunction describedLine(key: string, entry: RenderedSetting, note?: string): string {\n const description = KEY_DESCRIPTIONS[key];\n let line = formatLine(key, entry);\n if (description !== undefined) line += ` ${styleText('dim', `- ${description}`)}`;\n return withNote(line, note);\n}\n\n/** Append a dim parenthetical note (e.g. the full-auto downgrade) when present. */\nfunction withNote(line: string, note?: string): string {\n return note !== undefined ? `${line} ${styleText('dim', `(${note})`)}` : line;\n}\n\n/**\n * The `publish.mode` line gains a downgrade note when the effective mode came from\n * a committed `.tenjin.json` asking for `full-auto` (the loosening gate demoted it\n * to `auto`). Human-only; the machine `data` shape is unchanged.\n */\nfunction downgradeNote(key: PublishConfigKey, settings: EffectiveSettings): string | undefined {\n if (key !== 'publish.mode' || settings.publishMode.downgradedWarning === undefined) {\n return undefined;\n }\n return 'downgraded from full-auto: committed .tenjin.json (gitignore it to opt in)';\n}\n\nfunction displayValue(entry: RenderedSetting): string {\n const { value } = entry;\n if (Array.isArray(value)) return value.length > 0 ? value.join(', ') : '(empty)';\n if (typeof value === 'boolean') return value ? 'true' : 'false'; // evalCohort\n if (typeof value === 'object') return `${value.usd} USD`; // Money (spend keys)\n if (entry.threshold !== undefined) return `above ${entry.threshold.usd} USD`; // confirm\n return value; // 'always', baseUrl, rpcUrl\n}\n"],"mappings":"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","names":["settings","entry"]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createRequire as __tenjinCreateRequire } from 'node:module'; const require = __tenjinCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
UUID_RE,
|
|
4
|
+
newId
|
|
5
|
+
} from "./chunk-3OV7E5G5.js";
|
|
6
|
+
import {
|
|
7
|
+
hasCode
|
|
8
|
+
} from "./chunk-2I4QTOLB.js";
|
|
9
|
+
import {
|
|
10
|
+
writeFileAtomicExclusive
|
|
11
|
+
} from "./chunk-MUBUQVC3.js";
|
|
12
|
+
import {
|
|
13
|
+
external_exports
|
|
14
|
+
} from "./chunk-OYMYCW7J.js";
|
|
15
|
+
|
|
16
|
+
// src/lib/candidate-store.ts
|
|
17
|
+
import { mkdir, readFile, readdir, rm, stat } from "fs/promises";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
var DRAFT_FILE = "draft.md";
|
|
20
|
+
var META_FILE = "meta.json";
|
|
21
|
+
var CandidateMetaSchema = external_exports.object({
|
|
22
|
+
schemaVersion: external_exports.literal(1),
|
|
23
|
+
lookupId: external_exports.string(),
|
|
24
|
+
question: external_exports.string().optional(),
|
|
25
|
+
// Fixed-width UTC ISO-8601 (a trailing Z, never an offset): listCandidates
|
|
26
|
+
// sorts newest-first lexicographically on this string, which is only a valid
|
|
27
|
+
// proxy for chronological order when every value is same-zone UTC. z.iso
|
|
28
|
+
// .datetime() rejects offsets and non-datetime strings, pinning that invariant.
|
|
29
|
+
created: external_exports.iso.datetime(),
|
|
30
|
+
/** The repo root the add ran in, or the cwd when not in a repo. */
|
|
31
|
+
sourceProject: external_exports.string()
|
|
32
|
+
});
|
|
33
|
+
function candidatesDir(dataDir) {
|
|
34
|
+
return join(dataDir, "candidates");
|
|
35
|
+
}
|
|
36
|
+
function candidateDir(dataDir, id) {
|
|
37
|
+
return join(candidatesDir(dataDir), id);
|
|
38
|
+
}
|
|
39
|
+
async function createCandidate(dataDir, input) {
|
|
40
|
+
const id = newId();
|
|
41
|
+
const dir = candidateDir(dataDir, id);
|
|
42
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
43
|
+
const draftPath = join(dir, DRAFT_FILE);
|
|
44
|
+
const meta = {
|
|
45
|
+
schemaVersion: 1,
|
|
46
|
+
lookupId: input.lookupId,
|
|
47
|
+
...input.question !== void 0 ? { question: input.question } : {},
|
|
48
|
+
created: input.created,
|
|
49
|
+
sourceProject: input.sourceProject
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
await writeFileAtomicExclusive(draftPath, input.draft, { mode: 384, dirMode: 448 });
|
|
53
|
+
await writeFileAtomicExclusive(join(dir, META_FILE), `${JSON.stringify(meta, null, 2)}
|
|
54
|
+
`, {
|
|
55
|
+
mode: 384,
|
|
56
|
+
dirMode: 448
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (!hasCode(err, "EEXIST")) {
|
|
60
|
+
await rm(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
61
|
+
}
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
return { id, meta, dir, draftPath };
|
|
65
|
+
}
|
|
66
|
+
async function readCandidate(dataDir, id) {
|
|
67
|
+
if (!UUID_RE.test(id)) return null;
|
|
68
|
+
const dir = candidateDir(dataDir, id);
|
|
69
|
+
const meta = await readMeta(dir);
|
|
70
|
+
if (meta === null) return null;
|
|
71
|
+
return { id, meta, dir, draftPath: join(dir, DRAFT_FILE) };
|
|
72
|
+
}
|
|
73
|
+
async function listCandidates(dataDir) {
|
|
74
|
+
let entries;
|
|
75
|
+
try {
|
|
76
|
+
entries = await readdir(candidatesDir(dataDir), { withFileTypes: true });
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const records = [];
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
if (!entry.isDirectory() || !UUID_RE.test(entry.name)) continue;
|
|
83
|
+
const record = await readCandidate(dataDir, entry.name);
|
|
84
|
+
if (record !== null) records.push(record);
|
|
85
|
+
}
|
|
86
|
+
records.sort((a, b) => {
|
|
87
|
+
if (a.meta.created !== b.meta.created) return a.meta.created < b.meta.created ? 1 : -1;
|
|
88
|
+
return a.id < b.id ? 1 : -1;
|
|
89
|
+
});
|
|
90
|
+
return records;
|
|
91
|
+
}
|
|
92
|
+
async function dropCandidate(dataDir, id) {
|
|
93
|
+
if (!UUID_RE.test(id)) return false;
|
|
94
|
+
const dir = candidateDir(dataDir, id);
|
|
95
|
+
try {
|
|
96
|
+
if (!(await stat(dir)).isDirectory()) return false;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
await rm(dir, { recursive: true, force: true });
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
async function readMeta(dir) {
|
|
104
|
+
let raw;
|
|
105
|
+
try {
|
|
106
|
+
raw = await readFile(join(dir, META_FILE), "utf8");
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
let json;
|
|
111
|
+
try {
|
|
112
|
+
json = JSON.parse(raw);
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const parsed = CandidateMetaSchema.safeParse(json);
|
|
117
|
+
return parsed.success ? parsed.data : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export {
|
|
121
|
+
createCandidate,
|
|
122
|
+
readCandidate,
|
|
123
|
+
listCandidates,
|
|
124
|
+
dropCandidate
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=chunk-TLCMMYLY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/candidate-store.ts"],"sourcesContent":["import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises';\nimport type { Dirent } from 'node:fs';\nimport { join } from 'node:path';\nimport { z } from 'zod';\nimport { writeFileAtomicExclusive } from './atomic-json';\nimport { hasCode } from './errno';\nimport { newId, UUID_RE } from './ids';\n\n/**\n * The D40 candidate store: publish drafts parked locally that never touch the\n * network and never upload — only a later `tenjin publish --candidate <id>`\n * sends one, under the D38 consent scan. Each candidate is its OWN directory\n * `~/.tenjin/candidates/<id>/` holding the draft markdown plus a meta.json.\n *\n * Custody mirrors the wallet key discipline: the candidates tree is 0700 and\n * every file 0600, written create-once through writeFileAtomicExclusive so a\n * reader sees a complete file or none and a uuid collision is loud, never a\n * silent overwrite. Unlike lookup-store's single shared ledger, there is no shared\n * mutable file here — a fresh uuid per add means two concurrent adds land in two\n * distinct dirs and cannot contend — so no cross-process lock is needed. The\n * meta.json is written LAST and atomically, making it the commit point: `list`\n * skips any dir whose meta is absent or unreadable, so a torn or half-written\n * add reads as not-yet-there rather than a broken candidate, the same\n * best-effort posture loadLookups takes on a corrupt store. A crash mid-add is\n * cleaned up eagerly (the dir is removed on any write failure), and `drop` can\n * still remove a metaless orphan by id so a leaked draft is never stranded.\n */\n\nconst DRAFT_FILE = 'draft.md';\nconst META_FILE = 'meta.json';\n\nconst CandidateMetaSchema = z.object({\n schemaVersion: z.literal(1),\n lookupId: z.string(),\n question: z.string().optional(),\n // Fixed-width UTC ISO-8601 (a trailing Z, never an offset): listCandidates\n // sorts newest-first lexicographically on this string, which is only a valid\n // proxy for chronological order when every value is same-zone UTC. z.iso\n // .datetime() rejects offsets and non-datetime strings, pinning that invariant.\n created: z.iso.datetime(),\n /** The repo root the add ran in, or the cwd when not in a repo. */\n sourceProject: z.string(),\n});\nexport type CandidateMeta = z.infer<typeof CandidateMetaSchema>;\n\nexport interface CandidateRecord {\n id: string;\n meta: CandidateMeta;\n /** Absolute path of the candidate directory. */\n dir: string;\n /** Absolute path of the draft markdown inside the directory. */\n draftPath: string;\n}\n\nexport interface CreateCandidateInput {\n /** The draft markdown, copied verbatim into the candidate. */\n draft: string;\n lookupId: string;\n question?: string;\n /** ISO-8601 creation time; the command supplies it via its clock seam. */\n created: string;\n sourceProject: string;\n}\n\nfunction candidatesDir(dataDir: string): string {\n return join(dataDir, 'candidates');\n}\n\nfunction candidateDir(dataDir: string, id: string): string {\n return join(candidatesDir(dataDir), id);\n}\n\n/** Park a draft as a new candidate and return the created record. Both files are\n * written create-once (no-clobber, fsynced) at a fresh uuid path, meta.json\n * last, so the candidate is either fully present to `list`/`read` or absent,\n * never half-there. */\nexport async function createCandidate(\n dataDir: string,\n input: CreateCandidateInput,\n): Promise<CandidateRecord> {\n const id = newId();\n const dir = candidateDir(dataDir, id);\n await mkdir(dir, { recursive: true, mode: 0o700 });\n\n const draftPath = join(dir, DRAFT_FILE);\n const meta: CandidateMeta = {\n schemaVersion: 1,\n lookupId: input.lookupId,\n ...(input.question !== undefined ? { question: input.question } : {}),\n created: input.created,\n sourceProject: input.sourceProject,\n };\n try {\n // Exclusive (create-or-EEXIST, fsynced) so a uuid collision is loud, not a\n // silent overwrite of an existing candidate, and the crash-durable commit of\n // meta.json is the last, atomic step.\n await writeFileAtomicExclusive(draftPath, input.draft, { mode: 0o600, dirMode: 0o700 });\n await writeFileAtomicExclusive(join(dir, META_FILE), `${JSON.stringify(meta, null, 2)}\\n`, {\n mode: 0o600,\n dirMode: 0o700,\n });\n } catch (err) {\n // An EEXIST means something already lived at this fresh-uuid path (a real\n // collision, astronomically unlikely): surface it loudly and NEVER delete\n // what was already there. Any other failure is a partial write of our own\n // fresh dir — remove it so a crash mid-add can't strand a hidden dir holding\n // the (secrets-adjacent) draft that `list` skips and no one can name to `drop`.\n if (!hasCode(err, 'EEXIST')) {\n await rm(dir, { recursive: true, force: true }).catch(() => undefined);\n }\n throw err;\n }\n\n return { id, meta, dir, draftPath };\n}\n\n/** One candidate by id, or null when it is unknown (or the id is malformed, so a\n * stray argument can never traverse out of the store). */\nexport async function readCandidate(dataDir: string, id: string): Promise<CandidateRecord | null> {\n if (!UUID_RE.test(id)) return null;\n const dir = candidateDir(dataDir, id);\n const meta = await readMeta(dir);\n if (meta === null) return null;\n return { id, meta, dir, draftPath: join(dir, DRAFT_FILE) };\n}\n\n/** Every pending candidate, newest first by `created` (no auto-expiry — stale\n * candidates stay visible, D40). Tie-broken on id so the order is total. */\nexport async function listCandidates(dataDir: string): Promise<CandidateRecord[]> {\n let entries: Dirent[];\n try {\n entries = await readdir(candidatesDir(dataDir), { withFileTypes: true });\n } catch {\n return [];\n }\n const records: CandidateRecord[] = [];\n for (const entry of entries) {\n if (!entry.isDirectory() || !UUID_RE.test(entry.name)) continue;\n const record = await readCandidate(dataDir, entry.name);\n if (record !== null) records.push(record);\n }\n records.sort((a, b) => {\n if (a.meta.created !== b.meta.created) return a.meta.created < b.meta.created ? 1 : -1;\n return a.id < b.id ? 1 : -1;\n });\n return records;\n}\n\n/** Discard one candidate by id. Returns false when the id is malformed or no\n * such dir exists, so the command can surface a clean not-found; nothing is\n * ever auto-deleted. Removes the dir even when its meta is absent or corrupt,\n * so a crash-orphaned candidate can still be cleaned up by id (the id is\n * UUID-gated first, so a stray argument can never target a path outside the\n * store). */\nexport async function dropCandidate(dataDir: string, id: string): Promise<boolean> {\n if (!UUID_RE.test(id)) return false;\n const dir = candidateDir(dataDir, id);\n try {\n if (!(await stat(dir)).isDirectory()) return false;\n } catch {\n return false; // no such candidate\n }\n await rm(dir, { recursive: true, force: true });\n return true;\n}\n\n/** Read and validate a candidate's meta.json; null on any absence/corruption. */\nasync function readMeta(dir: string): Promise<CandidateMeta | null> {\n let raw: string;\n try {\n raw = await readFile(join(dir, META_FILE), 'utf8');\n } catch {\n return null;\n }\n let json: unknown;\n try {\n json = JSON.parse(raw);\n } catch {\n return null;\n }\n const parsed = CandidateMetaSchema.safeParse(json);\n return parsed.success ? parsed.data : null;\n}\n"],"mappings":"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","names":[]}
|