@mazjq/zhiro 0.0.0-stage → 0.1.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.
@@ -0,0 +1,558 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // apps/daemon/src/lock.ts
5
+ import net from "node:net";
6
+ import { createHash } from "node:crypto";
7
+ import { realpathSync, rmSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ var VaultBusy = class extends Error {
10
+ constructor(holder, vault) {
11
+ super(`数据目录已被${holder || "另一个 Zhiro 进程"}占用:${vault}`);
12
+ this.holder = holder;
13
+ }
14
+ holder;
15
+ static {
16
+ __name(this, "VaultBusy");
17
+ }
18
+ code = "VAULT_BUSY";
19
+ };
20
+ function endpoint(vault) {
21
+ let dir = vault;
22
+ try {
23
+ dir = realpathSync(vault);
24
+ } catch {
25
+ }
26
+ const key = createHash("sha256").update(process.platform === "win32" ? dir.toLowerCase() : dir).digest("hex").slice(0, 24);
27
+ return process.platform === "win32" ? `\\\\.\\pipe\\jobos-${key}` : join("/tmp", `jobos-${key}.sock`);
28
+ }
29
+ __name(endpoint, "endpoint");
30
+ function holderOf(path) {
31
+ return new Promise((done) => {
32
+ const c = net.connect(path);
33
+ let said = "";
34
+ c.setEncoding("utf8");
35
+ c.on("data", (d) => said += d);
36
+ c.on("end", () => done(said.trim()));
37
+ c.on("error", () => done(null));
38
+ setTimeout(() => {
39
+ c.destroy();
40
+ done(said.trim() || null);
41
+ }, 1500).unref();
42
+ });
43
+ }
44
+ __name(holderOf, "holderOf");
45
+ async function lockVault(vault, who) {
46
+ const path = endpoint(vault);
47
+ const label = `${who}(进程 ${process.pid})`;
48
+ for (let attempt = 0; attempt < 2; attempt++) {
49
+ const server = net.createServer((c) => {
50
+ c.on("error", () => {
51
+ });
52
+ c.end(label);
53
+ });
54
+ const got = await new Promise((done) => {
55
+ server.once("error", done);
56
+ server.listen(path, () => done(null));
57
+ });
58
+ if (!got) {
59
+ server.unref();
60
+ return { release: /* @__PURE__ */ __name(() => new Promise((done) => server.close(() => done())), "release") };
61
+ }
62
+ if (got.code !== "EADDRINUSE") throw got;
63
+ const holder = await holderOf(path);
64
+ if (holder !== null) throw new VaultBusy(holder, vault);
65
+ if (process.platform === "win32") throw new VaultBusy("", vault);
66
+ rmSync(path, { force: true });
67
+ }
68
+ throw new VaultBusy("", vault);
69
+ }
70
+ __name(lockVault, "lockVault");
71
+
72
+ // apps/daemon/src/move.ts
73
+ import { createHash as createHash2, randomBytes } from "node:crypto";
74
+ import { execFile, execFileSync } from "node:child_process";
75
+ import { DatabaseSync } from "node:sqlite";
76
+ import { closeSync, copyFileSync, existsSync, lstatSync, utimesSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync as realpathSync2, renameSync, rmSync as rmSync2, rmdirSync, statSync, statfsSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
77
+ import { readdir, stat } from "node:fs/promises";
78
+ import { basename, dirname, isAbsolute, join as join2, relative, resolve } from "node:path";
79
+ var VaultUnavailable = class extends Error {
80
+ static {
81
+ __name(this, "VaultUnavailable");
82
+ }
83
+ code = "VAULT_UNAVAILABLE";
84
+ };
85
+ var logPath = /* @__PURE__ */ __name((entry) => `${resolve(entry)}.move.json`, "logPath");
86
+ var mark = /* @__PURE__ */ __name((log) => join2(log.to, `.jobos-move-${log.id}`), "mark");
87
+ var probeName = /* @__PURE__ */ __name((log) => `${resolve(log.entry)}.moving-${log.id}`, "probeName");
88
+ var norm = /* @__PURE__ */ __name((p) => process.platform === "win32" ? resolve(p).toLowerCase() : resolve(p), "norm");
89
+ var same = /* @__PURE__ */ __name((a, b) => norm(a) === norm(b), "same");
90
+ var within = /* @__PURE__ */ __name((dir, p) => {
91
+ const r = relative(norm(dir), norm(p));
92
+ return r === "" || !r.startsWith("..") && !isAbsolute(r);
93
+ }, "within");
94
+ var isLink = /* @__PURE__ */ __name((p) => {
95
+ try {
96
+ return lstatSync(p).isSymbolicLink();
97
+ } catch {
98
+ return false;
99
+ }
100
+ }, "isLink");
101
+ var dropLink = /* @__PURE__ */ __name((p) => process.platform === "win32" ? rmSync2(p) : unlinkSync(p), "dropLink");
102
+ var real = /* @__PURE__ */ __name((p) => {
103
+ try {
104
+ return realpathSync2(p);
105
+ } catch {
106
+ return resolve(p);
107
+ }
108
+ }, "real");
109
+ var gb = /* @__PURE__ */ __name((n) => `${(n / 1e9).toFixed(1)} GB`, "gb");
110
+ var codeOf = /* @__PURE__ */ __name((e) => String(e?.code ?? e?.message ?? e).split("\n")[0].slice(0, 120), "codeOf");
111
+ function readLog(entry) {
112
+ let text;
113
+ try {
114
+ text = readFileSync(logPath(entry), "utf8");
115
+ } catch {
116
+ return null;
117
+ }
118
+ try {
119
+ return JSON.parse(text);
120
+ } catch {
121
+ throw new VaultUnavailable(`搬数据的记录读不出来:${logPath(entry)}。它记着数据现在在哪,Zhiro 不会在读不出来的时候猜`);
122
+ }
123
+ }
124
+ __name(readLog, "readLog");
125
+ function writeLog(entry, log) {
126
+ const p = logPath(entry), next = { ...log, at: (/* @__PURE__ */ new Date()).toISOString() };
127
+ writeFileSync(p + ".tmp", JSON.stringify(next, null, 2), "utf8");
128
+ renameSync(p + ".tmp", p);
129
+ return next;
130
+ }
131
+ __name(writeLog, "writeLog");
132
+ function files(root) {
133
+ const out = /* @__PURE__ */ new Map();
134
+ const walk = /* @__PURE__ */ __name((dir, rel) => {
135
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
136
+ const r = rel ? `${rel}/${e.name}` : e.name;
137
+ if (e.isDirectory()) walk(join2(dir, e.name), r);
138
+ else out.set(r, e.isFile() ? statSync(join2(dir, e.name)).size : -1);
139
+ }
140
+ }, "walk");
141
+ walk(root, "");
142
+ return out;
143
+ }
144
+ __name(files, "files");
145
+ function copyTree(from, to) {
146
+ mkdirSync(to, { recursive: true });
147
+ for (const e of readdirSync(from, { withFileTypes: true })) {
148
+ const a = join2(from, e.name), b = join2(to, e.name);
149
+ if (e.isDirectory()) copyTree(a, b);
150
+ else if (e.isFile()) {
151
+ copyFileSync(a, b);
152
+ const st = statSync(a);
153
+ utimesSync(b, st.atime, st.mtime);
154
+ } else throw new Error(`数据文件夹里有一个不是普通文件的东西,没法照原样复制:${a}`);
155
+ }
156
+ }
157
+ __name(copyTree, "copyTree");
158
+ function dirSize(root) {
159
+ let n = 0;
160
+ try {
161
+ for (const s of files(root).values()) if (s > 0) n += s;
162
+ } catch {
163
+ }
164
+ return n;
165
+ }
166
+ __name(dirSize, "dirSize");
167
+ function sha(path) {
168
+ const h = createHash2("sha256"), buf = Buffer.allocUnsafe(1 << 22), fd = openSync(path, "r");
169
+ try {
170
+ for (let n; (n = readSync(fd, buf, 0, buf.length, null)) > 0; ) h.update(buf.subarray(0, n));
171
+ } finally {
172
+ closeSync(fd);
173
+ }
174
+ return h.digest("hex");
175
+ }
176
+ __name(sha, "sha");
177
+ var DRIVES_PS = "Get-Partition | Where-Object DriveLetter | ForEach-Object { $v = Get-Volume -Partition $_; [pscustomobject]@{ letter = [string]$_.DriveLetter; bus = [string](Get-Disk -Number $_.DiskNumber).BusType; fs = [string]$v.FileSystemType; free = [double]$v.SizeRemaining; label = [string]$v.FileSystemLabel } } | ConvertTo-Json -Compress";
178
+ var DRIVES_ARGS = ["-NoProfile", "-NonInteractive", "-Command", DRIVES_PS];
179
+ var parseDrives = /* @__PURE__ */ __name((out) => [JSON.parse(out)].flat().filter((d) => d.fs === "NTFS" && d.bus !== "USB").map((d) => ({ root: `${d.letter}:\\`, label: d.label ?? "", freeBytes: Number(d.free) || 0 })).sort((x, y) => x.root.localeCompare(y.root)), "parseDrives");
180
+ function drives() {
181
+ if (process.platform !== "win32") return [];
182
+ try {
183
+ return parseDrives(execFileSync("powershell", DRIVES_ARGS, { encoding: "utf8", timeout: 2e4, windowsHide: true }));
184
+ } catch {
185
+ return [];
186
+ }
187
+ }
188
+ __name(drives, "drives");
189
+ var drivesLater = /* @__PURE__ */ __name(() => new Promise((done) => {
190
+ if (process.platform !== "win32") return done([]);
191
+ execFile("powershell", DRIVES_ARGS, { encoding: "utf8", timeout: 2e4, windowsHide: true }, (err, out) => {
192
+ try {
193
+ done(err ? [] : parseDrives(out));
194
+ } catch {
195
+ done([]);
196
+ }
197
+ });
198
+ }), "drivesLater");
199
+ async function sizeLater(root) {
200
+ let n = 0;
201
+ try {
202
+ const all = (await readdir(root, { recursive: true, withFileTypes: true })).filter((e) => e.isFile());
203
+ for (let i = 0; i < all.length; i += 256) for (const s of await Promise.all(all.slice(i, i + 256).map((e) => stat(join2(e.parentPath, e.name)).then((x) => x.size, () => 0)))) n += s;
204
+ } catch {
205
+ }
206
+ return n;
207
+ }
208
+ __name(sizeLater, "sizeLater");
209
+ function checkTarget(entry, to, need, ok = drives()) {
210
+ if (!isAbsolute(to)) return "要写完整路径,例如 E:\\Zhiro数据";
211
+ const parent = dirname(resolve(to));
212
+ if (same(parent, to)) return "不能直接用整个盘,写一个文件夹,例如 E:\\Zhiro数据";
213
+ if (!existsSync(parent)) return `上一级文件夹不存在:${parent}`;
214
+ if (isLink(to)) return "目标要是一个普通的空文件夹,或者还不存在";
215
+ if (existsSync(to)) {
216
+ if (!lstatSync(to).isDirectory()) return "目标要是一个普通的空文件夹,或者还不存在";
217
+ if (readdirSync(to).length) return "目标文件夹里已经有东西了,换一个空的";
218
+ }
219
+ const target = join2(real(parent), basename(resolve(to)));
220
+ const log = readLog(entry);
221
+ for (const p of [real(entry), resolve(entry), ...log?.old ? [log.old] : []]) if (within(p, target) || within(target, p)) return "目标不能在现在的数据文件夹里面,也不能包住它";
222
+ if (log?.old && existsSync(log.old)) return `上次搬家留下的旧的一份还在:${log.old}。确认不要了,先把它删掉再搬`;
223
+ if (process.platform === "win32" && !ok.some((d) => norm(target).startsWith(norm(d.root)))) return "只能搬到机内硬盘(NTFS 格式,不是 USB 移动硬盘)";
224
+ let free = 0;
225
+ try {
226
+ const s = statfsSync(real(parent));
227
+ free = Number(s.bavail) * Number(s.bsize);
228
+ } catch (e) {
229
+ return `读不到那个盘的剩余空间:${codeOf(e)}`;
230
+ }
231
+ if (free < need * 1.1) return `那个盘剩余空间不够:要 ${gb(need * 1.1)},只剩 ${gb(free)}`;
232
+ try {
233
+ const probe = join2(real(parent), `.jobos-probe-${randomBytes(4).toString("hex")}`);
234
+ mkdirSync(probe);
235
+ rmdirSync(probe);
236
+ } catch (e) {
237
+ return `那个位置写不进去:${codeOf(e)}`;
238
+ }
239
+ return null;
240
+ }
241
+ __name(checkTarget, "checkTarget");
242
+ function requestMove(entry, to, ok) {
243
+ const from = real(entry);
244
+ const why = checkTarget(entry, to, dirSize(from), ok);
245
+ if (why) throw new Error(`BAD_TARGET: ${why}`);
246
+ return writeLog(entry, { id: randomBytes(6).toString("hex"), entry: resolve(entry), from, to: resolve(to), phase: "requested", at: "" });
247
+ }
248
+ __name(requestMove, "requestMove");
249
+ function cancelMove(entry) {
250
+ const log = readLog(entry);
251
+ if (log?.phase === "requested") writeLog(entry, { ...log, phase: "failed", error: "" });
252
+ }
253
+ __name(cancelMove, "cancelMove");
254
+ function dropTarget(log) {
255
+ if (existsSync(mark(log))) rmSync2(log.to, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
256
+ }
257
+ __name(dropTarget, "dropTarget");
258
+ function ensureEntry(entry) {
259
+ const log = readLog(entry);
260
+ if (!log) return;
261
+ if (log.phase === "switched") throw new VaultUnavailable("上次搬数据停在了切换那一步:启动一次 Zhiro,它会接着处理");
262
+ const home = log.phase === "done" ? log.to : log.from;
263
+ if (existsSync(entry) && same(real(entry), real(home))) return;
264
+ const reachable = existsSync(join2(home, "jobos.db"));
265
+ const gone = `数据应该在 ${home},现在够不着(盘没连上,或者文件夹被移走了)。Zhiro 不会在它的位置新建一份空数据。确实要从头开始的话,删掉 ${logPath(entry)}`;
266
+ if (isLink(entry)) {
267
+ if (existsSync(join2(entry, "jobos.db"))) return;
268
+ throw new VaultUnavailable(gone);
269
+ }
270
+ if (!reachable) throw new VaultUnavailable(gone);
271
+ if (existsSync(entry)) {
272
+ try {
273
+ rmdirSync(entry);
274
+ } catch {
275
+ throw new VaultUnavailable(`数据在 ${home},但 ${entry} 现在是一个有内容的普通文件夹。Zhiro 不会猜哪一份是对的:把其中一个挪走再启动`);
276
+ }
277
+ }
278
+ symlinkSync(home, entry, "junction");
279
+ }
280
+ __name(ensureEntry, "ensureEntry");
281
+ async function resolveVault(entry, say = (m) => console.log(`[zhiro] ${m}`), ok) {
282
+ const log = readLog(entry);
283
+ if (!log) return;
284
+ if (!existsSync(entry) && !isLink(entry) && existsSync(probeName(log))) renameSync(probeName(log), entry);
285
+ if (log.phase === "requested") return runMove(entry, log, say, ok);
286
+ if (log.phase === "copying" || log.phase === "verified") {
287
+ dropTarget(log);
288
+ writeLog(entry, { ...log, phase: "failed", error: "上次搬到一半被打断了。数据还在原位置,没有丢" });
289
+ return;
290
+ }
291
+ if (log.phase === "switched") settle(entry, log, say);
292
+ }
293
+ __name(resolveVault, "resolveVault");
294
+ function settle(entry, log, say) {
295
+ const done = /* @__PURE__ */ __name(() => {
296
+ rmSync2(mark(log), { force: true });
297
+ writeLog(entry, { ...log, phase: "done" });
298
+ say(`数据已在 ${log.to}`);
299
+ }, "done");
300
+ const failed = /* @__PURE__ */ __name(() => {
301
+ writeLog(entry, { ...log, phase: "failed", old: void 0, error: "上次搬到最后一步被打断了。数据还在原位置,没有丢" });
302
+ }, "failed");
303
+ if (existsSync(entry) && same(real(entry), real(log.to))) return done();
304
+ if (existsSync(entry) || isLink(entry)) {
305
+ dropTarget(log);
306
+ return failed();
307
+ }
308
+ if (existsSync(join2(log.to, "jobos.db")) && existsSync(mark(log))) {
309
+ symlinkSync(log.to, entry, "junction");
310
+ return done();
311
+ }
312
+ const firstMove = !!log.old && same(dirname(log.old), dirname(resolve(entry)));
313
+ if (firstMove && existsSync(log.old)) {
314
+ renameSync(log.old, entry);
315
+ return failed();
316
+ }
317
+ if (!firstMove && existsSync(join2(log.from, "jobos.db"))) {
318
+ symlinkSync(log.from, entry, "junction");
319
+ return failed();
320
+ }
321
+ throw new VaultUnavailable(`上次搬数据停在了切换那一步,两边都够不着:${log.from}、${log.to}`);
322
+ }
323
+ __name(settle, "settle");
324
+ async function runMove(entry, start, say, ok) {
325
+ let log = start;
326
+ const fail = /* @__PURE__ */ __name((why) => {
327
+ writeLog(entry, { ...log, phase: "failed", old: void 0, error: why });
328
+ say(`数据没有搬:${why}`);
329
+ }, "fail");
330
+ let lock;
331
+ try {
332
+ lock = await lockVault(log.from, "搬数据");
333
+ } catch (e) {
334
+ return fail(e instanceof VaultBusy ? e.message : `拿不到数据目录的锁:${codeOf(e)}`);
335
+ }
336
+ try {
337
+ const need = dirSize(log.from);
338
+ const why = checkTarget(entry, log.to, need, ok);
339
+ if (why) return fail(why);
340
+ const dbFile = join2(log.from, "jobos.db");
341
+ if (existsSync(dbFile)) {
342
+ const db = new DatabaseSync(dbFile);
343
+ try {
344
+ if (db.prepare("pragma wal_checkpoint(truncate)").get()?.busy) return fail("数据库正被别的程序用着");
345
+ const r = db.prepare("pragma integrity_check").all();
346
+ if (r.length !== 1 || r[0].integrity_check !== "ok") return fail(`数据库自检没通过(和搬家无关,原位置的库本身有问题):${r.map((x) => x.integrity_check).join("; ").slice(0, 200)}`);
347
+ } finally {
348
+ db.close();
349
+ }
350
+ }
351
+ if (!isLink(entry)) {
352
+ try {
353
+ renameSync(entry, probeName(log));
354
+ } catch (e) {
355
+ return fail(`数据文件夹正被别的程序开着(${codeOf(e)})。关掉在里面打开的 Claude Code / Codex 窗口和 Zhiro 打开的网申浏览器,再试`);
356
+ }
357
+ try {
358
+ renameSync(probeName(log), entry);
359
+ } catch (e) {
360
+ throw new VaultUnavailable(`没能把数据文件夹改回原名(${codeOf(e)}):手工把 ${probeName(log)} 改名为 ${entry} 再启动`);
361
+ }
362
+ }
363
+ mkdirSync(log.to, { recursive: true });
364
+ writeFileSync(mark(log), "", "utf8");
365
+ log = writeLog(entry, { ...log, phase: "copying" });
366
+ say(`正在把数据(${gb(need)})复制到 ${log.to} …`);
367
+ try {
368
+ copyTree(log.from, log.to);
369
+ } catch (e) {
370
+ dropTarget(log);
371
+ return fail(`复制时出错:${codeOf(e)}`);
372
+ }
373
+ say("复制完了,正在逐个文件比对 …");
374
+ const a = files(log.from), b = files(log.to);
375
+ b.delete(basename(mark(log)));
376
+ let bad = a.size !== b.size ? `文件个数不一样(${a.size} / ${b.size})` : "";
377
+ for (const [rel, size] of a) {
378
+ if (bad) break;
379
+ if (b.get(rel) !== size) bad = `大小不一样:${rel}`;
380
+ else if (size >= 0 && sha(join2(log.from, rel)) !== sha(join2(log.to, rel))) bad = `内容不一样:${rel}`;
381
+ }
382
+ if (bad) {
383
+ dropTarget(log);
384
+ return fail(`复制出来的和原来的不一致(${bad})。可能有程序在复制期间改了文件`);
385
+ }
386
+ log = writeLog(entry, { ...log, phase: "verified" });
387
+ const linked = isLink(entry);
388
+ const old = linked ? log.from : `${resolve(entry)}.old-${log.id}`;
389
+ log = writeLog(entry, { ...log, phase: "switched", old });
390
+ try {
391
+ if (linked) dropLink(entry);
392
+ else renameSync(entry, old);
393
+ symlinkSync(log.to, entry, "junction");
394
+ } catch (e) {
395
+ try {
396
+ if (!existsSync(entry) && !isLink(entry)) {
397
+ if (linked) symlinkSync(log.from, entry, "junction");
398
+ else renameSync(old, entry);
399
+ }
400
+ } catch {
401
+ throw new VaultUnavailable(`切换到一半出错(${codeOf(e)}),也没能恢复原状。数据完好,在 ${linked ? log.from : old}:${linked ? `在 ${entry} 重建指向它的目录联接` : `手工把它改名为 ${entry}`} 再启动`);
402
+ }
403
+ dropTarget(log);
404
+ return fail(`切换时出错(${codeOf(e)})。多半是数据文件夹刚被别的程序打开了:关掉再试`);
405
+ }
406
+ rmSync2(mark(log), { force: true });
407
+ writeLog(entry, { ...log, phase: "done" });
408
+ say(`数据已搬到 ${log.to}(${a.size} 个文件逐个核对一致)。旧的一份留在 ${old},确认没问题后由你自己删`);
409
+ } finally {
410
+ await lock.release();
411
+ }
412
+ }
413
+ __name(runMove, "runMove");
414
+ async function locationInfo(entry, ok) {
415
+ const log = readLog(entry), path = real(entry);
416
+ let freeBytes = 0;
417
+ try {
418
+ const s = statfsSync(path);
419
+ freeBytes = Number(s.bavail) * Number(s.bsize);
420
+ } catch {
421
+ }
422
+ const oldPath = log?.phase === "done" && log.old && existsSync(log.old) ? log.old : null;
423
+ const [bytes, backupsBytes, oldBytes, list] = await Promise.all([sizeLater(path), sizeLater(join2(path, "backups")), oldPath ? sizeLater(oldPath) : 0, ok ?? drivesLater()]);
424
+ return {
425
+ entry: resolve(entry),
426
+ path,
427
+ bytes,
428
+ backupsBytes,
429
+ freeBytes,
430
+ drives: list,
431
+ pending: log?.phase === "requested" ? log.to : null,
432
+ lastError: log?.phase === "failed" ? log.error ?? "" : "",
433
+ old: oldPath ? { path: oldPath, bytes: oldBytes } : null
434
+ };
435
+ }
436
+ __name(locationInfo, "locationInfo");
437
+
438
+ // apps/daemon/src/datadir.ts
439
+ import { closeSync as closeSync2, existsSync as existsSync2, fsyncSync, lstatSync as lstatSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeSync } from "node:fs";
440
+ import { homedir } from "node:os";
441
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
442
+ var here = /* @__PURE__ */ __name(() => ({ argv: process.argv, env: process.env, platform: process.platform, home: homedir() }), "here");
443
+ function configHome(w = here()) {
444
+ if (w.platform === "win32") return join3(w.env.APPDATA ?? join3(w.home, "AppData", "Roaming"), "Zhiro");
445
+ if (w.platform === "darwin") return join3(w.home, "Library", "Preferences", "Zhiro");
446
+ return join3(w.env.XDG_CONFIG_HOME || join3(w.home, ".config"), "zhiro");
447
+ }
448
+ __name(configHome, "configHome");
449
+ var pointerFile = /* @__PURE__ */ __name((w = here()) => join3(configHome(w), "data-dir"), "pointerFile");
450
+ function legacyDir(w = here()) {
451
+ if (w.platform === "win32") return join3(w.env.APPDATA ?? join3(w.home, "AppData", "Roaming"), "JobOS", "vault");
452
+ if (w.platform === "darwin") return join3(w.home, "Library", "Application Support", "JobOS", "vault");
453
+ return join3(w.home, ".local", "share", "jobos", "vault");
454
+ }
455
+ __name(legacyDir, "legacyDir");
456
+ function defaultDir(w = here()) {
457
+ if (w.platform === "win32") return join3(w.env.APPDATA ?? join3(w.home, "AppData", "Roaming"), "Zhiro", "data");
458
+ if (w.platform === "darwin") return join3(w.home, "Library", "Application Support", "Zhiro");
459
+ return join3(w.env.XDG_DATA_HOME || join3(w.home, ".local", "share"), "zhiro");
460
+ }
461
+ __name(defaultDir, "defaultDir");
462
+ function argOf(argv) {
463
+ for (let i = 0; i < argv.length; i++) {
464
+ if (argv[i] === "--data") return argv[i + 1];
465
+ if (argv[i].startsWith("--data=")) return argv[i].slice(7);
466
+ }
467
+ return void 0;
468
+ }
469
+ __name(argOf, "argOf");
470
+ var present = /* @__PURE__ */ __name((p) => {
471
+ try {
472
+ lstatSync2(p);
473
+ return true;
474
+ } catch {
475
+ return false;
476
+ }
477
+ }, "present");
478
+ function findDataDir(w = here()) {
479
+ const arg = argOf(w.argv);
480
+ if (arg) return { dir: resolve2(arg), source: "arg" };
481
+ const env = w.env.ZHIRO_DATA || w.env.JOBOS_VAULT;
482
+ if (env) {
483
+ if (w.env.ZHIRO_DATA && w.env.JOBOS_VAULT && resolve2(w.env.ZHIRO_DATA) !== resolve2(w.env.JOBOS_VAULT)) console.warn(`[zhiro] ZHIRO_DATA 和 JOBOS_VAULT 都设了,用 ZHIRO_DATA(${w.env.ZHIRO_DATA})`);
484
+ return { dir: resolve2(env), source: "env" };
485
+ }
486
+ const ptr = pointerFile(w);
487
+ if (existsSync2(ptr)) {
488
+ const dir = readFileSync2(ptr, "utf8").trim();
489
+ if (!dir) throw new VaultUnavailable(`记着数据位置的文件是空的:${ptr}。删掉它再启动会重新问数据放哪`);
490
+ if (!present(dir)) throw new VaultUnavailable(`数据应该在 ${dir}(记在 ${ptr}),现在找不到(盘没连上,或者文件夹被移走了)。不会在别处新建一份空数据;确实要从头开始的话,删掉 ${ptr}`);
491
+ return { dir, source: "pointer" };
492
+ }
493
+ const old = legacyDir(w);
494
+ if (present(old) || existsSync2(`${old}.move.json`)) return { dir: old, source: "legacy" };
495
+ return { dir: defaultDir(w), source: "default" };
496
+ }
497
+ __name(findDataDir, "findDataDir");
498
+ var found = null;
499
+ function dataDir() {
500
+ return found ??= findDataDir();
501
+ }
502
+ __name(dataDir, "dataDir");
503
+ function setDataDir(d) {
504
+ found = d;
505
+ }
506
+ __name(setDataDir, "setDataDir");
507
+ function writePointer(dir, w = here()) {
508
+ const p = pointerFile(w);
509
+ mkdirSync2(dirname2(p), { recursive: true });
510
+ const tmp = `${p}.tmp`;
511
+ const fd = openSync2(tmp, "w");
512
+ try {
513
+ writeSync(fd, resolve2(dir) + "\n", null, "utf8");
514
+ fsyncSync(fd);
515
+ } finally {
516
+ closeSync2(fd);
517
+ }
518
+ renameSync2(tmp, p);
519
+ }
520
+ __name(writePointer, "writePointer");
521
+ async function firstRun(d, say = console.log) {
522
+ if (d.source !== "default" || existsSync2(d.dir)) return d;
523
+ if (!process.stdin.isTTY) {
524
+ say(`[zhiro] 数据放在 ${d.dir}(各平台的惯例位置)。要换:下次加 --data <文件夹> 启动,或在设置页「换个位置」`);
525
+ return d;
526
+ }
527
+ const { createInterface } = await import("node:readline/promises");
528
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
529
+ try {
530
+ const answer = (await rl.question(`第一次启动:数据存在哪?(直接回车用 ${d.dir})
531
+ > `)).trim().replace(/^["']|["']$/g, "");
532
+ const dir = answer ? resolve2(answer) : d.dir;
533
+ writePointer(dir);
534
+ say(`[zhiro] 数据放在 ${dir};以后在设置页「换个位置」可以搬`);
535
+ const chosen = { dir, source: "pointer" };
536
+ setDataDir(chosen);
537
+ return chosen;
538
+ } finally {
539
+ rl.close();
540
+ }
541
+ }
542
+ __name(firstRun, "firstRun");
543
+
544
+ export {
545
+ __name,
546
+ VaultBusy,
547
+ lockVault,
548
+ VaultUnavailable,
549
+ requestMove,
550
+ cancelMove,
551
+ ensureEntry,
552
+ resolveVault,
553
+ locationInfo,
554
+ pointerFile,
555
+ dataDir,
556
+ writePointer,
557
+ firstRun
558
+ };
@@ -0,0 +1,107 @@
1
+ import {
2
+ __name,
3
+ dataDir,
4
+ ensureEntry
5
+ } from "./chunk-2SSYR7VW.js";
6
+
7
+ // apps/daemon/src/config.ts
8
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { randomBytes } from "node:crypto";
11
+ import YAML from "yaml";
12
+ var DEFAULTS = {
13
+ vault: "",
14
+ http: { host: "127.0.0.1", port: 8787 },
15
+ llm: { provider: "openai-codex", model: "gpt-6-astra" },
16
+ feishu: {},
17
+ reminders: { daysBefore: [7, 3, 1] },
18
+ agent: { mode: "default", maxWritesPerTurn: 10, harnessConfirm: "harness", backend: "builtin" },
19
+ sources: []
20
+ };
21
+ function defaultVaultDir() {
22
+ return dataDir().dir;
23
+ }
24
+ __name(defaultVaultDir, "defaultVaultDir");
25
+ function loadConfig(vault = defaultVaultDir()) {
26
+ ensureEntry(vault);
27
+ for (const d of ["config", "captures", "snapshots", "submissions", "backups", "runs", "profile", "resume/templates", "resume/variants", "research"]) {
28
+ mkdirSync(join(vault, d), { recursive: true });
29
+ }
30
+ const file = configFile(vault);
31
+ let user = {};
32
+ if (existsSync(file)) user = YAML.parse(readFileSync(file, "utf8")) ?? {};
33
+ else replaceConfig(vault, { ...DEFAULTS, vault: void 0 });
34
+ return {
35
+ ...DEFAULTS,
36
+ ...user,
37
+ vault,
38
+ // ZHIRO_PORT: another port for this run (8787 taken, a second instance for a test) without touching the settings
39
+ http: { ...DEFAULTS.http, ...user.http ?? {}, ...process.env.ZHIRO_PORT ? { port: Number(process.env.ZHIRO_PORT) } : {} },
40
+ llm: { ...DEFAULTS.llm, ...user.llm ?? {} },
41
+ feishu: { ...user.feishu ?? {} },
42
+ reminders: { ...DEFAULTS.reminders, ...user.reminders ?? {} },
43
+ agent: { ...DEFAULTS.agent, ...user.agent ?? {} },
44
+ sources: Array.isArray(user.sources) ? user.sources : []
45
+ };
46
+ }
47
+ __name(loadConfig, "loadConfig");
48
+ var configFile = /* @__PURE__ */ __name((vault) => join(vault, "config", "jobos.yaml"), "configFile");
49
+ function writeWhole(vault, doc) {
50
+ const file = configFile(vault);
51
+ const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
52
+ try {
53
+ const fd = openSync(tmp, "wx");
54
+ try {
55
+ writeSync(fd, YAML.stringify(doc), null, "utf8");
56
+ fsyncSync(fd);
57
+ } finally {
58
+ closeSync(fd);
59
+ }
60
+ renameSync(tmp, file);
61
+ } catch (e) {
62
+ rmSync(tmp, { force: true });
63
+ throw e;
64
+ }
65
+ }
66
+ __name(writeWhole, "writeWhole");
67
+ function updateConfig(vault, change) {
68
+ const file = configFile(vault);
69
+ let doc = {};
70
+ try {
71
+ doc = YAML.parse(readFileSync(file, "utf8")) ?? {};
72
+ } catch (e) {
73
+ if (e?.code !== "ENOENT") throw e;
74
+ }
75
+ change(doc);
76
+ writeWhole(vault, doc);
77
+ }
78
+ __name(updateConfig, "updateConfig");
79
+ function replaceConfig(vault, doc) {
80
+ writeWhole(vault, doc);
81
+ }
82
+ __name(replaceConfig, "replaceConfig");
83
+ function clearConfigTemps(vault) {
84
+ const dir = join(vault, "config");
85
+ let names = [];
86
+ try {
87
+ names = readdirSync(dir);
88
+ } catch {
89
+ return;
90
+ }
91
+ for (const f of names) if (/^jobos\.yaml\.(\d+\.[0-9a-f]+\.)?tmp$/.test(f)) rmSync(join(dir, f), { force: true });
92
+ }
93
+ __name(clearConfigTemps, "clearConfigTemps");
94
+ function dbPath(vault) {
95
+ return join(vault, "jobos.db");
96
+ }
97
+ __name(dbPath, "dbPath");
98
+
99
+ export {
100
+ defaultVaultDir,
101
+ loadConfig,
102
+ configFile,
103
+ updateConfig,
104
+ replaceConfig,
105
+ clearConfigTemps,
106
+ dbPath
107
+ };