@bizhou/cli 1.0.0

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.
Files changed (3) hide show
  1. package/LICENSE +201 -0
  2. package/dist/index.js +3839 -0
  3. package/package.json +23 -0
package/dist/index.js ADDED
@@ -0,0 +1,3839 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync as readFileSync2 } from "fs";
5
+ import { dirname as dirname6, join as join12 } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { parseArgs } from "util";
8
+
9
+ // ../core/src/errors.ts
10
+ var BizhouError = class extends Error {
11
+ code;
12
+ constructor(code, message, options) {
13
+ super(message, options);
14
+ this.code = code;
15
+ this.name = `Bizhou${code}Error`;
16
+ }
17
+ };
18
+ var CryptoError = class extends BizhouError {
19
+ constructor(message, options) {
20
+ super("CRYPTO", message, options);
21
+ }
22
+ };
23
+ var AuthError = class extends BizhouError {
24
+ constructor(message, options) {
25
+ super("AUTH", message, options);
26
+ }
27
+ };
28
+ var ManifestError = class extends BizhouError {
29
+ constructor(message, options) {
30
+ super("MANIFEST", message, options);
31
+ }
32
+ };
33
+ var VaultError = class extends BizhouError {
34
+ constructor(message, options) {
35
+ super("VAULT", message, options);
36
+ }
37
+ };
38
+ var InvalidArgError = class extends BizhouError {
39
+ constructor(message, options) {
40
+ super("INVALID_ARG", message, options);
41
+ }
42
+ };
43
+
44
+ // ../core/src/account/index.ts
45
+ var ACCOUNTS_KEY = "accounts";
46
+ var MK_CACHE_KEY = "mkCache";
47
+ var AccountManager = class {
48
+ constructor(secrets) {
49
+ this.secrets = secrets;
50
+ }
51
+ secrets;
52
+ async state() {
53
+ const raw = await this.secrets.get(ACCOUNTS_KEY);
54
+ if (!raw) return { accounts: {} };
55
+ return JSON.parse(raw);
56
+ }
57
+ async save(state) {
58
+ await this.secrets.set(ACCOUNTS_KEY, JSON.stringify(state));
59
+ }
60
+ /** 新增/更新账号 token;若是首个账号则自动设为当前。 */
61
+ async upsertAccount(name, tokens) {
62
+ if (!name) throw new BizhouError("ACCOUNT", "\u8D26\u53F7\u540D\u4E0D\u80FD\u4E3A\u7A7A");
63
+ const state = await this.state();
64
+ state.accounts[name] = tokens;
65
+ if (!state.current) state.current = name;
66
+ await this.save(state);
67
+ }
68
+ async listAccounts() {
69
+ const state = await this.state();
70
+ return { names: Object.keys(state.accounts), current: state.current };
71
+ }
72
+ async useAccount(name) {
73
+ const state = await this.state();
74
+ if (!state.accounts[name]) throw new BizhouError("ACCOUNT", `\u8D26\u53F7\u4E0D\u5B58\u5728\uFF1A${name}`);
75
+ state.current = name;
76
+ await this.save(state);
77
+ }
78
+ async removeAccount(name) {
79
+ const state = await this.state();
80
+ if (!state.accounts[name]) throw new BizhouError("ACCOUNT", `\u8D26\u53F7\u4E0D\u5B58\u5728\uFF1A${name}`);
81
+ delete state.accounts[name];
82
+ if (state.current === name) state.current = Object.keys(state.accounts)[0];
83
+ await this.save(state);
84
+ }
85
+ async getCurrent() {
86
+ const state = await this.state();
87
+ if (!state.current) return void 0;
88
+ const tokens = state.accounts[state.current];
89
+ if (!tokens) return void 0;
90
+ return { name: state.current, tokens };
91
+ }
92
+ /** 更新指定账号 token(如刷新后)。 */
93
+ async updateTokens(name, tokens) {
94
+ const state = await this.state();
95
+ if (!state.accounts[name]) throw new BizhouError("ACCOUNT", `\u8D26\u53F7\u4E0D\u5B58\u5728\uFF1A${name}`);
96
+ state.accounts[name] = tokens;
97
+ await this.save(state);
98
+ }
99
+ // ---- 解锁后的 MK 缓存("每设备解锁一次")--------------------------------
100
+ /** 缓存解锁得到的 MK(base64),带绝对过期时间戳。 */
101
+ async cacheMk(mk, expiresAt) {
102
+ await this.secrets.set(MK_CACHE_KEY, JSON.stringify({ mk: mk.toString("base64"), expiresAt }));
103
+ }
104
+ /** 读取缓存 MK;未解锁或已过期(相对传入的 now)返回 undefined。 */
105
+ async getCachedMk(now) {
106
+ const raw = await this.secrets.get(MK_CACHE_KEY);
107
+ if (!raw) return void 0;
108
+ const { mk, expiresAt } = JSON.parse(raw);
109
+ if (typeof expiresAt === "number" && now > expiresAt) {
110
+ await this.secrets.delete(MK_CACHE_KEY);
111
+ return void 0;
112
+ }
113
+ return Buffer.from(mk, "base64");
114
+ }
115
+ /** 清除 MK 缓存(`bz lock`)。 */
116
+ async clearMk() {
117
+ await this.secrets.delete(MK_CACHE_KEY);
118
+ }
119
+ };
120
+
121
+ // ../core/src/baidu/client.ts
122
+ import { createHash } from "crypto";
123
+ var PAN_API = "https://pan.baidu.com/rest/2.0/xpan";
124
+ var PCS_SUPERFILE = "https://d.pcs.baidu.com/rest/2.0/pcs/superfile2";
125
+ var APP_ROOT = "/apps/bizhou";
126
+ var TRANSFER_SLICE = 4 * 1024 * 1024;
127
+ function md5hex(b) {
128
+ return createHash("md5").update(b).digest("hex");
129
+ }
130
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
131
+ async function withRetry(fn, opts = {}) {
132
+ const tries = opts.tries ?? 3;
133
+ const baseMs = opts.baseMs ?? 500;
134
+ let lastErr;
135
+ for (let i = 0; i < tries; i++) {
136
+ if (opts.signal?.aborted) throw lastErr ?? new BizhouError("BAIDU", "\u4E0A\u4F20\u5DF2\u53D6\u6D88");
137
+ try {
138
+ return await fn();
139
+ } catch (err) {
140
+ lastErr = err;
141
+ if (opts.signal?.aborted) throw err;
142
+ if (i < tries - 1) {
143
+ opts.onRetry?.(i + 1, err);
144
+ await sleep(baseMs * 2 ** i);
145
+ }
146
+ }
147
+ }
148
+ throw lastErr;
149
+ }
150
+ function sliceTransfer(data) {
151
+ const parts = [];
152
+ for (let off = 0; off < data.length; off += TRANSFER_SLICE) {
153
+ parts.push(data.subarray(off, Math.min(off + TRANSFER_SLICE, data.length)));
154
+ }
155
+ if (parts.length === 0) parts.push(Buffer.alloc(0));
156
+ return parts;
157
+ }
158
+ function lastSegment(path) {
159
+ return path.slice(path.lastIndexOf("/") + 1);
160
+ }
161
+ function form(params) {
162
+ return Object.entries(params).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
163
+ }
164
+ var BaiduClient = class {
165
+ constructor(_config, accessToken, http, opts = {}) {
166
+ this._config = _config;
167
+ this.accessToken = accessToken;
168
+ this.http = http;
169
+ this.maxRetries = opts.maxRetries ?? 3;
170
+ this.uploadConcurrency = Math.min(16, Math.max(1, opts.uploadConcurrency ?? 4));
171
+ }
172
+ _config;
173
+ accessToken;
174
+ http;
175
+ maxRetries;
176
+ uploadConcurrency;
177
+ setAccessToken(token) {
178
+ this.accessToken = token;
179
+ }
180
+ async fileApi(method, query, body) {
181
+ const url = `${PAN_API}/file?${form({ method, access_token: this.accessToken, ...query })}`;
182
+ const res = await this.http(url, {
183
+ method: body ? "POST" : "GET",
184
+ headers: body ? { "content-type": "application/x-www-form-urlencoded" } : void 0,
185
+ body
186
+ });
187
+ const data = await res.json();
188
+ const errno = data.errno;
189
+ if (typeof errno === "number" && errno !== 0) {
190
+ throw new BizhouError("BAIDU", `\u767E\u5EA6\u6587\u4EF6 API \u9519\u8BEF\uFF1Amethod=${method} errno=${errno}`);
191
+ }
192
+ return data;
193
+ }
194
+ /** 预创建,拿 uploadid 与需上传的分片列表。 */
195
+ async precreate(path, size, blockMd5) {
196
+ const data = await this.fileApi(
197
+ "precreate",
198
+ {},
199
+ form({
200
+ path,
201
+ size: String(size),
202
+ isdir: "0",
203
+ autoinit: "1",
204
+ rtype: "3",
205
+ block_list: JSON.stringify(blockMd5)
206
+ })
207
+ );
208
+ if (typeof data.uploadid !== "string") {
209
+ throw new BizhouError("BAIDU", "precreate \u672A\u8FD4\u56DE uploadid");
210
+ }
211
+ const blocks = Array.isArray(data.block_list) ? data.block_list : blockMd5.map((_, i) => i);
212
+ return { uploadid: data.uploadid, blocksToUpload: blocks };
213
+ }
214
+ /** 上传一个 4MB 传输分片,返回其 md5。 */
215
+ async uploadSlice(path, uploadid, partseq, slice, signal) {
216
+ const url = `${PCS_SUPERFILE}?${form({
217
+ method: "upload",
218
+ access_token: this.accessToken,
219
+ type: "tmpfile",
220
+ path,
221
+ uploadid,
222
+ partseq: String(partseq)
223
+ })}`;
224
+ const fd = new FormData();
225
+ fd.append("file", new Blob([new Uint8Array(slice)]), "part");
226
+ const res = await this.http(url, { method: "POST", body: fd, signal });
227
+ const data = await res.json();
228
+ if (typeof data.md5 !== "string") {
229
+ throw new BizhouError("BAIDU", `superfile2 \u5206\u7247 ${partseq} \u672A\u8FD4\u56DE md5`);
230
+ }
231
+ return data.md5;
232
+ }
233
+ /** 合并落盘。 */
234
+ async create(path, size, uploadid, blockMd5) {
235
+ const data = await this.fileApi(
236
+ "create",
237
+ {},
238
+ form({
239
+ path,
240
+ size: String(size),
241
+ isdir: "0",
242
+ uploadid,
243
+ rtype: "3",
244
+ block_list: JSON.stringify(blockMd5)
245
+ })
246
+ );
247
+ return { fsId: typeof data.fs_id === "number" ? data.fs_id : void 0 };
248
+ }
249
+ /**
250
+ * 上传一个逻辑分片文件(≤100MB)到 path:precreate → 逐 4MB superfile2 → create。
251
+ * 复用 uploadid,precreate 返回的 blocksToUpload 天然支持断点续传。
252
+ */
253
+ async uploadPart(path, data, onSlice) {
254
+ const slices = sliceTransfer(data);
255
+ const blockMd5 = slices.map(md5hex);
256
+ const { uploadid, blocksToUpload } = await this.precreate(path, data.length, blockMd5);
257
+ const need = blocksToUpload.filter((i) => i >= 0 && i < slices.length);
258
+ const total = slices.length;
259
+ const ac = new AbortController();
260
+ let nextK = 0;
261
+ const worker = async () => {
262
+ while (true) {
263
+ if (ac.signal.aborted) return;
264
+ const k = nextK++;
265
+ if (k >= need.length) return;
266
+ const i = need[k];
267
+ await withRetry(() => this.uploadSlice(path, uploadid, i, slices[i], ac.signal), {
268
+ tries: this.maxRetries,
269
+ signal: ac.signal
270
+ });
271
+ onSlice?.(i, total);
272
+ }
273
+ };
274
+ const poolN = Math.min(this.uploadConcurrency, Math.max(1, need.length));
275
+ try {
276
+ await Promise.all(Array.from({ length: poolN }, () => worker()));
277
+ } catch (err) {
278
+ ac.abort();
279
+ throw err;
280
+ }
281
+ return this.create(path, data.length, uploadid, blockMd5);
282
+ }
283
+ /** 列目录。 */
284
+ async list(dir) {
285
+ const data = await this.fileApi("list", { dir, order: "name" });
286
+ const list = Array.isArray(data.list) ? data.list : [];
287
+ return list.map((e) => ({
288
+ fsId: Number(e.fs_id),
289
+ path: String(e.path),
290
+ filename: String(e.server_filename ?? e.filename ?? ""),
291
+ size: Number(e.size ?? 0),
292
+ isdir: Number(e.isdir ?? 0) === 1
293
+ }));
294
+ }
295
+ /** 取 dlink(下载直链)。 */
296
+ async filemetas(fsIds) {
297
+ const url = `${PAN_API}/multimedia?${form({
298
+ method: "filemetas",
299
+ access_token: this.accessToken,
300
+ fsids: JSON.stringify(fsIds),
301
+ dlink: "1"
302
+ })}`;
303
+ const res = await this.http(url);
304
+ const data = await res.json();
305
+ const errno = data.errno;
306
+ if (typeof errno === "number" && errno !== 0) {
307
+ throw new BizhouError("BAIDU", `filemetas \u9519\u8BEF errno=${errno}`);
308
+ }
309
+ const list = Array.isArray(data.list) ? data.list : [];
310
+ return list.map((e) => ({
311
+ fsId: Number(e.fs_id),
312
+ dlink: String(e.dlink ?? ""),
313
+ filename: String(e.filename ?? e.server_filename ?? "")
314
+ }));
315
+ }
316
+ /** filemanager 通用封装:POST filemanager?opera=... body async=0&filelist=[...]。 */
317
+ async fileManagerOp(opera, filelist) {
318
+ await this.fileApi(
319
+ "filemanager",
320
+ { opera },
321
+ form({ async: "0", filelist: JSON.stringify(filelist), ondup: "fail" })
322
+ );
323
+ }
324
+ /** 删除远端路径(文件或目录)。 */
325
+ async deletePaths(paths) {
326
+ await this.fileManagerOp("delete", paths);
327
+ }
328
+ /** 移动 srcPath 到 dstDir 下(目录级,保留原名)。 */
329
+ async move(srcPath, dstDir) {
330
+ await this.fileManagerOp("move", [
331
+ { path: srcPath, dest: dstDir, newname: lastSegment(srcPath) }
332
+ ]);
333
+ }
334
+ /** 复制 srcPath 到 dstDir 下(目录级,保留原名;源保留)。 */
335
+ async copy(srcPath, dstDir) {
336
+ await this.fileManagerOp("copy", [
337
+ { path: srcPath, dest: dstDir, newname: lastSegment(srcPath) }
338
+ ]);
339
+ }
340
+ /** 原地改名(同目录下改末段名)。 */
341
+ async rename(srcPath, newName) {
342
+ await this.fileManagerOp("rename", [{ path: srcPath, newname: newName }]);
343
+ }
344
+ /** 创建目录(xpan create isdir=1,等价 mkdir -p)。 */
345
+ async mkdir(path) {
346
+ await this.fileApi("create", {}, form({ path, isdir: "1", rtype: "3" }));
347
+ }
348
+ /** 通过 dlink 下载文件字节(瞬时失败重试)。 */
349
+ async download(dlink) {
350
+ const sep3 = dlink.includes("?") ? "&" : "?";
351
+ const url = `${dlink}${sep3}access_token=${encodeURIComponent(this.accessToken)}`;
352
+ return withRetry(
353
+ async () => {
354
+ const res = await this.http(url, { headers: { "User-Agent": "pan.baidu.com" } });
355
+ if (!res.ok) {
356
+ throw new BizhouError("BAIDU", `\u4E0B\u8F7D\u5931\u8D25\uFF1AHTTP ${res.status}`);
357
+ }
358
+ return Buffer.from(await res.arrayBuffer());
359
+ },
360
+ { tries: this.maxRetries }
361
+ );
362
+ }
363
+ };
364
+
365
+ // ../core/src/bundle/index.ts
366
+ import { randomBytes as randomBytes2 } from "crypto";
367
+
368
+ // ../core/src/crypto/index.ts
369
+ import {
370
+ createCipheriv,
371
+ createDecipheriv,
372
+ createHmac,
373
+ randomBytes,
374
+ scrypt as scryptCallback,
375
+ timingSafeEqual
376
+ } from "crypto";
377
+ import { promisify } from "util";
378
+ var scryptAsync = promisify(scryptCallback);
379
+ var CIPHER_ALGO = "aes-256-gcm";
380
+ var KEY_BYTES = 32;
381
+ var IV_BYTES = 12;
382
+ var TAG_BYTES = 16;
383
+ var SALT_BYTES = 16;
384
+ var DEFAULT_SCRYPT = {
385
+ algo: "scrypt",
386
+ N: 1 << 15,
387
+ r: 8,
388
+ p: 1,
389
+ keylen: KEY_BYTES
390
+ };
391
+ function assertKey(key) {
392
+ if (key.length !== KEY_BYTES) {
393
+ throw new CryptoError(`\u5BC6\u94A5\u957F\u5EA6\u5FC5\u987B\u4E3A ${KEY_BYTES} \u5B57\u8282\uFF0C\u5B9E\u9645 ${key.length}`);
394
+ }
395
+ }
396
+ function deriveDeterministicIv(key, context) {
397
+ return createHmac("sha256", key).update(context).digest().subarray(0, IV_BYTES);
398
+ }
399
+ function aeadEncrypt(key, plaintext, aad, iv) {
400
+ assertKey(key);
401
+ if (iv && iv.length !== IV_BYTES) throw new CryptoError(`IV \u957F\u5EA6\u5FC5\u987B\u4E3A ${IV_BYTES} \u5B57\u8282`);
402
+ const nonce = iv ?? randomBytes(IV_BYTES);
403
+ const cipher = createCipheriv(CIPHER_ALGO, key, nonce, { authTagLength: TAG_BYTES });
404
+ if (aad) cipher.setAAD(aad);
405
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
406
+ const tag = cipher.getAuthTag();
407
+ return { iv: nonce, ciphertext, tag };
408
+ }
409
+ function aeadDecrypt(key, iv, ciphertext, tag, aad) {
410
+ assertKey(key);
411
+ if (iv.length !== IV_BYTES) throw new CryptoError(`IV \u957F\u5EA6\u5FC5\u987B\u4E3A ${IV_BYTES} \u5B57\u8282`);
412
+ if (tag.length !== TAG_BYTES) throw new CryptoError(`tag \u957F\u5EA6\u5FC5\u987B\u4E3A ${TAG_BYTES} \u5B57\u8282`);
413
+ const decipher = createDecipheriv(CIPHER_ALGO, key, iv, { authTagLength: TAG_BYTES });
414
+ if (aad) decipher.setAAD(aad);
415
+ decipher.setAuthTag(tag);
416
+ try {
417
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
418
+ } catch (cause) {
419
+ throw new AuthError("\u89E3\u5BC6\u5931\u8D25\uFF1AGCM \u8BA4\u8BC1\u6807\u7B7E\u6821\u9A8C\u4E0D\u901A\u8FC7\uFF08\u5185\u5BB9\u88AB\u7BE1\u6539\u3001\u5BC6\u94A5\u9519\u8BEF\u6216 AAD \u4E0D\u5339\u914D\uFF09", {
420
+ cause
421
+ });
422
+ }
423
+ }
424
+ function packAead(r) {
425
+ return Buffer.concat([r.iv, r.tag, r.ciphertext]);
426
+ }
427
+ function unpackAead(blob) {
428
+ const min = IV_BYTES + TAG_BYTES;
429
+ if (blob.length < min) {
430
+ throw new CryptoError(`AEAD blob \u8FC7\u77ED\uFF1A\u81F3\u5C11 ${min} \u5B57\u8282\uFF0C\u5B9E\u9645 ${blob.length}`);
431
+ }
432
+ const iv = blob.subarray(0, IV_BYTES);
433
+ const tag = blob.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);
434
+ const ciphertext = blob.subarray(IV_BYTES + TAG_BYTES);
435
+ return { iv, ciphertext, tag };
436
+ }
437
+ function sealToBase64(key, plaintext, aad) {
438
+ return packAead(aeadEncrypt(key, plaintext, aad)).toString("base64");
439
+ }
440
+ function openFromBase64(key, b64, aad) {
441
+ const { iv, ciphertext, tag } = unpackAead(Buffer.from(b64, "base64"));
442
+ return aeadDecrypt(key, iv, ciphertext, tag, aad);
443
+ }
444
+ function generateSalt() {
445
+ return randomBytes(SALT_BYTES);
446
+ }
447
+ async function deriveKey(password, salt, params = DEFAULT_SCRYPT) {
448
+ if (params.algo !== "scrypt") {
449
+ throw new InvalidArgError(`\u4E0D\u652F\u6301\u7684 KDF \u7B97\u6CD5\uFF1A${String(params.algo)}`);
450
+ }
451
+ const pw = Buffer.from(password.normalize("NFKC"), "utf8");
452
+ const maxmem = 256 * params.N * params.r + 1024 * 1024;
453
+ const key = await scryptAsync(pw, salt, params.keylen, {
454
+ N: params.N,
455
+ r: params.r,
456
+ p: params.p,
457
+ maxmem
458
+ });
459
+ return key;
460
+ }
461
+ function generateKey() {
462
+ return randomBytes(KEY_BYTES);
463
+ }
464
+ function wrapKey(kek, key) {
465
+ assertKey(key);
466
+ return sealToBase64(kek, key);
467
+ }
468
+ function unwrapKey(kek, wrapped) {
469
+ const key = openFromBase64(kek, wrapped);
470
+ assertKey(key);
471
+ return key;
472
+ }
473
+
474
+ // ../core/src/bundle/index.ts
475
+ var MANIFEST_VERSION = 1;
476
+ var BUNDLE_SUFFIX = ".bz";
477
+ var MANIFEST_FILENAME = "manifest.json";
478
+ var PREVIEW_FILENAME = "preview.part";
479
+ function generateBundleId() {
480
+ return randomBytes2(16).toString("hex");
481
+ }
482
+ function bundleDirName(bundleId) {
483
+ return `${bundleId}${BUNDLE_SUFFIX}`;
484
+ }
485
+ function chunkFileName(seq) {
486
+ return `${String(seq).padStart(3, "0")}.part`;
487
+ }
488
+ function chunkAad(bundleId, seq) {
489
+ return Buffer.from(`${bundleId}:${seq}`, "utf8");
490
+ }
491
+ function sealMeta(dek, meta) {
492
+ return sealToBase64(dek, Buffer.from(JSON.stringify(meta), "utf8"));
493
+ }
494
+ function openMeta(dek, encMeta) {
495
+ const json = openFromBase64(dek, encMeta).toString("utf8");
496
+ let parsed;
497
+ try {
498
+ parsed = JSON.parse(json);
499
+ } catch (cause) {
500
+ throw new ManifestError("encMeta \u89E3\u5BC6\u540E\u4E0D\u662F\u5408\u6CD5 JSON", { cause });
501
+ }
502
+ const m = parsed;
503
+ if (typeof m.name !== "string" || typeof m.size !== "number") {
504
+ throw new ManifestError("encMeta \u7F3A\u5C11\u5FC5\u9700\u5B57\u6BB5 name/size");
505
+ }
506
+ return parsed;
507
+ }
508
+ function serializeManifest(m) {
509
+ const ordered = {
510
+ version: m.version,
511
+ bundleId: m.bundleId,
512
+ createdAt: m.createdAt,
513
+ cipher: m.cipher,
514
+ compression: m.compression,
515
+ chunkSize: m.chunkSize,
516
+ wrappedKey: m.wrappedKey,
517
+ chunks: m.chunks.map((c2) => ({
518
+ seq: c2.seq,
519
+ file: c2.file,
520
+ plainSize: c2.plainSize,
521
+ encSize: c2.encSize,
522
+ iv: c2.iv,
523
+ tag: c2.tag,
524
+ sha256: c2.sha256
525
+ })),
526
+ ...m.preview ? { preview: m.preview } : {},
527
+ encMeta: m.encMeta
528
+ };
529
+ return JSON.stringify(ordered, null, 2);
530
+ }
531
+ function req(obj, key, type) {
532
+ const v = obj[key];
533
+ if (typeof v !== type) {
534
+ throw new ManifestError(`manifest \u5B57\u6BB5 '${key}' \u5E94\u4E3A ${type}\uFF0C\u5B9E\u9645 ${typeof v}`);
535
+ }
536
+ return v;
537
+ }
538
+ function validateChunk(raw, index) {
539
+ if (typeof raw !== "object" || raw === null) {
540
+ throw new ManifestError(`manifest.chunks[${index}] \u4E0D\u662F\u5BF9\u8C61`);
541
+ }
542
+ const c2 = raw;
543
+ const seq = req(c2, "seq", "number");
544
+ const chunk = {
545
+ seq,
546
+ file: req(c2, "file", "string"),
547
+ plainSize: req(c2, "plainSize", "number"),
548
+ encSize: req(c2, "encSize", "number"),
549
+ iv: req(c2, "iv", "string"),
550
+ tag: req(c2, "tag", "string"),
551
+ sha256: req(c2, "sha256", "string")
552
+ };
553
+ if (chunk.seq !== index) {
554
+ throw new ManifestError(
555
+ `manifest.chunks[${index}] \u7684 seq=${chunk.seq} \u4E0E\u4F4D\u7F6E\u4E0D\u7B26\uFF08\u5E94\u8FDE\u7EED\u4ECE 0\uFF09`
556
+ );
557
+ }
558
+ return chunk;
559
+ }
560
+ function parseManifest(json) {
561
+ let parsed;
562
+ try {
563
+ parsed = JSON.parse(json);
564
+ } catch (cause) {
565
+ throw new ManifestError("manifest \u4E0D\u662F\u5408\u6CD5 JSON", { cause });
566
+ }
567
+ if (typeof parsed !== "object" || parsed === null) {
568
+ throw new ManifestError("manifest \u9876\u5C42\u4E0D\u662F\u5BF9\u8C61");
569
+ }
570
+ const o = parsed;
571
+ const version2 = req(o, "version", "number");
572
+ if (version2 !== MANIFEST_VERSION) {
573
+ throw new ManifestError(`\u4E0D\u652F\u6301\u7684 manifest \u7248\u672C\uFF1A${version2}\uFF08\u5F53\u524D\u4EC5\u652F\u6301 ${MANIFEST_VERSION}\uFF09`);
574
+ }
575
+ const cipher = req(o, "cipher", "string");
576
+ if (cipher !== "AES-256-GCM") {
577
+ throw new ManifestError(`\u4E0D\u652F\u6301\u7684 cipher\uFF1A${cipher}`);
578
+ }
579
+ const compression = req(o, "compression", "string");
580
+ if (compression !== "none" && compression !== "gzip") {
581
+ throw new ManifestError(`\u4E0D\u652F\u6301\u7684 compression\uFF1A${compression}`);
582
+ }
583
+ if (!Array.isArray(o.chunks)) {
584
+ throw new ManifestError("manifest.chunks \u5FC5\u987B\u662F\u6570\u7EC4");
585
+ }
586
+ const chunks = o.chunks.map(validateChunk);
587
+ let preview;
588
+ if (o.preview !== void 0) {
589
+ if (typeof o.preview !== "object" || o.preview === null) {
590
+ throw new ManifestError("manifest.preview \u4E0D\u662F\u5BF9\u8C61");
591
+ }
592
+ const p = o.preview;
593
+ const kind = req(p, "kind", "string");
594
+ if (kind !== "video" && kind !== "audio" && kind !== "image" && kind !== "text") {
595
+ throw new ManifestError(`\u4E0D\u652F\u6301\u7684 preview.kind\uFF1A${kind}`);
596
+ }
597
+ preview = {
598
+ file: req(p, "file", "string"),
599
+ kind,
600
+ iv: req(p, "iv", "string"),
601
+ tag: req(p, "tag", "string")
602
+ };
603
+ }
604
+ return {
605
+ version: MANIFEST_VERSION,
606
+ bundleId: req(o, "bundleId", "string"),
607
+ createdAt: req(o, "createdAt", "string"),
608
+ cipher: "AES-256-GCM",
609
+ compression,
610
+ chunkSize: req(o, "chunkSize", "number"),
611
+ wrappedKey: req(o, "wrappedKey", "string"),
612
+ chunks,
613
+ ...preview ? { preview } : {},
614
+ encMeta: req(o, "encMeta", "string")
615
+ };
616
+ }
617
+
618
+ // ../core/src/cloudpath/index.ts
619
+ import { basename, dirname, isAbsolute, join, relative } from "path";
620
+ function normalizeCloudPath(p) {
621
+ const parts = p.split(/[/\\]/).filter((s) => s.length > 0 && s !== ".");
622
+ if (parts.includes("..")) {
623
+ throw new InvalidArgError(`\u4E91\u7AEF\u8DEF\u5F84\u4E0D\u5141\u8BB8 '..' \u6BB5\uFF1A${p}`);
624
+ }
625
+ return parts.length === 0 ? "/" : `/${parts.join("/")}`;
626
+ }
627
+ function joinCloudPath(...parts) {
628
+ return normalizeCloudPath(parts.join("/"));
629
+ }
630
+ function cloudBasename(p) {
631
+ const n = normalizeCloudPath(p);
632
+ if (n === "/") return "";
633
+ return n.slice(n.lastIndexOf("/") + 1);
634
+ }
635
+ function assertNameSegment(name) {
636
+ if (name === "" || name === "." || name === ".." || /[/\\]/.test(name)) {
637
+ throw new InvalidArgError(`\u975E\u6CD5\u540D\u79F0\uFF08\u987B\u4E3A\u5355\u6BB5\u6587\u4EF6\u540D\uFF0C\u4E0D\u542B\u8DEF\u5F84\u5206\u9694\u7B26/..\uFF09\uFF1A${name}`);
638
+ }
639
+ }
640
+ function defaultUploadCloudDir(sourceAbs, fileRoot) {
641
+ const rel = relative(fileRoot, dirname(sourceAbs));
642
+ if (rel === "") return "/";
643
+ if (isAbsolute(rel) || rel.split(/[/\\]/)[0] === "..") return "/";
644
+ return normalizeCloudPath(rel);
645
+ }
646
+ function downloadLocalPath(fileRoot, cloudDir, name) {
647
+ const segs = normalizeCloudPath(cloudDir).split("/").filter(Boolean);
648
+ const safeName = basename(name.replace(/\\/g, "/"));
649
+ if (safeName === "" || safeName === "." || safeName === "..") {
650
+ throw new InvalidArgError(`\u975E\u6CD5\u6587\u4EF6\u540D\uFF08\u89E3\u5BC6\u5143\u6570\u636E\u53EF\u80FD\u88AB\u7BE1\u6539\uFF09\uFF1A${name}`);
651
+ }
652
+ return join(fileRoot, ...segs, safeName);
653
+ }
654
+
655
+ // ../core/src/baidu/store.ts
656
+ var BaiduBundleStore = class {
657
+ constructor(client, bundleId, cloudDir = "") {
658
+ this.client = client;
659
+ this.bundleId = bundleId;
660
+ this.dir = `${APP_ROOT}${joinCloudPath("/", cloudDir, bundleDirName(bundleId))}`;
661
+ }
662
+ client;
663
+ bundleId;
664
+ dir;
665
+ fsidCache;
666
+ remotePath(filename) {
667
+ return `${this.dir}/${filename}`;
668
+ }
669
+ async refreshFsids() {
670
+ const entries = await this.client.list(this.dir);
671
+ const map = /* @__PURE__ */ new Map();
672
+ for (const e of entries) if (!e.isdir) map.set(e.filename, e.fsId);
673
+ this.fsidCache = map;
674
+ return map;
675
+ }
676
+ async fsidOf(filename) {
677
+ let map = this.fsidCache ?? await this.refreshFsids();
678
+ if (!map.has(filename)) map = await this.refreshFsids();
679
+ const fsid = map.get(filename);
680
+ if (fsid === void 0) {
681
+ throw new BizhouError("BAIDU", `\u8FDC\u7AEF\u7F3A\u5C11\u6587\u4EF6\uFF1A${this.remotePath(filename)}`);
682
+ }
683
+ return fsid;
684
+ }
685
+ async downloadByName(filename) {
686
+ const fsid = await this.fsidOf(filename);
687
+ const metas = await this.client.filemetas([fsid]);
688
+ const dlink = metas[0]?.dlink;
689
+ if (!dlink) throw new BizhouError("BAIDU", `\u65E0\u6CD5\u83B7\u53D6 dlink\uFF1A${filename}`);
690
+ return this.client.download(dlink);
691
+ }
692
+ async putChunk(seq, data) {
693
+ await this.client.uploadPart(this.remotePath(chunkFileName(seq)), data);
694
+ this.fsidCache = void 0;
695
+ }
696
+ async getChunk(seq) {
697
+ return this.downloadByName(chunkFileName(seq));
698
+ }
699
+ async putManifest(json) {
700
+ await this.client.uploadPart(this.remotePath(MANIFEST_FILENAME), Buffer.from(json, "utf8"));
701
+ this.fsidCache = void 0;
702
+ }
703
+ async getManifest() {
704
+ return (await this.downloadByName(MANIFEST_FILENAME)).toString("utf8");
705
+ }
706
+ async putPreview(data) {
707
+ await this.client.uploadPart(this.remotePath(PREVIEW_FILENAME), data);
708
+ this.fsidCache = void 0;
709
+ }
710
+ async getPreview() {
711
+ return this.downloadByName(PREVIEW_FILENAME);
712
+ }
713
+ async listChunks() {
714
+ const map = await this.refreshFsids();
715
+ const seqs = [];
716
+ for (const name of map.keys()) {
717
+ const m = /^(\d+)\.part$/.exec(name);
718
+ if (m) seqs.push(Number(m[1]));
719
+ }
720
+ return seqs.sort((a, b) => a - b);
721
+ }
722
+ async remove() {
723
+ await this.client.deletePaths([this.dir]);
724
+ this.fsidCache = void 0;
725
+ }
726
+ };
727
+
728
+ // ../core/src/backend/baidu.ts
729
+ var NO_TRASH_MANAGEMENT_MSG = "\u767E\u5EA6\u5F00\u653E\u5E73\u53F0\u672A\u63D0\u4F9B\u56DE\u6536\u7AD9\u7BA1\u7406\u63A5\u53E3\uFF0C\u8BF7\u5230\u767E\u5EA6\u7F51\u76D8 App/\u7F51\u9875\u7684\u56DE\u6536\u7AD9\u64CD\u4F5C";
730
+ var BaiduBackend = class {
731
+ constructor(client) {
732
+ this.client = client;
733
+ }
734
+ client;
735
+ remote(cloudDir) {
736
+ const n = normalizeCloudPath(cloudDir);
737
+ return n === "/" ? APP_ROOT : `${APP_ROOT}${n}`;
738
+ }
739
+ async mkdir(cloudDir) {
740
+ await this.client.mkdir(this.remote(cloudDir));
741
+ }
742
+ async listDir(cloudDir) {
743
+ const dir = normalizeCloudPath(cloudDir);
744
+ const entries = await this.client.list(this.remote(cloudDir));
745
+ const dirs = [];
746
+ const bundles = [];
747
+ for (const e of entries) {
748
+ if (!e.isdir) continue;
749
+ if (e.filename.endsWith(BUNDLE_SUFFIX)) {
750
+ bundles.push({ id: e.filename.slice(0, -BUNDLE_SUFFIX.length), dir });
751
+ } else {
752
+ dirs.push(e.filename);
753
+ }
754
+ }
755
+ return { dirs, bundles };
756
+ }
757
+ bundleStore(bundleId, cloudDir) {
758
+ return new BaiduBundleStore(this.client, bundleId, cloudDir);
759
+ }
760
+ async move(srcCloudPath, dstDir) {
761
+ await this.client.move(this.remote(srcCloudPath), this.remote(dstDir));
762
+ }
763
+ async copy(srcCloudPath, dstDir) {
764
+ await this.client.copy(this.remote(srcCloudPath), this.remote(dstDir));
765
+ }
766
+ async rename(srcCloudPath, newName) {
767
+ assertNameSegment(newName);
768
+ await this.client.rename(this.remote(srcCloudPath), newName);
769
+ }
770
+ /** 删到百度原生回收站(filemanager delete)。deletedAt 由核心库以外注入,此后端不使用。 */
771
+ async trashPath(cloudPath, _deletedAt) {
772
+ await this.client.deletePaths([this.remote(cloudPath)]);
773
+ }
774
+ async listTrash() {
775
+ throw new BizhouError("BAIDU", NO_TRASH_MANAGEMENT_MSG);
776
+ }
777
+ async restoreTrash(_entryId) {
778
+ throw new BizhouError("BAIDU", NO_TRASH_MANAGEMENT_MSG);
779
+ }
780
+ async deleteTrash(_entryId) {
781
+ throw new BizhouError("BAIDU", NO_TRASH_MANAGEMENT_MSG);
782
+ }
783
+ async clearTrash() {
784
+ throw new BizhouError("BAIDU", NO_TRASH_MANAGEMENT_MSG);
785
+ }
786
+ };
787
+
788
+ // ../core/src/backend/local.ts
789
+ import { randomBytes as randomBytes3 } from "crypto";
790
+ import { cp, mkdir as mkdir2, readdir as readdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
791
+ import { dirname as dirname2, join as join3 } from "path";
792
+
793
+ // ../core/src/store/index.ts
794
+ import { mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
795
+ import { join as join2 } from "path";
796
+ var LocalBundleStore = class {
797
+ bundleId;
798
+ dir;
799
+ constructor(baseDir, bundleId) {
800
+ this.bundleId = bundleId;
801
+ this.dir = join2(baseDir, bundleDirName(bundleId));
802
+ }
803
+ /** bundle 目录路径。 */
804
+ get path() {
805
+ return this.dir;
806
+ }
807
+ async ensureDir() {
808
+ await mkdir(this.dir, { recursive: true });
809
+ }
810
+ async putChunk(seq, data) {
811
+ await this.ensureDir();
812
+ await writeFile(join2(this.dir, chunkFileName(seq)), data);
813
+ }
814
+ async getChunk(seq) {
815
+ return readFile(join2(this.dir, chunkFileName(seq)));
816
+ }
817
+ async putManifest(json) {
818
+ await this.ensureDir();
819
+ await writeFile(join2(this.dir, MANIFEST_FILENAME), json, "utf8");
820
+ }
821
+ async getManifest() {
822
+ return readFile(join2(this.dir, MANIFEST_FILENAME), "utf8");
823
+ }
824
+ async putPreview(data) {
825
+ await this.ensureDir();
826
+ await writeFile(join2(this.dir, PREVIEW_FILENAME), data);
827
+ }
828
+ async getPreview() {
829
+ return readFile(join2(this.dir, PREVIEW_FILENAME));
830
+ }
831
+ async listChunks() {
832
+ let entries;
833
+ try {
834
+ entries = await readdir(this.dir);
835
+ } catch {
836
+ return [];
837
+ }
838
+ const seqs = [];
839
+ for (const name of entries) {
840
+ const m = /^(\d+)\.part$/.exec(name);
841
+ if (m) seqs.push(Number(m[1]));
842
+ }
843
+ return seqs.sort((a, b) => a - b);
844
+ }
845
+ async remove() {
846
+ await rm(this.dir, { recursive: true, force: true });
847
+ }
848
+ };
849
+
850
+ // ../core/src/backend/local.ts
851
+ var TRASH_DIR = ".trash";
852
+ var LocalBackend = class {
853
+ constructor(baseDir) {
854
+ this.baseDir = baseDir;
855
+ }
856
+ baseDir;
857
+ abs(cloudDir) {
858
+ const n = normalizeCloudPath(cloudDir);
859
+ return n === "/" ? this.baseDir : join3(this.baseDir, ...n.split("/").filter(Boolean));
860
+ }
861
+ get trashRoot() {
862
+ return join3(this.baseDir, TRASH_DIR);
863
+ }
864
+ trashMetaPath(entryId) {
865
+ assertNameSegment(entryId);
866
+ return join3(this.trashRoot, `${entryId}.json`);
867
+ }
868
+ trashItemDir(entryId) {
869
+ assertNameSegment(entryId);
870
+ return join3(this.trashRoot, entryId);
871
+ }
872
+ async readTrashEntry(entryId) {
873
+ const raw = await readFile2(this.trashMetaPath(entryId), "utf8");
874
+ return JSON.parse(raw);
875
+ }
876
+ async mkdir(cloudDir) {
877
+ await mkdir2(this.abs(cloudDir), { recursive: true });
878
+ }
879
+ async listDir(cloudDir) {
880
+ let entries;
881
+ try {
882
+ entries = await readdir2(this.abs(cloudDir), { withFileTypes: true });
883
+ } catch {
884
+ return { dirs: [], bundles: [] };
885
+ }
886
+ const dir = normalizeCloudPath(cloudDir);
887
+ const dirs = [];
888
+ const bundles = [];
889
+ for (const e of entries) {
890
+ if (!e.isDirectory()) continue;
891
+ if (e.name === TRASH_DIR) continue;
892
+ if (e.name.endsWith(BUNDLE_SUFFIX)) {
893
+ bundles.push({ id: e.name.slice(0, -BUNDLE_SUFFIX.length), dir });
894
+ } else {
895
+ dirs.push(e.name);
896
+ }
897
+ }
898
+ return { dirs, bundles };
899
+ }
900
+ bundleStore(bundleId, cloudDir) {
901
+ return new LocalBundleStore(this.abs(cloudDir), bundleId);
902
+ }
903
+ async move(srcCloudPath, dstDir) {
904
+ const base = cloudBasename(normalizeCloudPath(srcCloudPath));
905
+ await mkdir2(this.abs(dstDir), { recursive: true });
906
+ await rename(this.abs(srcCloudPath), join3(this.abs(dstDir), base));
907
+ }
908
+ async copy(srcCloudPath, dstDir) {
909
+ const base = cloudBasename(normalizeCloudPath(srcCloudPath));
910
+ await mkdir2(this.abs(dstDir), { recursive: true });
911
+ await cp(this.abs(srcCloudPath), join3(this.abs(dstDir), base), { recursive: true });
912
+ }
913
+ async rename(srcCloudPath, newName) {
914
+ assertNameSegment(newName);
915
+ const absSrc = this.abs(srcCloudPath);
916
+ await rename(absSrc, join3(dirname2(absSrc), newName));
917
+ }
918
+ async trashPath(cloudPath, deletedAt) {
919
+ const originalPath = normalizeCloudPath(cloudPath);
920
+ const name = cloudBasename(originalPath);
921
+ const entryId = randomBytes3(8).toString("hex");
922
+ await mkdir2(this.trashItemDir(entryId), { recursive: true });
923
+ await rename(this.abs(originalPath), join3(this.trashItemDir(entryId), name));
924
+ const entry = { entryId, name, originalPath, deletedAt };
925
+ await writeFile2(this.trashMetaPath(entryId), JSON.stringify(entry), "utf8");
926
+ }
927
+ async listTrash() {
928
+ let files;
929
+ try {
930
+ files = await readdir2(this.trashRoot);
931
+ } catch {
932
+ return [];
933
+ }
934
+ const entries = [];
935
+ for (const f of files) {
936
+ if (!f.endsWith(".json")) continue;
937
+ entries.push(await this.readTrashEntry(f.slice(0, -".json".length)));
938
+ }
939
+ return entries;
940
+ }
941
+ async restoreTrash(entryId) {
942
+ const entry = await this.readTrashEntry(entryId);
943
+ const absTarget = this.abs(entry.originalPath);
944
+ await mkdir2(dirname2(absTarget), { recursive: true });
945
+ await rename(join3(this.trashItemDir(entryId), entry.name), absTarget);
946
+ await rm2(this.trashItemDir(entryId), { recursive: true, force: true });
947
+ await rm2(this.trashMetaPath(entryId), { force: true });
948
+ }
949
+ async deleteTrash(entryId) {
950
+ await rm2(this.trashItemDir(entryId), { recursive: true, force: true });
951
+ await rm2(this.trashMetaPath(entryId), { force: true });
952
+ }
953
+ async clearTrash() {
954
+ await rm2(this.trashRoot, { recursive: true, force: true });
955
+ }
956
+ };
957
+
958
+ // ../core/src/backup/index.ts
959
+ import { randomBytes as randomBytes4 } from "crypto";
960
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
961
+ import { join as join4 } from "path";
962
+ var BACKUPS_FILENAME = "backups.json";
963
+ var BACKUPS_VERSION = 1;
964
+ function backupsPath(keyRoot) {
965
+ return join4(keyRoot, BACKUPS_FILENAME);
966
+ }
967
+ async function readBackups(keyRoot) {
968
+ let raw;
969
+ try {
970
+ raw = await readFile3(backupsPath(keyRoot), "utf8");
971
+ } catch (err) {
972
+ if (err.code === "ENOENT") return [];
973
+ throw err;
974
+ }
975
+ try {
976
+ const f = JSON.parse(raw);
977
+ if (!Array.isArray(f.jobs)) return [];
978
+ return f.jobs.filter((j) => typeof j?.id === "string" && typeof j?.localDir === "string");
979
+ } catch {
980
+ return [];
981
+ }
982
+ }
983
+ async function writeBackups(keyRoot, jobs) {
984
+ await mkdir3(keyRoot, { recursive: true });
985
+ const p = backupsPath(keyRoot);
986
+ const tmp = `${p}.${process.pid}.${randomBytes4(4).toString("hex")}.tmp`;
987
+ await writeFile3(tmp, JSON.stringify({ version: BACKUPS_VERSION, jobs }, null, 2), "utf8");
988
+ await rename2(tmp, p);
989
+ }
990
+ async function addBackup(keyRoot, input) {
991
+ const jobs = await readBackups(keyRoot);
992
+ const existing = jobs.find(
993
+ (j) => j.localDir === input.localDir && (j.cloudDir ?? "") === (input.cloudDir ?? "")
994
+ );
995
+ if (existing) return existing;
996
+ const job = {
997
+ id: randomBytes4(4).toString("hex"),
998
+ localDir: input.localDir,
999
+ ...input.cloudDir ? { cloudDir: input.cloudDir } : {},
1000
+ addedAt: input.addedAt
1001
+ };
1002
+ await writeBackups(keyRoot, [...jobs, job]);
1003
+ return job;
1004
+ }
1005
+ async function removeBackup(keyRoot, id) {
1006
+ const jobs = await readBackups(keyRoot);
1007
+ const next = jobs.filter((j) => j.id !== id);
1008
+ if (next.length === jobs.length) return false;
1009
+ await writeBackups(keyRoot, next);
1010
+ return true;
1011
+ }
1012
+ async function updateLastBackup(keyRoot, id, whenISO) {
1013
+ const jobs = await readBackups(keyRoot);
1014
+ let changed = false;
1015
+ const next = jobs.map((j) => {
1016
+ if (j.id === id) {
1017
+ changed = true;
1018
+ return { ...j, lastBackupAt: whenISO };
1019
+ }
1020
+ return j;
1021
+ });
1022
+ if (changed) await writeBackups(keyRoot, next);
1023
+ }
1024
+
1025
+ // ../core/src/baidu/oauth.ts
1026
+ var OAUTH_BASE = "https://openapi.baidu.com/oauth/2.0";
1027
+ var DEFAULT_SCOPE = "basic,netdisk";
1028
+ function enc(params) {
1029
+ return Object.entries(params).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
1030
+ }
1031
+ async function asJson(res) {
1032
+ const data = await res.json();
1033
+ if (typeof data.error === "string") {
1034
+ throw new BizhouError(
1035
+ "OAUTH",
1036
+ `\u767E\u5EA6 OAuth \u9519\u8BEF\uFF1A${data.error} - ${String(data.error_description ?? "")}`
1037
+ );
1038
+ }
1039
+ return data;
1040
+ }
1041
+ function toTokenResponse(d) {
1042
+ if (typeof d.access_token !== "string") {
1043
+ throw new BizhouError("OAUTH", "OAuth \u54CD\u5E94\u7F3A\u5C11 access_token");
1044
+ }
1045
+ return {
1046
+ accessToken: d.access_token,
1047
+ refreshToken: typeof d.refresh_token === "string" ? d.refresh_token : void 0,
1048
+ expiresIn: typeof d.expires_in === "number" ? d.expires_in : 0,
1049
+ scope: typeof d.scope === "string" ? d.scope : void 0
1050
+ };
1051
+ }
1052
+ function buildAuthorizeUrl(config, redirectUri, opts = {}) {
1053
+ const params = {
1054
+ response_type: "code",
1055
+ client_id: config.appKey,
1056
+ redirect_uri: redirectUri,
1057
+ scope: opts.scope ?? DEFAULT_SCOPE
1058
+ };
1059
+ if (opts.state) params.state = opts.state;
1060
+ return `${OAUTH_BASE}/authorize?${enc(params)}`;
1061
+ }
1062
+ async function exchangeCodeForToken(config, code, redirectUri, fetchLike) {
1063
+ const url = `${OAUTH_BASE}/token?${enc({
1064
+ grant_type: "authorization_code",
1065
+ code,
1066
+ client_id: config.appKey,
1067
+ client_secret: config.secretKey,
1068
+ redirect_uri: redirectUri
1069
+ })}`;
1070
+ return toTokenResponse(await asJson(await fetchLike(url)));
1071
+ }
1072
+ async function startDeviceFlow(config, fetchLike, scope = DEFAULT_SCOPE) {
1073
+ const url = `${OAUTH_BASE}/device/code?${enc({
1074
+ response_type: "device_code",
1075
+ client_id: config.appKey,
1076
+ scope
1077
+ })}`;
1078
+ const d = await asJson(await fetchLike(url));
1079
+ if (typeof d.device_code !== "string" || typeof d.user_code !== "string") {
1080
+ throw new BizhouError("OAUTH", "\u8BBE\u5907\u7801\u54CD\u5E94\u7F3A\u5C11 device_code/user_code");
1081
+ }
1082
+ return {
1083
+ deviceCode: d.device_code,
1084
+ userCode: d.user_code,
1085
+ verificationUrl: String(d.verification_url ?? "https://openapi.baidu.com/device"),
1086
+ qrcodeUrl: typeof d.qrcode_url === "string" ? d.qrcode_url : void 0,
1087
+ interval: typeof d.interval === "number" ? d.interval : 5,
1088
+ expiresIn: typeof d.expires_in === "number" ? d.expires_in : 0
1089
+ };
1090
+ }
1091
+ async function pollDeviceToken(config, deviceCode, fetchLike) {
1092
+ const url = `${OAUTH_BASE}/token?${enc({
1093
+ grant_type: "device_token",
1094
+ code: deviceCode,
1095
+ client_id: config.appKey,
1096
+ client_secret: config.secretKey
1097
+ })}`;
1098
+ const raw = await (await fetchLike(url)).json();
1099
+ if (raw.error === "authorization_pending" || raw.error === "slow_down") {
1100
+ return null;
1101
+ }
1102
+ if (typeof raw.error === "string") {
1103
+ throw new BizhouError("OAUTH", `\u8BBE\u5907\u7801\u6388\u6743\u5931\u8D25\uFF1A${raw.error}`);
1104
+ }
1105
+ return toTokenResponse(raw);
1106
+ }
1107
+ async function refreshAccessToken(config, refreshToken, fetchLike) {
1108
+ const url = `${OAUTH_BASE}/token?${enc({
1109
+ grant_type: "refresh_token",
1110
+ refresh_token: refreshToken,
1111
+ client_id: config.appKey,
1112
+ client_secret: config.secretKey
1113
+ })}`;
1114
+ return toTokenResponse(await asJson(await fetchLike(url)));
1115
+ }
1116
+
1117
+ // ../core/src/cache/index.ts
1118
+ import { mkdir as mkdir4, readFile as readFile4, rename as rename3, rm as rm3, writeFile as writeFile4 } from "fs/promises";
1119
+ import { join as join5 } from "path";
1120
+ var CACHE_SUBDIR = join5(".cache", "manifests");
1121
+ function cacheFile(keyRoot, bundleId) {
1122
+ assertNameSegment(bundleId);
1123
+ return join5(keyRoot, CACHE_SUBDIR, `${bundleId}.json`);
1124
+ }
1125
+ async function getCachedManifest(keyRoot, bundleId) {
1126
+ try {
1127
+ return await readFile4(cacheFile(keyRoot, bundleId), "utf8");
1128
+ } catch (err) {
1129
+ if (err.code === "ENOENT") return null;
1130
+ throw err;
1131
+ }
1132
+ }
1133
+ async function putCachedManifest(keyRoot, bundleId, rawManifest) {
1134
+ const path = cacheFile(keyRoot, bundleId);
1135
+ await mkdir4(join5(keyRoot, CACHE_SUBDIR), { recursive: true });
1136
+ const tmp = `${path}.tmp`;
1137
+ await writeFile4(tmp, rawManifest, "utf8");
1138
+ await rename3(tmp, path);
1139
+ }
1140
+ async function invalidateManifest(keyRoot, bundleId) {
1141
+ await rm3(cacheFile(keyRoot, bundleId), { force: true });
1142
+ }
1143
+
1144
+ // ../core/src/chunker/index.ts
1145
+ import { createHash as createHash2 } from "crypto";
1146
+ import { open } from "fs/promises";
1147
+ import { gunzipSync, gzipSync } from "zlib";
1148
+ function sha256hex(b) {
1149
+ return createHash2("sha256").update(b).digest("hex");
1150
+ }
1151
+ async function encryptFileToChunks(opts) {
1152
+ if (opts.chunkSize <= 0) throw new BizhouError("INVALID_ARG", "chunkSize \u5FC5\u987B\u4E3A\u6B63");
1153
+ const totalChunks = Math.max(1, Math.ceil(opts.fileSize / opts.chunkSize));
1154
+ const skip = new Set(opts.skipExisting ?? []);
1155
+ const chunks = [];
1156
+ const fh = await open(opts.filePath, "r");
1157
+ try {
1158
+ const buf = Buffer.allocUnsafe(opts.chunkSize);
1159
+ let seq = 0;
1160
+ let position = 0;
1161
+ let bytesDone = 0;
1162
+ while (true) {
1163
+ const { bytesRead } = await fh.read(buf, 0, opts.chunkSize, position);
1164
+ if (bytesRead === 0 && seq > 0) break;
1165
+ const plain = buf.subarray(0, bytesRead);
1166
+ const payload = opts.compression === "gzip" ? gzipSync(plain) : plain;
1167
+ const aad = chunkAad(opts.bundleId, seq);
1168
+ const { iv, ciphertext, tag } = aeadEncrypt(
1169
+ opts.dek,
1170
+ payload,
1171
+ aad,
1172
+ deriveDeterministicIv(opts.dek, aad)
1173
+ );
1174
+ if (!skip.has(seq)) {
1175
+ await opts.store.putChunk(seq, ciphertext);
1176
+ }
1177
+ chunks.push({
1178
+ seq,
1179
+ file: chunkFileName(seq),
1180
+ plainSize: bytesRead,
1181
+ encSize: ciphertext.length,
1182
+ iv: iv.toString("base64"),
1183
+ tag: tag.toString("base64"),
1184
+ sha256: sha256hex(ciphertext)
1185
+ });
1186
+ bytesDone += bytesRead;
1187
+ position += bytesRead;
1188
+ opts.onProgress?.({
1189
+ phase: "encrypt",
1190
+ seq,
1191
+ totalChunks,
1192
+ bytesDone,
1193
+ bytesTotal: opts.fileSize
1194
+ });
1195
+ seq++;
1196
+ if (bytesRead < opts.chunkSize) break;
1197
+ }
1198
+ return chunks;
1199
+ } finally {
1200
+ await fh.close();
1201
+ }
1202
+ }
1203
+ async function decryptChunksToFile(opts) {
1204
+ const totalChunks = opts.chunks.length;
1205
+ const bytesTotal = opts.chunks.reduce((a, c2) => a + c2.plainSize, 0);
1206
+ const skip = new Set(opts.skip ?? []);
1207
+ const resuming = skip.size > 0;
1208
+ let bytesDone = 0;
1209
+ const fh = await open(opts.outPath, resuming ? "r+" : "w");
1210
+ try {
1211
+ let position = 0;
1212
+ for (const info2 of opts.chunks) {
1213
+ if (skip.has(info2.seq)) {
1214
+ position += info2.plainSize;
1215
+ bytesDone += info2.plainSize;
1216
+ opts.onProgress?.({ phase: "decrypt", seq: info2.seq, totalChunks, bytesDone, bytesTotal });
1217
+ continue;
1218
+ }
1219
+ const ct = await opts.store.getChunk(info2.seq);
1220
+ if (sha256hex(ct) !== info2.sha256) {
1221
+ throw new BizhouError("CHUNK", `\u5206\u7247 ${info2.seq} \u5BC6\u6587 sha256 \u6821\u9A8C\u5931\u8D25\uFF08\u6570\u636E\u635F\u574F\u6216\u88AB\u7BE1\u6539\uFF09`);
1222
+ }
1223
+ const iv = Buffer.from(info2.iv, "base64");
1224
+ const tag = Buffer.from(info2.tag, "base64");
1225
+ const payload = aeadDecrypt(opts.dek, iv, ct, tag, chunkAad(opts.bundleId, info2.seq));
1226
+ const plain = opts.compression === "gzip" ? gunzipSync(payload) : payload;
1227
+ if (plain.length !== info2.plainSize) {
1228
+ throw new BizhouError(
1229
+ "CHUNK",
1230
+ `\u5206\u7247 ${info2.seq} \u8FD8\u539F\u957F\u5EA6 ${plain.length} \u4E0E manifest plainSize ${info2.plainSize} \u4E0D\u7B26`
1231
+ );
1232
+ }
1233
+ await fh.write(plain, 0, plain.length, position);
1234
+ position += plain.length;
1235
+ bytesDone += plain.length;
1236
+ opts.onProgress?.({ phase: "decrypt", seq: info2.seq, totalChunks, bytesDone, bytesTotal });
1237
+ }
1238
+ await fh.truncate(position);
1239
+ return { bytesWritten: bytesDone };
1240
+ } finally {
1241
+ await fh.close();
1242
+ }
1243
+ }
1244
+
1245
+ // ../core/src/config/index.ts
1246
+ import { join as join6 } from "path";
1247
+ var VAULT_FILENAME = "vault.json";
1248
+ var SECRETS_FILENAME = "secrets.enc";
1249
+ var DEVICE_KEY_FILENAME = "device.key";
1250
+ var CONFIG_FILENAME = "config.json";
1251
+ function resolveKeyRoot(env, platform) {
1252
+ if (env.BIZHOU_HOME) return env.BIZHOU_HOME;
1253
+ if (env.BIZHOU_CONFIG_DIR) return env.BIZHOU_CONFIG_DIR;
1254
+ const home = env.HOME ?? env.USERPROFILE ?? ".";
1255
+ return join6(home, ".bizhou");
1256
+ }
1257
+ function defaultDownloadsDir(env, platform) {
1258
+ const home = platform === "win32" ? env.USERPROFILE ?? env.HOME ?? "." : env.HOME ?? ".";
1259
+ return join6(home, "Downloads");
1260
+ }
1261
+ function resolveFileRoot(env, platform, configFileRoot) {
1262
+ if (env.BIZHOU_FILE_ROOT) return env.BIZHOU_FILE_ROOT;
1263
+ if (configFileRoot) return configFileRoot;
1264
+ return defaultDownloadsDir(env, platform);
1265
+ }
1266
+ function configPaths(env, platform) {
1267
+ const dir = resolveKeyRoot(env, platform);
1268
+ return {
1269
+ dir,
1270
+ vault: join6(dir, VAULT_FILENAME),
1271
+ secrets: join6(dir, SECRETS_FILENAME),
1272
+ deviceKey: join6(dir, DEVICE_KEY_FILENAME),
1273
+ config: join6(dir, CONFIG_FILENAME)
1274
+ };
1275
+ }
1276
+
1277
+ // ../core/src/content/index.ts
1278
+ import { createHmac as createHmac2, hkdfSync } from "crypto";
1279
+ import { open as open2 } from "fs/promises";
1280
+ var CONTENT_ID_INFO = Buffer.from("bizhou-content-id", "utf8");
1281
+ var READ_BUF_BYTES = 1024 * 1024;
1282
+ function deriveContentKey(mk) {
1283
+ return Buffer.from(hkdfSync("sha256", mk, Buffer.alloc(0), CONTENT_ID_INFO, 32));
1284
+ }
1285
+ async function hashPlaintextFile(filePath, contentKey) {
1286
+ const hmac = createHmac2("sha256", contentKey);
1287
+ const fh = await open2(filePath, "r");
1288
+ try {
1289
+ const buf = Buffer.allocUnsafe(READ_BUF_BYTES);
1290
+ let position = 0;
1291
+ while (true) {
1292
+ const { bytesRead } = await fh.read(buf, 0, READ_BUF_BYTES, position);
1293
+ if (bytesRead === 0) break;
1294
+ hmac.update(buf.subarray(0, bytesRead));
1295
+ position += bytesRead;
1296
+ }
1297
+ } finally {
1298
+ await fh.close();
1299
+ }
1300
+ return hmac.digest("hex");
1301
+ }
1302
+
1303
+ // ../core/src/crypto/base32.ts
1304
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
1305
+ var CHAR_TO_VAL = {};
1306
+ for (let i = 0; i < ALPHABET.length; i++) CHAR_TO_VAL[ALPHABET[i]] = i;
1307
+ function base32Encode(data) {
1308
+ let bits = 0;
1309
+ let value = 0;
1310
+ let out2 = "";
1311
+ for (const byte of data) {
1312
+ value = value << 8 | byte;
1313
+ bits += 8;
1314
+ while (bits >= 5) {
1315
+ out2 += ALPHABET[value >>> bits - 5 & 31];
1316
+ bits -= 5;
1317
+ }
1318
+ }
1319
+ if (bits > 0) {
1320
+ out2 += ALPHABET[value << 5 - bits & 31];
1321
+ }
1322
+ return out2;
1323
+ }
1324
+ function base32Decode(input) {
1325
+ const clean = input.replace(/[-\s]/g, "").toUpperCase();
1326
+ let bits = 0;
1327
+ let value = 0;
1328
+ const bytes = [];
1329
+ for (const ch of clean) {
1330
+ const v = CHAR_TO_VAL[ch];
1331
+ if (v === void 0) {
1332
+ throw new InvalidArgError(`base32 \u542B\u975E\u6CD5\u5B57\u7B26\uFF1A'${ch}'`);
1333
+ }
1334
+ value = value << 5 | v;
1335
+ bits += 5;
1336
+ if (bits >= 8) {
1337
+ bytes.push(value >>> bits - 8 & 255);
1338
+ bits -= 8;
1339
+ }
1340
+ }
1341
+ return Buffer.from(bytes);
1342
+ }
1343
+ function groupBase32(s, groupSize = 4) {
1344
+ const groups = [];
1345
+ for (let i = 0; i < s.length; i += groupSize) {
1346
+ groups.push(s.slice(i, i + groupSize));
1347
+ }
1348
+ return groups.join("-");
1349
+ }
1350
+
1351
+ // ../core/src/journal/index.ts
1352
+ import { createHash as createHash3 } from "crypto";
1353
+ import { mkdir as mkdir5, readFile as readFile5, rename as rename4, rm as rm4, writeFile as writeFile5 } from "fs/promises";
1354
+ import { dirname as dirname3, join as join7 } from "path";
1355
+ var KIND_DIR = { upload: ".uploads", download: ".downloads" };
1356
+ function journalPath(keyRoot, kind, contentId, destKey) {
1357
+ const destHash = createHash3("sha256").update(destKey).digest("hex").slice(0, 16);
1358
+ return join7(keyRoot, KIND_DIR[kind], `${contentId}@${destHash}.json`);
1359
+ }
1360
+ function isValidJournalEntry(e) {
1361
+ if (typeof e !== "object" || e === null) return false;
1362
+ const o = e;
1363
+ const common = typeof o.bundleId === "string" && typeof o.cloudDir === "string" && typeof o.contentId === "string" && Array.isArray(o.doneChunks) && o.doneChunks.every((n) => typeof n === "number") && typeof o.totalChunks === "number" && typeof o.startedAt === "string" && typeof o.pid === "number";
1364
+ if (!common) return false;
1365
+ if (o.wrappedKey !== void 0 && typeof o.wrappedKey !== "string") return false;
1366
+ if (o.chunkSize !== void 0 && typeof o.chunkSize !== "number") return false;
1367
+ if (o.compression !== void 0 && o.compression !== "none" && o.compression !== "gzip")
1368
+ return false;
1369
+ return true;
1370
+ }
1371
+ async function readJournal(path) {
1372
+ try {
1373
+ const raw = await readFile5(path, "utf8");
1374
+ const e = JSON.parse(raw);
1375
+ if (!isValidJournalEntry(e)) return null;
1376
+ return e;
1377
+ } catch {
1378
+ return null;
1379
+ }
1380
+ }
1381
+ async function writeJournal(path, entry) {
1382
+ await mkdir5(dirname3(path), { recursive: true });
1383
+ const tmp = `${path}.tmp`;
1384
+ await writeFile5(tmp, JSON.stringify(entry), "utf8");
1385
+ await rename4(tmp, path);
1386
+ }
1387
+ async function appendDoneChunk(path, seq) {
1388
+ const e = await readJournal(path);
1389
+ if (!e) return;
1390
+ if (!e.doneChunks.includes(seq)) {
1391
+ e.doneChunks.push(seq);
1392
+ e.doneChunks.sort((a, b) => a - b);
1393
+ await writeJournal(path, e);
1394
+ }
1395
+ }
1396
+ async function removeJournal(path) {
1397
+ await rm4(path, { force: true });
1398
+ }
1399
+ function isLockAlive(entry, opts) {
1400
+ if (opts.pidAlive) return true;
1401
+ return opts.now - Date.parse(entry.startedAt) < opts.ttlMs;
1402
+ }
1403
+
1404
+ // ../core/src/keystore/index.ts
1405
+ import { randomBytes as randomBytes5 } from "crypto";
1406
+ import { chmod, mkdir as mkdir6, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
1407
+ var FileSecretStore = class {
1408
+ constructor(dir, secretsPath, deviceKeyPath) {
1409
+ this.dir = dir;
1410
+ this.secretsPath = secretsPath;
1411
+ this.deviceKeyPath = deviceKeyPath;
1412
+ }
1413
+ dir;
1414
+ secretsPath;
1415
+ deviceKeyPath;
1416
+ async ensureDir() {
1417
+ await mkdir6(this.dir, { recursive: true });
1418
+ }
1419
+ async deviceKey() {
1420
+ try {
1421
+ const b64 = await readFile6(this.deviceKeyPath, "utf8");
1422
+ return Buffer.from(b64.trim(), "base64");
1423
+ } catch {
1424
+ await this.ensureDir();
1425
+ const key = randomBytes5(32);
1426
+ await writeFile6(this.deviceKeyPath, key.toString("base64"), { mode: 384 });
1427
+ await chmod(this.deviceKeyPath, 384).catch(() => {
1428
+ });
1429
+ return key;
1430
+ }
1431
+ }
1432
+ async load() {
1433
+ let enc2;
1434
+ try {
1435
+ enc2 = await readFile6(this.secretsPath, "utf8");
1436
+ } catch {
1437
+ return {};
1438
+ }
1439
+ const key = await this.deviceKey();
1440
+ const json = openFromBase64(key, enc2.trim()).toString("utf8");
1441
+ return JSON.parse(json);
1442
+ }
1443
+ async save(data) {
1444
+ await this.ensureDir();
1445
+ const key = await this.deviceKey();
1446
+ const blob = sealToBase64(key, Buffer.from(JSON.stringify(data), "utf8"));
1447
+ await writeFile6(this.secretsPath, blob, { mode: 384 });
1448
+ await chmod(this.secretsPath, 384).catch(() => {
1449
+ });
1450
+ }
1451
+ async getAll() {
1452
+ return this.load();
1453
+ }
1454
+ async get(key) {
1455
+ return (await this.load())[key];
1456
+ }
1457
+ async set(key, value) {
1458
+ const data = await this.load();
1459
+ data[key] = value;
1460
+ await this.save(data);
1461
+ }
1462
+ async delete(key) {
1463
+ const data = await this.load();
1464
+ delete data[key];
1465
+ await this.save(data);
1466
+ }
1467
+ };
1468
+
1469
+ // ../core/src/resource/index.ts
1470
+ import { basename as basename2 } from "path";
1471
+
1472
+ // ../core/src/vault/index.ts
1473
+ var MK_CHECK_MARKER = Buffer.from("bizhou-vault-check-v1", "utf8");
1474
+ var VAULT_VERSION = 1;
1475
+ function makeMkCheck(mk) {
1476
+ return sealToBase64(mk, MK_CHECK_MARKER);
1477
+ }
1478
+ function verifyMk(vault, mk) {
1479
+ let opened;
1480
+ try {
1481
+ opened = openFromBase64(mk, vault.mkCheck);
1482
+ } catch (cause) {
1483
+ throw new VaultError("vault \u6821\u9A8C\u5931\u8D25\uFF1A\u89E3\u9501\u5F97\u5230\u7684\u4E3B\u5BC6\u94A5\u65E0\u6CD5\u9A8C\u8BC1\uFF08vault \u6587\u4EF6\u53EF\u80FD\u635F\u574F\uFF09", {
1484
+ cause
1485
+ });
1486
+ }
1487
+ if (!opened.equals(MK_CHECK_MARKER)) {
1488
+ throw new VaultError("vault \u6821\u9A8C\u5931\u8D25\uFF1A\u4E3B\u5BC6\u94A5\u6807\u8BB0\u4E0D\u5339\u914D");
1489
+ }
1490
+ }
1491
+ async function createVault(masterPassword, opts = { createdAt: "" }) {
1492
+ if (masterPassword.length === 0) {
1493
+ throw new VaultError("\u4E3B\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A");
1494
+ }
1495
+ const params = opts.params ?? DEFAULT_SCRYPT;
1496
+ const mk = generateKey();
1497
+ const pwSalt = generateSalt();
1498
+ const kekPw = await deriveKey(masterPassword, pwSalt, params);
1499
+ const recoveryRaw = generateKey();
1500
+ const recoveryKey = groupBase32(base32Encode(recoveryRaw));
1501
+ const vault = {
1502
+ version: VAULT_VERSION,
1503
+ kdf: params,
1504
+ pwSalt: pwSalt.toString("base64"),
1505
+ wrappedMkByPassword: wrapKey(kekPw, mk),
1506
+ wrappedMkByRecovery: wrapKey(recoveryRaw, mk),
1507
+ mkCheck: makeMkCheck(mk),
1508
+ createdAt: opts.createdAt
1509
+ };
1510
+ return { vault, recoveryKey };
1511
+ }
1512
+ async function unlockWithPassword(vault, masterPassword) {
1513
+ const pwSalt = Buffer.from(vault.pwSalt, "base64");
1514
+ const kekPw = await deriveKey(masterPassword, pwSalt, vault.kdf);
1515
+ let mk;
1516
+ try {
1517
+ mk = unwrapKey(kekPw, vault.wrappedMkByPassword);
1518
+ } catch (cause) {
1519
+ throw new AuthError("\u4E3B\u5BC6\u7801\u9519\u8BEF\uFF1A\u65E0\u6CD5\u89E3\u5F00\u4E3B\u5BC6\u94A5", { cause });
1520
+ }
1521
+ verifyMk(vault, mk);
1522
+ return mk;
1523
+ }
1524
+ async function unlockWithRecovery(vault, recoveryKey) {
1525
+ const raw = base32Decode(recoveryKey);
1526
+ if (raw.length !== KEY_BYTES) {
1527
+ throw new AuthError(`\u6062\u590D\u5BC6\u94A5\u957F\u5EA6\u4E0D\u6B63\u786E\uFF08\u5E94\u89E3\u51FA ${KEY_BYTES} \u5B57\u8282\uFF0C\u5B9E\u9645 ${raw.length}\uFF09`);
1528
+ }
1529
+ let mk;
1530
+ try {
1531
+ mk = unwrapKey(raw, vault.wrappedMkByRecovery);
1532
+ } catch (cause) {
1533
+ throw new AuthError("\u6062\u590D\u5BC6\u94A5\u9519\u8BEF\uFF1A\u65E0\u6CD5\u89E3\u5F00\u4E3B\u5BC6\u94A5", { cause });
1534
+ }
1535
+ verifyMk(vault, mk);
1536
+ return mk;
1537
+ }
1538
+ async function changePassword(vault, currentPassword, newPassword, params = DEFAULT_SCRYPT) {
1539
+ if (newPassword.length === 0) throw new VaultError("\u65B0\u4E3B\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A");
1540
+ const mk = await unlockWithPassword(vault, currentPassword);
1541
+ const pwSalt = generateSalt();
1542
+ const kekPw = await deriveKey(newPassword, pwSalt, params);
1543
+ return {
1544
+ ...vault,
1545
+ kdf: params,
1546
+ pwSalt: pwSalt.toString("base64"),
1547
+ wrappedMkByPassword: wrapKey(kekPw, mk)
1548
+ };
1549
+ }
1550
+ function wrapDek(mk, dek) {
1551
+ return wrapKey(mk, dek);
1552
+ }
1553
+ function unwrapDek(mk, wrapped) {
1554
+ return unwrapKey(mk, wrapped);
1555
+ }
1556
+
1557
+ // ../core/src/resource/index.ts
1558
+ var DEFAULT_CHUNK_SIZE = 100 * 1024 * 1024;
1559
+ async function packResource(opts) {
1560
+ const chunkSize = opts.chunkSize ?? DEFAULT_CHUNK_SIZE;
1561
+ const compression = opts.compression ?? "none";
1562
+ const dek = opts.dek ?? generateKey();
1563
+ const chunks = await encryptFileToChunks({
1564
+ filePath: opts.filePath,
1565
+ fileSize: opts.fileSize,
1566
+ dek,
1567
+ bundleId: opts.bundleId,
1568
+ chunkSize,
1569
+ compression,
1570
+ store: opts.store,
1571
+ onProgress: opts.onProgress,
1572
+ skipExisting: opts.skipExisting
1573
+ });
1574
+ const meta = {
1575
+ name: opts.name ?? basename2(opts.filePath),
1576
+ size: opts.fileSize,
1577
+ ...opts.mtime ? { mtime: opts.mtime } : {},
1578
+ ...opts.contentType ? { contentType: opts.contentType } : {},
1579
+ ...opts.contentId ? { contentId: opts.contentId } : {}
1580
+ };
1581
+ let previewInfo;
1582
+ if (opts.preview) {
1583
+ const { iv, ciphertext, tag } = aeadEncrypt(dek, opts.preview.data);
1584
+ await opts.store.putPreview(ciphertext);
1585
+ previewInfo = {
1586
+ file: PREVIEW_FILENAME,
1587
+ kind: opts.preview.kind,
1588
+ iv: iv.toString("base64"),
1589
+ tag: tag.toString("base64")
1590
+ };
1591
+ }
1592
+ const manifest = {
1593
+ version: 1,
1594
+ bundleId: opts.bundleId,
1595
+ createdAt: opts.createdAt,
1596
+ cipher: "AES-256-GCM",
1597
+ compression,
1598
+ chunkSize,
1599
+ wrappedKey: wrapDek(opts.mk, dek),
1600
+ chunks,
1601
+ ...previewInfo ? { preview: previewInfo } : {},
1602
+ encMeta: sealMeta(dek, meta)
1603
+ };
1604
+ await opts.store.putManifest(serializeManifest(manifest));
1605
+ return manifest;
1606
+ }
1607
+ async function openPreview(mk, store) {
1608
+ const manifest = parseManifest(await store.getManifest());
1609
+ if (!manifest.preview) {
1610
+ throw new BizhouError("BUNDLE", "\u8BE5\u8D44\u6E90\u6CA1\u6709\u9884\u89C8\u5305");
1611
+ }
1612
+ const dek = unwrapDek(mk, manifest.wrappedKey);
1613
+ const ct = await store.getPreview();
1614
+ const data = aeadDecrypt(
1615
+ dek,
1616
+ Buffer.from(manifest.preview.iv, "base64"),
1617
+ ct,
1618
+ Buffer.from(manifest.preview.tag, "base64")
1619
+ );
1620
+ return { kind: manifest.preview.kind, data };
1621
+ }
1622
+ async function unpackResource(opts) {
1623
+ const manifest = parseManifest(await opts.store.getManifest());
1624
+ const dek = unwrapDek(opts.mk, manifest.wrappedKey);
1625
+ const meta = openMeta(dek, manifest.encMeta);
1626
+ const { bytesWritten } = await decryptChunksToFile({
1627
+ chunks: manifest.chunks,
1628
+ dek,
1629
+ bundleId: manifest.bundleId,
1630
+ compression: manifest.compression,
1631
+ store: opts.store,
1632
+ outPath: opts.outPath,
1633
+ skip: opts.skip,
1634
+ onProgress: opts.onProgress
1635
+ });
1636
+ return { manifest, meta, bytesWritten };
1637
+ }
1638
+ async function readResourceMeta(mk, store) {
1639
+ const manifest = parseManifest(await store.getManifest());
1640
+ const dek = unwrapDek(mk, manifest.wrappedKey);
1641
+ return { manifest, meta: openMeta(dek, manifest.encMeta) };
1642
+ }
1643
+ async function renameResource(mk, store, newName) {
1644
+ const manifest = parseManifest(await store.getManifest());
1645
+ const dek = unwrapDek(mk, manifest.wrappedKey);
1646
+ const meta = openMeta(dek, manifest.encMeta);
1647
+ const manifest2 = {
1648
+ ...manifest,
1649
+ encMeta: sealMeta(dek, { ...meta, name: newName })
1650
+ };
1651
+ await store.putManifest(serializeManifest(manifest2));
1652
+ }
1653
+
1654
+ // src/commands.ts
1655
+ import { mkdir as mkdir8, mkdtemp as mkdtemp2, readdir as readdir4, rename as rename5, rm as rm6, stat, writeFile as writeFile8 } from "fs/promises";
1656
+ import { createServer } from "http";
1657
+ import { tmpdir as tmpdir2 } from "os";
1658
+ import { basename as basename3, dirname as dirname4, join as join10, relative as relative2, resolve as resolve3, sep } from "path";
1659
+
1660
+ // src/export7z.ts
1661
+ import { spawn } from "child_process";
1662
+ import { access } from "fs/promises";
1663
+ import { resolve } from "path";
1664
+ var CANDIDATES = ["7z", "7za", "7zz"];
1665
+ async function onPath(bin) {
1666
+ const dirs = (process.env.PATH ?? "").split(":");
1667
+ for (const d of dirs) {
1668
+ try {
1669
+ await access(`${d}/${bin}`);
1670
+ return true;
1671
+ } catch {
1672
+ }
1673
+ }
1674
+ return false;
1675
+ }
1676
+ async function findSevenZip() {
1677
+ if (process.env.BIZHOU_7Z_BIN) return process.env.BIZHOU_7Z_BIN;
1678
+ for (const c2 of CANDIDATES) {
1679
+ if (await onPath(c2)) return c2;
1680
+ }
1681
+ return null;
1682
+ }
1683
+ function sevenZipArchive(bin, outArchive, inputPaths, password) {
1684
+ const outAbs = resolve(outArchive);
1685
+ const inAbs = inputPaths.map((p) => resolve(p));
1686
+ return new Promise((resolvePromise, reject) => {
1687
+ const args = ["a", "-t7z", "-mhe=on", `-p${password}`, "--", outAbs, ...inAbs];
1688
+ const p = spawn(bin, args, { stdio: "ignore" });
1689
+ p.on("error", reject);
1690
+ p.on(
1691
+ "close",
1692
+ (code) => code === 0 ? resolvePromise() : reject(new BizhouError("IO", `7z \u6253\u5305\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${code}\uFF09`))
1693
+ );
1694
+ });
1695
+ }
1696
+
1697
+ // src/preview.ts
1698
+ import { spawn as spawn2 } from "child_process";
1699
+ import { createReadStream } from "fs";
1700
+ import { mkdtemp, open as open3, readdir as readdir3, readFile as readFile7, rm as rm5 } from "fs/promises";
1701
+ import { tmpdir } from "os";
1702
+ import { extname, join as join8, resolve as resolve2 } from "path";
1703
+ import { createGunzip } from "zlib";
1704
+ var IMAGE_EXT = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff", ".heic"]);
1705
+ var VIDEO_EXT = /* @__PURE__ */ new Set([".mp4", ".mov", ".mkv", ".avi", ".webm", ".flv", ".m4v", ".wmv"]);
1706
+ var AUDIO_EXT = /* @__PURE__ */ new Set([".mp3", ".flac", ".wav", ".aac", ".ogg", ".m4a", ".wma"]);
1707
+ var TEXT_EXT = /* @__PURE__ */ new Set([
1708
+ ".txt",
1709
+ ".md",
1710
+ ".markdown",
1711
+ ".json",
1712
+ ".jsonc",
1713
+ ".csv",
1714
+ ".tsv",
1715
+ ".log",
1716
+ ".yaml",
1717
+ ".yml",
1718
+ ".xml",
1719
+ ".ini",
1720
+ ".toml",
1721
+ ".cfg",
1722
+ ".conf",
1723
+ ".env",
1724
+ ".properties",
1725
+ ".ts",
1726
+ ".tsx",
1727
+ ".js",
1728
+ ".jsx",
1729
+ ".mjs",
1730
+ ".cjs",
1731
+ ".py",
1732
+ ".go",
1733
+ ".rs",
1734
+ ".java",
1735
+ ".kt",
1736
+ ".c",
1737
+ ".h",
1738
+ ".cpp",
1739
+ ".hpp",
1740
+ ".cc",
1741
+ ".cs",
1742
+ ".rb",
1743
+ ".php",
1744
+ ".swift",
1745
+ ".scala",
1746
+ ".sh",
1747
+ ".bash",
1748
+ ".zsh",
1749
+ ".ps1",
1750
+ ".sql",
1751
+ ".html",
1752
+ ".htm",
1753
+ ".css",
1754
+ ".scss",
1755
+ ".less",
1756
+ ".vue",
1757
+ ".svelte"
1758
+ ]);
1759
+ function detectStrategy(path) {
1760
+ const lower = path.toLowerCase();
1761
+ const ext = extname(lower);
1762
+ if (IMAGE_EXT.has(ext)) return "image";
1763
+ if (VIDEO_EXT.has(ext)) return "video";
1764
+ if (AUDIO_EXT.has(ext)) return "audio";
1765
+ if (ext === ".pdf") return "pdf";
1766
+ if (ext === ".zip" || ext === ".tar" || ext === ".tgz" || lower.endsWith(".tar.gz")) {
1767
+ return "archive";
1768
+ }
1769
+ if (TEXT_EXT.has(ext)) return "text";
1770
+ return null;
1771
+ }
1772
+ var TEXT_PREVIEW_BYTES = 32 * 1024;
1773
+ function trimToUtf8Boundary(buf) {
1774
+ if (buf.length === 0) return buf;
1775
+ let i = buf.length - 1;
1776
+ let cont = 0;
1777
+ while (i >= 0 && (buf[i] & 192) === 128 && cont < 3) {
1778
+ i--;
1779
+ cont++;
1780
+ }
1781
+ if (i < 0) return buf;
1782
+ const lead = buf[i];
1783
+ let need;
1784
+ if ((lead & 128) === 0) need = 1;
1785
+ else if ((lead & 224) === 192) need = 2;
1786
+ else if ((lead & 240) === 224) need = 3;
1787
+ else if ((lead & 248) === 240) need = 4;
1788
+ else return buf;
1789
+ const have = buf.length - i;
1790
+ return have >= need ? buf : buf.subarray(0, i);
1791
+ }
1792
+ async function genText(path) {
1793
+ let fh;
1794
+ try {
1795
+ fh = await open3(path, "r");
1796
+ const buf = Buffer.allocUnsafe(TEXT_PREVIEW_BYTES);
1797
+ const { bytesRead } = await fh.read(buf, 0, TEXT_PREVIEW_BYTES, 0);
1798
+ return { kind: "text", data: trimToUtf8Boundary(buf.subarray(0, bytesRead)) };
1799
+ } catch {
1800
+ return null;
1801
+ } finally {
1802
+ await fh?.close();
1803
+ }
1804
+ }
1805
+ function ffmpegBin() {
1806
+ return process.env.BIZHOU_FFMPEG_BIN ?? "ffmpeg";
1807
+ }
1808
+ function runFfmpeg(args) {
1809
+ return new Promise((resolve5, reject) => {
1810
+ const p = spawn2(ffmpegBin(), ["-y", "-loglevel", "error", ...args], { stdio: "ignore" });
1811
+ p.on("error", reject);
1812
+ p.on("close", (code) => code === 0 ? resolve5() : reject(new Error(`ffmpeg \u9000\u51FA\u7801 ${code}`)));
1813
+ });
1814
+ }
1815
+ async function genFfmpeg(src, kind) {
1816
+ let work;
1817
+ try {
1818
+ work = await mkdtemp(join8(tmpdir(), "bizhou-prev-"));
1819
+ if (kind === "image" || kind === "video") {
1820
+ const out3 = join8(work, "thumb.jpg");
1821
+ const vf = "scale=320:-1";
1822
+ const args = kind === "video" ? ["-i", src, "-ss", "00:00:01", "-vframes", "1", "-vf", vf, out3] : ["-i", src, "-vf", vf, out3];
1823
+ await runFfmpeg(args);
1824
+ return { kind, data: await readFile7(out3) };
1825
+ }
1826
+ const out2 = join8(work, "clip.mp3");
1827
+ await runFfmpeg(["-i", src, "-t", "15", "-b:a", "64k", out2]);
1828
+ return { kind, data: await readFile7(out2) };
1829
+ } catch {
1830
+ return null;
1831
+ } finally {
1832
+ if (work) await rm5(work, { recursive: true, force: true });
1833
+ }
1834
+ }
1835
+ var ARCHIVE_MAX_ENTRIES = 500;
1836
+ var TarLister = class {
1837
+ names = [];
1838
+ buf = Buffer.alloc(0);
1839
+ skip = 0;
1840
+ // 待丢弃的数据字节
1841
+ done = false;
1842
+ feed(chunk) {
1843
+ if (this.done) return;
1844
+ let data = chunk;
1845
+ if (this.skip > 0) {
1846
+ const drop = Math.min(this.skip, data.length);
1847
+ this.skip -= drop;
1848
+ data = data.subarray(drop);
1849
+ if (data.length === 0) return;
1850
+ }
1851
+ this.buf = this.buf.length ? Buffer.concat([this.buf, data]) : data;
1852
+ while (this.buf.length >= 512) {
1853
+ const block = this.buf.subarray(0, 512);
1854
+ if (block.every((b) => b === 0)) {
1855
+ this.done = true;
1856
+ return;
1857
+ }
1858
+ const raw = block.subarray(0, 100);
1859
+ const nul = raw.indexOf(0);
1860
+ const name = raw.toString("utf8", 0, nul === -1 ? 100 : nul);
1861
+ const sizeStr = block.toString("ascii", 124, 136).replace(/\0.*$/, "").trim();
1862
+ const size = Math.max(0, Number.parseInt(sizeStr, 8) || 0);
1863
+ if (name) this.names.push(name);
1864
+ if (this.names.length >= ARCHIVE_MAX_ENTRIES) {
1865
+ this.done = true;
1866
+ return;
1867
+ }
1868
+ const dataLen = Math.ceil(size / 512) * 512;
1869
+ this.buf = this.buf.subarray(512);
1870
+ const inBuf = Math.min(dataLen, this.buf.length);
1871
+ this.buf = this.buf.subarray(inBuf);
1872
+ this.skip = dataLen - inBuf;
1873
+ if (this.skip > 0) return;
1874
+ }
1875
+ }
1876
+ result() {
1877
+ return this.names;
1878
+ }
1879
+ };
1880
+ function listTar(buf) {
1881
+ const l = new TarLister();
1882
+ l.feed(buf);
1883
+ return l.result();
1884
+ }
1885
+ function listZip(buf) {
1886
+ const EOCD = 101010256;
1887
+ let p = buf.length - 22;
1888
+ const min = Math.max(0, buf.length - 22 - 65535);
1889
+ for (; p >= min; p--) {
1890
+ if (p + 4 <= buf.length && buf.readUInt32LE(p) === EOCD) break;
1891
+ }
1892
+ if (p < min) throw new Error("no EOCD");
1893
+ const count = buf.readUInt16LE(p + 10);
1894
+ let off = buf.readUInt32LE(p + 16);
1895
+ const names = [];
1896
+ for (let i = 0; i < count && i < ARCHIVE_MAX_ENTRIES; i++) {
1897
+ if (off + 46 > buf.length || buf.readUInt32LE(off) !== 33639248) break;
1898
+ const nameLen = buf.readUInt16LE(off + 28);
1899
+ const extraLen = buf.readUInt16LE(off + 30);
1900
+ const commentLen = buf.readUInt16LE(off + 32);
1901
+ names.push(buf.toString("utf8", off + 46, off + 46 + nameLen));
1902
+ off += 46 + nameLen + extraLen + commentLen;
1903
+ }
1904
+ return names;
1905
+ }
1906
+ var ARCHIVE_MAX_SCAN_BYTES = 64 * 1024 * 1024;
1907
+ function listTarGz(path) {
1908
+ return new Promise((resolve5, reject) => {
1909
+ const lister = new TarLister();
1910
+ const gunzip = createGunzip();
1911
+ const rs = createReadStream(path);
1912
+ let totalDecompressed = 0;
1913
+ const finish = () => {
1914
+ rs.destroy();
1915
+ gunzip.destroy();
1916
+ resolve5(lister.result());
1917
+ };
1918
+ gunzip.on("data", (c2) => {
1919
+ totalDecompressed += c2.length;
1920
+ try {
1921
+ lister.feed(c2);
1922
+ } catch {
1923
+ finish();
1924
+ return;
1925
+ }
1926
+ if (lister.done || totalDecompressed > ARCHIVE_MAX_SCAN_BYTES) finish();
1927
+ });
1928
+ gunzip.on("end", () => resolve5(lister.result()));
1929
+ gunzip.on("error", reject);
1930
+ rs.on("error", reject);
1931
+ rs.pipe(gunzip);
1932
+ });
1933
+ }
1934
+ async function genArchive(path) {
1935
+ const lower = path.toLowerCase();
1936
+ const ext = extname(lower);
1937
+ try {
1938
+ let names;
1939
+ if (ext === ".zip") names = listZip(await readFile7(path));
1940
+ else if (ext === ".tgz" || lower.endsWith(".tar.gz")) names = await listTarGz(path);
1941
+ else if (ext === ".tar") names = listTar(await readFile7(path));
1942
+ else return null;
1943
+ if (names.length === 0) return null;
1944
+ let text = names.join("\n");
1945
+ if (names.length >= ARCHIVE_MAX_ENTRIES) text += `
1946
+ \u2026\uFF08\u4EC5\u5217\u524D ${ARCHIVE_MAX_ENTRIES} \u6761\uFF09`;
1947
+ return { kind: "text", data: Buffer.from(text, "utf8") };
1948
+ } catch {
1949
+ return null;
1950
+ }
1951
+ }
1952
+ function pdftoppmBin() {
1953
+ return process.env.BIZHOU_PDFTOPPM_BIN ?? "pdftoppm";
1954
+ }
1955
+ function runPdftoppm(args) {
1956
+ return new Promise((resolvePromise, reject) => {
1957
+ const p = spawn2(pdftoppmBin(), args, { stdio: "ignore" });
1958
+ p.on("error", reject);
1959
+ p.on(
1960
+ "close",
1961
+ (code) => code === 0 ? resolvePromise() : reject(new Error(`pdftoppm \u9000\u51FA\u7801 ${code}`))
1962
+ );
1963
+ });
1964
+ }
1965
+ async function genPdf(path) {
1966
+ const src = resolve2(path);
1967
+ let work;
1968
+ try {
1969
+ work = await mkdtemp(join8(tmpdir(), "bizhou-pdf-"));
1970
+ const prefix = join8(work, "pg");
1971
+ await runPdftoppm(["-jpeg", "-f", "1", "-l", "1", "-scale-to", "320", src, prefix]);
1972
+ const files = (await readdir3(work)).filter((f) => f.toLowerCase().endsWith(".jpg")).sort();
1973
+ if (files.length === 0) return null;
1974
+ return { kind: "image", data: await readFile7(join8(work, files[0])) };
1975
+ } catch {
1976
+ return null;
1977
+ } finally {
1978
+ if (work) await rm5(work, { recursive: true, force: true });
1979
+ }
1980
+ }
1981
+ async function generatePreview(sourcePath) {
1982
+ const strat = detectStrategy(sourcePath);
1983
+ if (!strat) return null;
1984
+ const src = resolve2(sourcePath);
1985
+ switch (strat) {
1986
+ case "image":
1987
+ case "video":
1988
+ case "audio":
1989
+ return genFfmpeg(src, strat);
1990
+ case "text":
1991
+ return genText(src);
1992
+ case "pdf":
1993
+ return genPdf(src);
1994
+ case "archive":
1995
+ return genArchive(src);
1996
+ }
1997
+ }
1998
+
1999
+ // src/prompt.ts
2000
+ function readLineFromStdin() {
2001
+ return new Promise((resolve5) => {
2002
+ let data = "";
2003
+ process.stdin.setEncoding("utf8");
2004
+ process.stdin.on("data", (ch) => data += ch);
2005
+ process.stdin.on("end", () => resolve5(data.replace(/\r?\n$/, "")));
2006
+ process.stdin.resume();
2007
+ });
2008
+ }
2009
+ function readPassword(promptText) {
2010
+ const stdin = process.stdin;
2011
+ if (!stdin.isTTY) return readLineFromStdin();
2012
+ return new Promise((resolve5, reject) => {
2013
+ process.stderr.write(promptText);
2014
+ let input = "";
2015
+ const wasRaw = stdin.isRaw;
2016
+ stdin.setRawMode?.(true);
2017
+ stdin.resume();
2018
+ stdin.setEncoding("utf8");
2019
+ const cleanup = () => {
2020
+ stdin.setRawMode?.(wasRaw ?? false);
2021
+ stdin.pause();
2022
+ stdin.off("data", onData);
2023
+ };
2024
+ const onData = (ch) => {
2025
+ for (const ct of ch) {
2026
+ if (ct === "\r" || ct === "\n") {
2027
+ cleanup();
2028
+ process.stderr.write("\n");
2029
+ resolve5(input);
2030
+ return;
2031
+ } else if (ct === "") {
2032
+ cleanup();
2033
+ reject(new BizhouError("INVALID_ARG", "\u5DF2\u53D6\u6D88"));
2034
+ return;
2035
+ } else if (ct === "\x7F" || ct === "\b") {
2036
+ input = input.slice(0, -1);
2037
+ } else if (ct >= " ") {
2038
+ input += ct;
2039
+ }
2040
+ }
2041
+ };
2042
+ stdin.on("data", onData);
2043
+ });
2044
+ }
2045
+ async function resolveMasterPassword(promptText, opts = {}) {
2046
+ if (opts.passwordStdin) return readLineFromStdin();
2047
+ const fromEnv = process.env.BIZHOU_MASTER_PASSWORD;
2048
+ if (fromEnv !== void 0) return fromEnv;
2049
+ return readPassword(promptText);
2050
+ }
2051
+
2052
+ // src/render.ts
2053
+ var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
2054
+ function paint(code, s) {
2055
+ return useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
2056
+ }
2057
+ var c = {
2058
+ green: (s) => paint("32", s),
2059
+ red: (s) => paint("31", s),
2060
+ yellow: (s) => paint("33", s),
2061
+ dim: (s) => paint("2", s),
2062
+ bold: (s) => paint("1", s),
2063
+ cyan: (s) => paint("36", s)
2064
+ };
2065
+ function info(msg) {
2066
+ process.stderr.write(`${msg}
2067
+ `);
2068
+ }
2069
+ function ok(msg) {
2070
+ process.stderr.write(`${c.green("\u2713 ") + msg}
2071
+ `);
2072
+ }
2073
+ function warn(msg) {
2074
+ process.stderr.write(`${c.yellow("\u26A0 ") + msg}
2075
+ `);
2076
+ }
2077
+ function errorLine(msg) {
2078
+ process.stderr.write(`${c.red("\u2717 ") + msg}
2079
+ `);
2080
+ }
2081
+ function out(msg) {
2082
+ process.stdout.write(`${msg}
2083
+ `);
2084
+ }
2085
+ function formatBytes(n) {
2086
+ const units = ["B", "KB", "MB", "GB", "TB"];
2087
+ let v = n;
2088
+ let i = 0;
2089
+ while (v >= 1024 && i < units.length - 1) {
2090
+ v /= 1024;
2091
+ i++;
2092
+ }
2093
+ return `${i === 0 ? v : v.toFixed(2)} ${units[i]}`;
2094
+ }
2095
+ function renderProgress(label, done, total) {
2096
+ if (!process.stderr.isTTY) return;
2097
+ const pct = total > 0 ? Math.min(100, Math.floor(done / total * 100)) : 100;
2098
+ const width = 24;
2099
+ const filled = Math.floor(pct / 100 * width);
2100
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
2101
+ process.stderr.write(
2102
+ `\r${label} ${c.cyan(bar)} ${pct}% ${formatBytes(done)}/${formatBytes(total)} `
2103
+ );
2104
+ }
2105
+ function endProgress() {
2106
+ if (process.stderr.isTTY) process.stderr.write("\n");
2107
+ }
2108
+ function exitCodeFor(code) {
2109
+ switch (code) {
2110
+ case "INVALID_ARG":
2111
+ return 2;
2112
+ case "AUTH":
2113
+ return 3;
2114
+ case "OAUTH":
2115
+ return 4;
2116
+ case "BAIDU":
2117
+ return 5;
2118
+ case "MANIFEST":
2119
+ case "BUNDLE":
2120
+ case "CHUNK":
2121
+ return 6;
2122
+ case "ACCOUNT":
2123
+ case "VAULT":
2124
+ return 7;
2125
+ default:
2126
+ return 1;
2127
+ }
2128
+ }
2129
+
2130
+ // src/runtime.ts
2131
+ import { existsSync, readFileSync } from "fs";
2132
+ import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
2133
+ import { join as join9 } from "path";
2134
+ function parseDotenv(text) {
2135
+ const out2 = {};
2136
+ for (const rawLine of text.split(/\r?\n/)) {
2137
+ const line = rawLine.trim();
2138
+ if (!line || line.startsWith("#")) continue;
2139
+ const eq = line.indexOf("=");
2140
+ if (eq < 0) continue;
2141
+ const key = line.slice(0, eq).trim();
2142
+ let val = line.slice(eq + 1).trim();
2143
+ if (val.startsWith("'") && val.endsWith("'") || val.startsWith('"') && val.endsWith('"')) {
2144
+ val = val.slice(1, -1);
2145
+ }
2146
+ out2[key] = val;
2147
+ }
2148
+ return out2;
2149
+ }
2150
+ function loadDotenv(cwd = process.cwd()) {
2151
+ const p = join9(cwd, ".env");
2152
+ if (!existsSync(p)) return;
2153
+ const parsed = parseDotenv(readFileSync(p, "utf8"));
2154
+ for (const [k, v] of Object.entries(parsed)) {
2155
+ if (process.env[k] === void 0) process.env[k] = v;
2156
+ }
2157
+ }
2158
+ var httpAdapter = (url, init) => fetch(url, init);
2159
+ function createRuntime() {
2160
+ loadDotenv();
2161
+ const paths = configPaths(process.env, process.platform);
2162
+ const secrets = new FileSecretStore(paths.dir, paths.secrets, paths.deviceKey);
2163
+ const accounts = new AccountManager(secrets);
2164
+ let configFileRoot;
2165
+ let configUploadConcurrency;
2166
+ let configDaemonSweepIntervalMs;
2167
+ let configDaemonDebounceMs;
2168
+ try {
2169
+ const cfg = JSON.parse(readFileSync(paths.config, "utf8"));
2170
+ configFileRoot = cfg.fileRoot;
2171
+ configUploadConcurrency = cfg.uploadConcurrency;
2172
+ configDaemonSweepIntervalMs = cfg.daemonSweepIntervalMs;
2173
+ configDaemonDebounceMs = cfg.daemonDebounceMs;
2174
+ } catch {
2175
+ }
2176
+ const fileRoot = resolveFileRoot(process.env, process.platform, configFileRoot);
2177
+ const uploadConcurrency = Math.min(16, Math.max(1, configUploadConcurrency ?? 4));
2178
+ const daemonSweepIntervalMs = Math.max(5e3, configDaemonSweepIntervalMs ?? 30 * 60 * 1e3);
2179
+ const daemonDebounceMs = Math.max(100, configDaemonDebounceMs ?? 2e3);
2180
+ return {
2181
+ paths,
2182
+ accounts,
2183
+ http: httpAdapter,
2184
+ fileRoot,
2185
+ uploadConcurrency,
2186
+ daemonSweepIntervalMs,
2187
+ daemonDebounceMs,
2188
+ now: () => Date.now(),
2189
+ oauthConfig() {
2190
+ const appKey = process.env.BAIDU_APP_KEY;
2191
+ const secretKey = process.env.BAIDU_SECRET_KEY;
2192
+ if (!appKey || !secretKey) {
2193
+ throw new VaultError(
2194
+ "\u7F3A\u5C11\u767E\u5EA6\u51ED\u8BC1\uFF1A\u8BF7\u5728 .env \u914D\u7F6E BAIDU_APP_KEY \u4E0E BAIDU_SECRET_KEY\uFF08\u89C1 .env.example\uFF09"
2195
+ );
2196
+ }
2197
+ return { appKey, secretKey };
2198
+ },
2199
+ vaultExists: () => existsSync(paths.vault),
2200
+ async loadVault() {
2201
+ if (!existsSync(paths.vault)) {
2202
+ throw new VaultError("\u5C1A\u672A\u521D\u59CB\u5316\uFF1A\u8BF7\u5148\u8FD0\u884C `bz init`");
2203
+ }
2204
+ return JSON.parse(await readFile8(paths.vault, "utf8"));
2205
+ },
2206
+ async saveVault(v) {
2207
+ await mkdir7(paths.dir, { recursive: true });
2208
+ await writeFile7(paths.vault, JSON.stringify(v, null, 2), "utf8");
2209
+ },
2210
+ async resolveMk(opts = {}) {
2211
+ const cached = await accounts.getCachedMk(Date.now());
2212
+ if (cached) return cached;
2213
+ const vault = await this.loadVault();
2214
+ const pw = await resolveMasterPassword("\u4E3B\u5BC6\u7801: ", opts);
2215
+ return unlockWithPassword(vault, pw);
2216
+ }
2217
+ };
2218
+ }
2219
+ async function baiduClientForCurrent(rt, concurrency) {
2220
+ const cur = await rt.accounts.getCurrent();
2221
+ if (!cur) {
2222
+ throw new VaultError("\u5C1A\u672A\u767B\u5F55\u4EFB\u4F55\u8D26\u53F7\uFF1A\u8BF7\u5148 `bz login`");
2223
+ }
2224
+ let tokens = cur.tokens;
2225
+ const nearExpiry = tokens.expiresAt !== void 0 && Date.now() > tokens.expiresAt - 6e4;
2226
+ if (nearExpiry && tokens.refreshToken) {
2227
+ const r = await refreshAccessToken(rt.oauthConfig(), tokens.refreshToken, rt.http);
2228
+ tokens = {
2229
+ accessToken: r.accessToken,
2230
+ refreshToken: r.refreshToken ?? tokens.refreshToken,
2231
+ expiresAt: Date.now() + r.expiresIn * 1e3,
2232
+ scope: r.scope ?? tokens.scope
2233
+ };
2234
+ await rt.accounts.updateTokens(cur.name, tokens);
2235
+ }
2236
+ return new BaiduClient(rt.oauthConfig(), tokens.accessToken, rt.http, {
2237
+ uploadConcurrency: concurrency ?? rt.uploadConcurrency
2238
+ });
2239
+ }
2240
+ async function makeBackend(rt, localDir, concurrency) {
2241
+ if (localDir) return new LocalBackend(localDir);
2242
+ return new BaiduBackend(await baiduClientForCurrent(rt, concurrency));
2243
+ }
2244
+
2245
+ // src/commands.ts
2246
+ function parseSize(s) {
2247
+ const m = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb|k|m|g)?$/i.exec(s.trim());
2248
+ if (!m) throw new BizhouError("INVALID_ARG", `\u65E0\u6CD5\u89E3\u6790\u5927\u5C0F\uFF1A${s}`);
2249
+ const n = Number.parseFloat(m[1]);
2250
+ const unit = (m[2] ?? "b").toLowerCase();
2251
+ const mult = {
2252
+ b: 1,
2253
+ k: 1024,
2254
+ kb: 1024,
2255
+ m: 1024 ** 2,
2256
+ mb: 1024 ** 2,
2257
+ g: 1024 ** 3,
2258
+ gb: 1024 ** 3
2259
+ };
2260
+ return Math.floor(n * mult[unit]);
2261
+ }
2262
+ var SHARE_PREFIX = "BZK1-";
2263
+ async function cmdInit(rt, opts) {
2264
+ if (rt.vaultExists() && !opts.force) {
2265
+ throw new BizhouError(
2266
+ "VAULT",
2267
+ "\u5DF2\u521D\u59CB\u5316\uFF08vault \u5DF2\u5B58\u5728\uFF09\u3002\u5982\u9700\u91CD\u7F6E\u8BF7\u52A0 --force\uFF08\u4F1A\u4F7F\u65E7\u6570\u636E\u4E0D\u53EF\u89E3\uFF01\uFF09"
2268
+ );
2269
+ }
2270
+ const interactive = process.stdin.isTTY && !opts.passwordStdin && process.env.BIZHOU_MASTER_PASSWORD === void 0;
2271
+ const pw = await resolveMasterPassword("\u8BBE\u7F6E\u4E3B\u5BC6\u7801: ", opts);
2272
+ if (!pw) throw new BizhouError("INVALID_ARG", "\u4E3B\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A");
2273
+ if (interactive) {
2274
+ const pw2 = await readPassword("\u518D\u6B21\u8F93\u5165\u786E\u8BA4: ");
2275
+ if (pw !== pw2) throw new BizhouError("INVALID_ARG", "\u4E24\u6B21\u8F93\u5165\u4E0D\u4E00\u81F4");
2276
+ }
2277
+ const { vault, recoveryKey } = await createVault(pw, { createdAt: (/* @__PURE__ */ new Date()).toISOString() });
2278
+ await rt.saveVault(vault);
2279
+ ok(`\u5DF2\u521D\u59CB\u5316 vault\uFF1A${rt.paths.vault}`);
2280
+ info("");
2281
+ warn("\u4EE5\u4E0B\u662F\u4F60\u7684\u6062\u590D\u5BC6\u94A5\uFF0C\u53EA\u663E\u793A\u8FD9\u4E00\u6B21\uFF0C\u8BF7\u79BB\u7EBF\u59A5\u5584\u4FDD\u7BA1\uFF08\u5FD8\u8BB0\u4E3B\u5BC6\u7801\u65F6\u7528\u5B83\u6062\u590D\uFF09\uFF1A");
2282
+ out(c.bold(recoveryKey));
2283
+ info("");
2284
+ warn("\u4EFB\u4F55\u4EBA\u62FF\u5230\u6062\u590D\u5BC6\u94A5\u5373\u53EF\u89E3\u5F00\u4F60\u7684\u6570\u636E \u2014\u2014 \u5207\u52FF\u4E0A\u4F20\u3001\u622A\u56FE\u6216\u5B58\u5165\u4E91\u7AEF\u3002");
2285
+ }
2286
+ async function cmdUnlock(rt, opts) {
2287
+ const vault = await rt.loadVault();
2288
+ const pw = await resolveMasterPassword("\u4E3B\u5BC6\u7801: ", opts);
2289
+ const mk = await unlockWithPassword(vault, pw);
2290
+ const ttlSec = opts.ttl ?? Number(process.env.BIZHOU_UNLOCK_TTL ?? 8 * 3600);
2291
+ await rt.accounts.cacheMk(mk, Date.now() + ttlSec * 1e3);
2292
+ ok(`\u5DF2\u89E3\u9501\u672C\u8BBE\u5907\u4F1A\u8BDD\uFF08${Math.round(ttlSec / 3600)} \u5C0F\u65F6\u540E\u81EA\u52A8\u4E0A\u9501\uFF09`);
2293
+ }
2294
+ async function cmdLock(rt) {
2295
+ await rt.accounts.clearMk();
2296
+ ok("\u5DF2\u4E0A\u9501\uFF08\u6E05\u9664\u7F13\u5B58\u4E3B\u5BC6\u94A5\uFF09");
2297
+ }
2298
+ async function cmdPasswd(rt, opts) {
2299
+ const vault = await rt.loadVault();
2300
+ const cur = await resolveMasterPassword("\u5F53\u524D\u4E3B\u5BC6\u7801: ", opts);
2301
+ const next = await readPassword("\u65B0\u4E3B\u5BC6\u7801: ");
2302
+ const confirm = await readPassword("\u518D\u6B21\u8F93\u5165\u65B0\u4E3B\u5BC6\u7801: ");
2303
+ if (next !== confirm) throw new BizhouError("INVALID_ARG", "\u4E24\u6B21\u8F93\u5165\u4E0D\u4E00\u81F4");
2304
+ const v2 = await changePassword(vault, cur, next);
2305
+ await rt.saveVault(v2);
2306
+ await rt.accounts.clearMk();
2307
+ ok("\u4E3B\u5BC6\u7801\u5DF2\u66F4\u65B0\uFF08\u6062\u590D\u5BC6\u94A5\u4E0D\u53D8\uFF1B\u5DF2\u4E0A\u9501\uFF0C\u8BF7\u91CD\u65B0 unlock\uFF09");
2308
+ }
2309
+ async function cmdRecover(rt) {
2310
+ const vault = await rt.loadVault();
2311
+ info("\u7528\u6062\u590D\u5BC6\u94A5\u9A8C\u8BC1\u5E76\u91CD\u8BBE\u4E3B\u5BC6\u7801\u3002");
2312
+ const rk = (await readPassword("\u6062\u590D\u5BC6\u94A5: ")).trim();
2313
+ const mk = await unlockWithRecovery(vault, rk);
2314
+ const next = await readPassword("\u65B0\u4E3B\u5BC6\u7801: ");
2315
+ const confirm = await readPassword("\u518D\u6B21\u8F93\u5165\u65B0\u4E3B\u5BC6\u7801: ");
2316
+ if (next !== confirm) throw new BizhouError("INVALID_ARG", "\u4E24\u6B21\u8F93\u5165\u4E0D\u4E00\u81F4");
2317
+ const salt = generateSalt();
2318
+ const kek = await deriveKey(next, salt, vault.kdf);
2319
+ const v2 = { ...vault, pwSalt: salt.toString("base64"), wrappedMkByPassword: wrapKey(kek, mk) };
2320
+ await rt.saveVault(v2);
2321
+ await rt.accounts.clearMk();
2322
+ ok("\u5DF2\u7528\u6062\u590D\u5BC6\u94A5\u91CD\u8BBE\u4E3B\u5BC6\u7801\u3002");
2323
+ }
2324
+ async function completeLoginToken(rt, name, token) {
2325
+ await rt.accounts.upsertAccount(name, {
2326
+ accessToken: token.accessToken,
2327
+ refreshToken: token.refreshToken,
2328
+ expiresAt: Date.now() + token.expiresIn * 1e3,
2329
+ scope: token.scope
2330
+ });
2331
+ ok(`\u8D26\u53F7\u300C${name}\u300D\u767B\u5F55\u6210\u529F\u3002`);
2332
+ }
2333
+ async function cmdLogin(rt, opts) {
2334
+ const config = rt.oauthConfig();
2335
+ const name = opts.name ?? "default";
2336
+ if (opts.device) {
2337
+ const dc = await startDeviceFlow(config, rt.http);
2338
+ info(`\u8BF7\u5728\u6D4F\u89C8\u5668\u6253\u5F00\uFF1A${c.cyan(dc.verificationUrl)}`);
2339
+ info(`\u5E76\u8F93\u5165\u8BBE\u5907\u7801\uFF1A${c.bold(dc.userCode)}`);
2340
+ info("\u7B49\u5F85\u6388\u6743\u4E2D\u2026\uFF08Ctrl-C \u53D6\u6D88\uFF09");
2341
+ const deadline = Date.now() + dc.expiresIn * 1e3;
2342
+ while (Date.now() < deadline) {
2343
+ await new Promise((r) => setTimeout(r, dc.interval * 1e3));
2344
+ const token2 = await pollDeviceToken(config, dc.deviceCode, rt.http);
2345
+ if (token2) {
2346
+ await completeLoginToken(rt, name, token2);
2347
+ return;
2348
+ }
2349
+ }
2350
+ throw new BizhouError("OAUTH", "\u8BBE\u5907\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u8BD5\u3002");
2351
+ }
2352
+ const port = opts.port ?? 8899;
2353
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
2354
+ const url = buildAuthorizeUrl(config, redirectUri);
2355
+ info(`\u8BF7\u5728\u6D4F\u89C8\u5668\u6253\u5F00\u4EE5\u4E0B\u5730\u5740\u5B8C\u6210\u6388\u6743\uFF1A`);
2356
+ out(url);
2357
+ const code = await waitForCallbackCode(port);
2358
+ const token = await exchangeCodeForToken(config, code, redirectUri, rt.http);
2359
+ await completeLoginToken(rt, name, token);
2360
+ }
2361
+ function waitForCallbackCode(port) {
2362
+ return new Promise((resolve5, reject) => {
2363
+ const server = createServer((req2, res) => {
2364
+ const u = new URL(req2.url ?? "/", `http://127.0.0.1:${port}`);
2365
+ const code = u.searchParams.get("code");
2366
+ if (code) {
2367
+ res.end("\u655D\u5E1A\uFF1A\u6388\u6743\u6210\u529F\uFF0C\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\u8FD4\u56DE\u7EC8\u7AEF\u3002");
2368
+ server.close();
2369
+ resolve5(code);
2370
+ } else {
2371
+ res.statusCode = 400;
2372
+ res.end("\u7F3A\u5C11 code");
2373
+ }
2374
+ });
2375
+ server.on("error", reject);
2376
+ server.listen(port, "127.0.0.1");
2377
+ });
2378
+ }
2379
+ async function cmdLogout(rt) {
2380
+ const cur = await rt.accounts.getCurrent();
2381
+ if (!cur) throw new BizhouError("ACCOUNT", "\u5F53\u524D\u65E0\u767B\u5F55\u8D26\u53F7");
2382
+ await rt.accounts.removeAccount(cur.name);
2383
+ ok(`\u8D26\u53F7\u300C${cur.name}\u300D\u5DF2\u6CE8\u9500\u3002`);
2384
+ }
2385
+ async function cmdAccount(rt, sub, arg) {
2386
+ if (!sub || sub === "list") {
2387
+ const { names, current } = await rt.accounts.listAccounts();
2388
+ if (names.length === 0) {
2389
+ info("\uFF08\u6682\u65E0\u8D26\u53F7\uFF0C\u5148 `bz login`\uFF09");
2390
+ return;
2391
+ }
2392
+ for (const n of names) out(`${n === current ? c.green("* ") : " "}${n}`);
2393
+ return;
2394
+ }
2395
+ if (sub === "use") {
2396
+ if (!arg) throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz account use <name>");
2397
+ await rt.accounts.useAccount(arg);
2398
+ ok(`\u5DF2\u5207\u6362\u5230\u8D26\u53F7\u300C${arg}\u300D`);
2399
+ return;
2400
+ }
2401
+ if (sub === "add") {
2402
+ await cmdLogin(rt, { name: arg });
2403
+ return;
2404
+ }
2405
+ throw new BizhouError("INVALID_ARG", `\u672A\u77E5\u5B50\u547D\u4EE4\uFF1Aaccount ${sub}`);
2406
+ }
2407
+ var UPLOAD_LOCK_TTL_MS = 30 * 60 * 1e3;
2408
+ function pidAlive(pid) {
2409
+ try {
2410
+ process.kill(pid, 0);
2411
+ return true;
2412
+ } catch (e) {
2413
+ return e.code === "EPERM";
2414
+ }
2415
+ }
2416
+ function resolveUploadConcurrency(rt, flag) {
2417
+ const v = Number.isFinite(flag) ? flag : rt.uploadConcurrency ?? 4;
2418
+ const finite = Number.isFinite(v) ? v : 4;
2419
+ return Math.min(16, Math.max(1, Math.floor(finite)));
2420
+ }
2421
+ async function findDuplicateBundle(rt, backend, mk, cloudDir, targetContentId) {
2422
+ const { bundles } = await backend.listDir(cloudDir);
2423
+ for (const b of bundles) {
2424
+ let raw = await getCachedManifest(rt.paths.dir, b.id);
2425
+ if (raw === null) {
2426
+ try {
2427
+ raw = await backend.bundleStore(b.id, cloudDir).getManifest();
2428
+ } catch {
2429
+ continue;
2430
+ }
2431
+ await putCachedManifest(rt.paths.dir, b.id, raw);
2432
+ }
2433
+ try {
2434
+ const m = parseManifest(raw);
2435
+ const meta = openMeta(unwrapDek(mk, m.wrappedKey), m.encMeta);
2436
+ if (meta.contentId === targetContentId) return b.id;
2437
+ } catch {
2438
+ }
2439
+ }
2440
+ return null;
2441
+ }
2442
+ async function pushOneFile(rt, backend, mk, contentKey, absFile, cloudDir, opts) {
2443
+ const st = await stat(absFile);
2444
+ const contentId = await hashPlaintextFile(absFile, contentKey);
2445
+ const jpath = journalPath(rt.paths.dir, "upload", contentId, cloudDir);
2446
+ if (cloudDir !== "/") await backend.mkdir(cloudDir);
2447
+ if (!opts.force) {
2448
+ const dup = await findDuplicateBundle(rt, backend, mk, cloudDir, contentId);
2449
+ if (dup) {
2450
+ warn(`\u76EE\u6807\u5DF2\u6709\u76F8\u540C\u6587\u4EF6\uFF08${c.bold(dup)}\uFF09\uFF0C\u8DF3\u8FC7\uFF1A${absFile}`);
2451
+ return { bundleId: dup, status: "skipped-dup" };
2452
+ }
2453
+ }
2454
+ const optChunkSize = opts.noSplit ? Math.max(st.size, 1) : opts.chunk ? parseSize(opts.chunk) : DEFAULT_CHUNK_SIZE;
2455
+ const optCompression = opts.compress ? "gzip" : "none";
2456
+ const existing = await readJournal(jpath);
2457
+ let bundleId;
2458
+ let skipExisting = [];
2459
+ let status = "uploaded";
2460
+ let dek;
2461
+ let chunkSize;
2462
+ let compression;
2463
+ if (existing) {
2464
+ const alive = isLockAlive(existing, {
2465
+ ttlMs: UPLOAD_LOCK_TTL_MS,
2466
+ now: rt.now(),
2467
+ pidAlive: pidAlive(existing.pid)
2468
+ });
2469
+ if (alive && !opts.force) {
2470
+ warn(`\u540C\u6587\u4EF6\u6B63\u5728\u4E0A\u4F20\u81F3\u8BE5\u76EE\u5F55\uFF0C\u672C\u6B21\u8DF3\u8FC7\uFF1A${absFile}`);
2471
+ return { bundleId: existing.bundleId, status: "locked" };
2472
+ }
2473
+ if (existing.wrappedKey === void 0 || existing.chunkSize === void 0 || existing.compression === void 0) {
2474
+ throw new BizhouError(
2475
+ "INVALID_ARG",
2476
+ `\u4E0A\u4F20\u65E5\u5FD7\u7F3A\u5C11\u5FC5\u8981\u5B57\u6BB5\uFF08wrappedKey/chunkSize/compression\uFF09\uFF0C\u65E0\u6CD5\u7EED\u4F20\uFF1A${jpath}`
2477
+ );
2478
+ }
2479
+ bundleId = existing.bundleId;
2480
+ skipExisting = existing.doneChunks;
2481
+ dek = unwrapDek(mk, existing.wrappedKey);
2482
+ status = "resumed";
2483
+ chunkSize = existing.chunkSize;
2484
+ compression = existing.compression;
2485
+ if (chunkSize !== optChunkSize || compression !== optCompression) {
2486
+ warn("\u7EED\u4F20\uFF1A\u6CBF\u7528\u539F\u5206\u7247\u5927\u5C0F/\u538B\u7F29\u8BBE\u7F6E\uFF0C\u5FFD\u7565\u672C\u6B21\u4E0D\u540C\u7684 --chunk/--no-split/--compress");
2487
+ }
2488
+ } else {
2489
+ bundleId = generateBundleId();
2490
+ dek = generateKey();
2491
+ chunkSize = optChunkSize;
2492
+ compression = optCompression;
2493
+ }
2494
+ const totalChunks = Math.max(1, Math.ceil(st.size / chunkSize));
2495
+ const store = backend.bundleStore(bundleId, cloudDir);
2496
+ await writeJournal(jpath, {
2497
+ bundleId,
2498
+ cloudDir,
2499
+ contentId,
2500
+ doneChunks: skipExisting,
2501
+ totalChunks,
2502
+ chunkSize,
2503
+ // 固定分片大小,续传原样沿用(杜绝确定性 IV 的 nonce 复用)
2504
+ compression,
2505
+ // 固定压缩方式,同上
2506
+ wrappedKey: wrapDek(mk, dek),
2507
+ // MK 包裹的 DEK(非裸密钥),供续传还原同一 DEK
2508
+ startedAt: new Date(rt.now()).toISOString(),
2509
+ pid: process.pid
2510
+ });
2511
+ let preview;
2512
+ if (opts.preview) {
2513
+ let p = null;
2514
+ try {
2515
+ p = await generatePreview(absFile);
2516
+ } catch {
2517
+ p = null;
2518
+ }
2519
+ if (p) {
2520
+ preview = p;
2521
+ info(`\u5DF2\u751F\u6210\u9884\u89C8\u5305\uFF08${p.kind}\uFF0C${formatBytes(p.data.length)}\uFF09`);
2522
+ } else {
2523
+ warn("\u672A\u751F\u6210\u9884\u89C8\uFF08\u975E\u76EE\u6807\u7C7B\u578B\u6216\u6240\u9700\u5916\u90E8\u5DE5\u5177\u4E0D\u53EF\u7528\uFF09\uFF0C\u7EE7\u7EED\u4E0A\u4F20\u539F\u6587\u4EF6\u3002");
2524
+ }
2525
+ }
2526
+ let chain = Promise.resolve();
2527
+ try {
2528
+ await packResource({
2529
+ filePath: absFile,
2530
+ fileSize: st.size,
2531
+ mk,
2532
+ dek,
2533
+ bundleId,
2534
+ createdAt: new Date(rt.now()).toISOString(),
2535
+ chunkSize,
2536
+ compression,
2537
+ store,
2538
+ name: opts.name ?? basename3(absFile),
2539
+ mtime: st.mtime.toISOString(),
2540
+ contentId,
2541
+ preview,
2542
+ skipExisting,
2543
+ onProgress: (e) => {
2544
+ renderProgress("\u52A0\u5BC6", e.bytesDone, e.bytesTotal);
2545
+ chain = chain.then(() => appendDoneChunk(jpath, e.seq));
2546
+ }
2547
+ });
2548
+ await chain;
2549
+ } catch (err) {
2550
+ await chain.catch(() => {
2551
+ });
2552
+ endProgress();
2553
+ throw err;
2554
+ }
2555
+ endProgress();
2556
+ await removeJournal(jpath);
2557
+ return { bundleId, status };
2558
+ }
2559
+ async function cmdPush(rt, filePath, opts) {
2560
+ const st = await stat(filePath);
2561
+ if (opts.recursive) {
2562
+ if (!st.isDirectory()) throw new BizhouError("INVALID_ARG", `-r \u9700\u8981\u76EE\u5F55\uFF1A${filePath}`);
2563
+ return cmdPushRecursive(rt, filePath, opts);
2564
+ }
2565
+ if (!st.isFile()) throw new BizhouError("INVALID_ARG", `\u4E0D\u662F\u6587\u4EF6\uFF1A${filePath}`);
2566
+ const mk = await rt.resolveMk(opts);
2567
+ const contentKey = deriveContentKey(mk);
2568
+ const cloudDir = opts.to ? normalizeCloudPath(opts.to) : defaultUploadCloudDir(resolve3(filePath), rt.fileRoot);
2569
+ const backend = await makeBackend(rt, opts.local, resolveUploadConcurrency(rt, opts.concurrency));
2570
+ info(`\u52A0\u5BC6\u4E0A\u4F20\uFF1A${filePath}\uFF08${formatBytes(st.size)}\uFF09\u2192 ${cloudDir}`);
2571
+ const r = await pushOneFile(rt, backend, mk, contentKey, resolve3(filePath), cloudDir, opts);
2572
+ if (r.status === "skipped-dup" || r.status === "locked") return r.bundleId;
2573
+ ok(`\u5DF2\u4E0A\u4F20${r.status === "resumed" ? "\uFF08\u7EED\u4F20\uFF09" : ""}\u3002\u8D44\u6E90 ID\uFF1A${c.bold(r.bundleId)}`);
2574
+ out(r.bundleId);
2575
+ return r.bundleId;
2576
+ }
2577
+ async function walkLocalFiles(absDir) {
2578
+ const result = [];
2579
+ const walk = async (dir) => {
2580
+ const entries = await readdir4(dir, { withFileTypes: true });
2581
+ for (const e of entries) {
2582
+ const p = join10(dir, e.name);
2583
+ if (e.isDirectory()) await walk(p);
2584
+ else if (e.isFile()) result.push(p);
2585
+ }
2586
+ };
2587
+ await walk(absDir);
2588
+ return result;
2589
+ }
2590
+ async function cmdPushRecursive(rt, dirPath, opts) {
2591
+ const mk = await rt.resolveMk(opts);
2592
+ const contentKey = deriveContentKey(mk);
2593
+ const absDir = resolve3(dirPath);
2594
+ const baseCloud = opts.to ? normalizeCloudPath(opts.to) : defaultUploadCloudDir(absDir + sep, rt.fileRoot);
2595
+ const rootCloud = joinCloudPath(baseCloud, basename3(absDir));
2596
+ const backend = await makeBackend(rt, opts.local, resolveUploadConcurrency(rt, opts.concurrency));
2597
+ if (rootCloud !== "/") await backend.mkdir(rootCloud);
2598
+ const files = await walkLocalFiles(absDir);
2599
+ if (files.length === 0) {
2600
+ info(`\uFF08\u7A7A\u76EE\u5F55\uFF0C\u65E0\u6587\u4EF6\u53EF\u4E0A\u4F20\uFF09\uFF1A${absDir}`);
2601
+ return rootCloud;
2602
+ }
2603
+ let uploaded = 0;
2604
+ let skipped = 0;
2605
+ for (const abs of files) {
2606
+ const rel = relative2(absDir, abs);
2607
+ const relDir = dirname4(rel);
2608
+ const cloudDir = relDir === "." ? rootCloud : joinCloudPath(rootCloud, relDir);
2609
+ info(`\u52A0\u5BC6\u4E0A\u4F20\uFF1A${abs} \u2192 ${cloudDir}/${basename3(abs)}`);
2610
+ const r = await pushOneFile(rt, backend, mk, contentKey, abs, cloudDir, opts);
2611
+ if (r.status === "skipped-dup") skipped++;
2612
+ else if (r.status === "locked") {
2613
+ } else uploaded++;
2614
+ if (r.status === "uploaded" || r.status === "resumed") ok(`\u5DF2\u4E0A\u4F20\uFF1A${rel} \u2192 ${r.bundleId}`);
2615
+ }
2616
+ ok(
2617
+ `\u6574\u6811\u5B8C\u6210\uFF1A\u4E0A\u4F20 ${uploaded}\uFF0C\u8DF3\u8FC7\uFF08\u5DF2\u5B58\u5728\uFF09${skipped}\uFF0C\u5171 ${files.length} \u4E2A\u6587\u4EF6 \u2192 ${rootCloud}`
2618
+ );
2619
+ return rootCloud;
2620
+ }
2621
+ var DOWNLOAD_LOCK_TTL_MS = 30 * 60 * 1e3;
2622
+ function downloadJournalKey(contentId, bundleId) {
2623
+ return contentId && contentId.length > 0 ? contentId : bundleId;
2624
+ }
2625
+ async function fileExists(p) {
2626
+ try {
2627
+ await stat(p);
2628
+ return true;
2629
+ } catch {
2630
+ return false;
2631
+ }
2632
+ }
2633
+ async function pullOneBundle(rt, backend, mk, contentKey, fullId, dir, outPath, opts) {
2634
+ const store = backend.bundleStore(fullId, dir);
2635
+ const { manifest, meta } = await readResourceMeta(mk, store);
2636
+ const cid = meta.contentId;
2637
+ const jkey = downloadJournalKey(cid, fullId);
2638
+ const jpath = journalPath(rt.paths.dir, "download", jkey, outPath);
2639
+ const partPath = `${outPath}.part`;
2640
+ if (!opts.force && cid && await fileExists(outPath)) {
2641
+ if (await hashPlaintextFile(outPath, contentKey) === cid) {
2642
+ warn(`\u76EE\u6807\u5DF2\u6709\u76F8\u540C\u6587\u4EF6\uFF0C\u8DF3\u8FC7\uFF1A${outPath}`);
2643
+ return { status: "skipped-dup", bytesWritten: 0 };
2644
+ }
2645
+ }
2646
+ const existing = await readJournal(jpath);
2647
+ let skip = [];
2648
+ let status = "restored";
2649
+ if (existing) {
2650
+ const alive = isLockAlive(existing, {
2651
+ ttlMs: DOWNLOAD_LOCK_TTL_MS,
2652
+ now: rt.now(),
2653
+ pidAlive: pidAlive(existing.pid)
2654
+ });
2655
+ if (alive && !opts.force) {
2656
+ warn(`\u540C\u6587\u4EF6\u6B63\u5728\u4E0B\u8F7D\uFF0C\u672C\u6B21\u8DF3\u8FC7\uFF1A${outPath}`);
2657
+ return { status: "locked", bytesWritten: 0 };
2658
+ }
2659
+ if (await fileExists(partPath)) {
2660
+ skip = existing.doneChunks;
2661
+ status = "resumed";
2662
+ }
2663
+ }
2664
+ await mkdir8(dirname4(outPath), { recursive: true });
2665
+ await writeJournal(jpath, {
2666
+ bundleId: fullId,
2667
+ cloudDir: dir,
2668
+ contentId: cid ?? "",
2669
+ doneChunks: skip,
2670
+ totalChunks: manifest.chunks.length,
2671
+ startedAt: new Date(rt.now()).toISOString(),
2672
+ pid: process.pid
2673
+ });
2674
+ let chain = Promise.resolve();
2675
+ let bytesWritten = 0;
2676
+ try {
2677
+ const res = await unpackResource({
2678
+ mk,
2679
+ store,
2680
+ outPath: partPath,
2681
+ skip,
2682
+ onProgress: (e) => {
2683
+ renderProgress("\u89E3\u5BC6", e.bytesDone, e.bytesTotal);
2684
+ chain = chain.then(() => appendDoneChunk(jpath, e.seq));
2685
+ }
2686
+ });
2687
+ bytesWritten = res.bytesWritten;
2688
+ await chain;
2689
+ } catch (err) {
2690
+ endProgress();
2691
+ await chain.catch(() => {
2692
+ });
2693
+ throw err;
2694
+ }
2695
+ endProgress();
2696
+ if (cid) {
2697
+ const got = await hashPlaintextFile(partPath, contentKey);
2698
+ if (got !== cid) {
2699
+ throw new BizhouError(
2700
+ "CHUNK",
2701
+ `\u4E0B\u8F7D\u6587\u4EF6 contentId \u6821\u9A8C\u5931\u8D25\uFF08\u6570\u636E\u4E0D\u5B8C\u6574\u6216\u635F\u574F\uFF09\uFF0C\u672A\u4EA4\u4ED8\uFF1A${outPath}`
2702
+ );
2703
+ }
2704
+ }
2705
+ await rename5(partPath, outPath);
2706
+ await removeJournal(jpath);
2707
+ return { status, bytesWritten };
2708
+ }
2709
+ async function cmdPull(rt, id, opts) {
2710
+ const mk = await rt.resolveMk(opts);
2711
+ const backend = await makeBackend(rt, opts.local);
2712
+ if (opts.recursive) {
2713
+ const startDir = normalizeCloudPath(id);
2714
+ const bundles = await walkBundlesUnder(backend, startDir);
2715
+ if (bundles.length === 0) {
2716
+ info(`\uFF08\u7A7A\uFF09\u4E91\u7AEF\u76EE\u5F55\u4E0B\u65E0\u8D44\u6E90\uFF1A${startDir}`);
2717
+ return;
2718
+ }
2719
+ const contentKey2 = deriveContentKey(mk);
2720
+ let restored = 0;
2721
+ let skipped = 0;
2722
+ for (const b of bundles) {
2723
+ const { meta: meta2 } = await readResourceMeta(mk, backend.bundleStore(b.id, b.dir));
2724
+ const outPath2 = downloadLocalPath(rt.fileRoot, b.dir, meta2.name);
2725
+ info(`\u4E0B\u8F7D\u8FD8\u539F\uFF1A${b.id} \u2192 ${outPath2}\uFF08${formatBytes(meta2.size)}\uFF09`);
2726
+ const r2 = await pullOneBundle(rt, backend, mk, contentKey2, b.id, b.dir, outPath2, opts);
2727
+ if (r2.status === "skipped-dup") skipped++;
2728
+ else if (r2.status === "locked") {
2729
+ } else {
2730
+ restored++;
2731
+ ok(
2732
+ `\u5DF2\u8FD8\u539F${r2.status === "resumed" ? "\uFF08\u7EED\u4F20\uFF09" : ""} ${formatBytes(r2.bytesWritten)} \u2192 ${outPath2}`
2733
+ );
2734
+ }
2735
+ }
2736
+ ok(
2737
+ `\u6574\u6811\u8FD8\u539F\u5B8C\u6210\uFF1A\u8FD8\u539F ${restored}\uFF0C\u8DF3\u8FC7\uFF08\u5DF2\u5B58\u5728\uFF09${skipped}\uFF0C\u5171 ${bundles.length} \u4E2A \u2192 ${rt.fileRoot}`
2738
+ );
2739
+ return;
2740
+ }
2741
+ const { id: fullId, dir } = await resolveBundle(rt, id, opts.local);
2742
+ const { meta } = await readResourceMeta(mk, backend.bundleStore(fullId, dir));
2743
+ const outPath = downloadLocalPath(rt.fileRoot, opts.out ?? dir, meta.name);
2744
+ const contentKey = deriveContentKey(mk);
2745
+ info(`\u4E0B\u8F7D\u8FD8\u539F\uFF1A${fullId} \u2192 ${outPath}\uFF08${formatBytes(meta.size)}\uFF09`);
2746
+ const r = await pullOneBundle(rt, backend, mk, contentKey, fullId, dir, outPath, opts);
2747
+ if (r.status === "skipped-dup" || r.status === "locked") return;
2748
+ ok(
2749
+ `\u5DF2\u8FD8\u539F${r.status === "resumed" ? "\uFF08\u7EED\u4F20\uFF09" : ""} ${formatBytes(r.bytesWritten)} \u2192 ${outPath}`
2750
+ );
2751
+ }
2752
+ async function walkBundlesUnder(backend, startDir) {
2753
+ const result = [];
2754
+ const walk = async (dir) => {
2755
+ const listing = await backend.listDir(dir);
2756
+ result.push(...listing.bundles);
2757
+ for (const d of listing.dirs) {
2758
+ await walk(dir === "/" ? `/${d}` : `${dir}/${d}`);
2759
+ }
2760
+ };
2761
+ await walk(startDir);
2762
+ return result;
2763
+ }
2764
+ async function listBundles(rt, local, backend) {
2765
+ const b = backend ?? await makeBackend(rt, local);
2766
+ return walkBundlesUnder(b, "/");
2767
+ }
2768
+ async function resolveBundle(rt, idOrPrefix, local, backend) {
2769
+ const bundles = await listBundles(rt, local, backend);
2770
+ if (/^[0-9a-f]{32}$/.test(idOrPrefix)) {
2771
+ const found = bundles.find((b) => b.id === idOrPrefix);
2772
+ if (!found) throw new BizhouError("INVALID_ARG", `\u627E\u4E0D\u5230\u8D44\u6E90\uFF1A${idOrPrefix}`);
2773
+ return found;
2774
+ }
2775
+ const matches = bundles.filter((b) => b.id.startsWith(idOrPrefix.toLowerCase()));
2776
+ if (matches.length === 0) throw new BizhouError("INVALID_ARG", `\u627E\u4E0D\u5230\u8D44\u6E90\uFF1A${idOrPrefix}`);
2777
+ if (matches.length > 1) {
2778
+ throw new BizhouError(
2779
+ "INVALID_ARG",
2780
+ `ID \u524D\u7F00 ${idOrPrefix} \u4E0D\u552F\u4E00\uFF08\u5339\u914D ${matches.length} \u4E2A\uFF09\uFF0C\u8BF7\u7ED9\u66F4\u957F\u524D\u7F00`
2781
+ );
2782
+ }
2783
+ return matches[0];
2784
+ }
2785
+ async function resolveBundleOrNull(rt, idOrPrefix, local, backend) {
2786
+ const bundles = await listBundles(rt, local, backend);
2787
+ const full = /^[0-9a-f]{32}$/.test(idOrPrefix) ? bundles.filter((b) => b.id === idOrPrefix) : bundles.filter((b) => b.id.startsWith(idOrPrefix.toLowerCase()));
2788
+ if (full.length === 0) return null;
2789
+ if (full.length > 1) {
2790
+ throw new BizhouError(
2791
+ "INVALID_ARG",
2792
+ `ID \u524D\u7F00 ${idOrPrefix} \u4E0D\u552F\u4E00\uFF08\u5339\u914D ${full.length} \u4E2A\uFF09\uFF0C\u8BF7\u7ED9\u66F4\u957F\u524D\u7F00`
2793
+ );
2794
+ }
2795
+ return full[0];
2796
+ }
2797
+ async function cmdMkdir(rt, cloudDir, opts) {
2798
+ const backend = await makeBackend(rt, opts.local);
2799
+ await backend.mkdir(normalizeCloudPath(cloudDir));
2800
+ ok(`\u5DF2\u521B\u5EFA\u76EE\u5F55 ${normalizeCloudPath(cloudDir)}`);
2801
+ }
2802
+ async function cmdLs(rt, cloudDir, opts) {
2803
+ const mk = await rt.resolveMk(opts);
2804
+ const backend = await makeBackend(rt, opts.local);
2805
+ const start = normalizeCloudPath(cloudDir ?? "/");
2806
+ let printedAny = false;
2807
+ const walk = async (dir, depth) => {
2808
+ const listing = await backend.listDir(dir);
2809
+ for (const d of listing.dirs.sort()) {
2810
+ printedAny = true;
2811
+ out(`${" ".repeat(depth)}${c.cyan(`${d}/`)}`);
2812
+ if (opts.recursive) await walk(dir === "/" ? `/${d}` : `${dir}/${d}`, depth + 1);
2813
+ }
2814
+ for (const b of listing.bundles) {
2815
+ printedAny = true;
2816
+ try {
2817
+ const store = backend.bundleStore(b.id, dir);
2818
+ const { meta } = await readResourceMeta(mk, store);
2819
+ out(
2820
+ `${" ".repeat(depth)}${c.dim(b.id.slice(0, 12))} ${formatBytes(meta.size).padStart(10)} ${meta.name}`
2821
+ );
2822
+ } catch {
2823
+ out(`${" ".repeat(depth)}${c.dim(b.id.slice(0, 12))} ${c.yellow("(\u65E0\u6CD5\u8BFB\u53D6)")}`);
2824
+ }
2825
+ }
2826
+ };
2827
+ await walk(start, 0);
2828
+ if (!printedAny) info("\uFF08\u7A7A\uFF09");
2829
+ }
2830
+ async function cmdInfo(rt, id, opts) {
2831
+ const mk = await rt.resolveMk(opts);
2832
+ const { id: fullId, dir } = await resolveBundle(rt, id, opts.local);
2833
+ const backend = await makeBackend(rt, opts.local);
2834
+ const store = backend.bundleStore(fullId, dir);
2835
+ const { manifest, meta } = await readResourceMeta(mk, store);
2836
+ out(`\u8D44\u6E90 ID : ${manifest.bundleId}`);
2837
+ out(`\u539F\u6587\u4EF6\u540D : ${meta.name}`);
2838
+ out(`\u5927\u5C0F : ${formatBytes(meta.size)}`);
2839
+ out(`\u521B\u5EFA\u65F6\u95F4 : ${manifest.createdAt}`);
2840
+ out(`\u52A0\u5BC6\u7B97\u6CD5 : ${manifest.cipher}`);
2841
+ out(`\u538B\u7F29 : ${manifest.compression}`);
2842
+ out(`\u5206\u7247 : ${manifest.chunks.length} \u7247 \xD7 ${formatBytes(manifest.chunkSize)}`);
2843
+ if (meta.mtime) out(`\u539F\u4FEE\u6539\u65F6\u95F4: ${meta.mtime}`);
2844
+ }
2845
+ async function cmdRm(rt, src, opts) {
2846
+ const backend = await makeBackend(rt, opts.local);
2847
+ const b = await resolveBundleOrNull(rt, src, opts.local, backend);
2848
+ const isBundle = b !== null;
2849
+ const path = b ? joinCloudPath(b.dir, bundleDirName(b.id)) : normalizeCloudPath(src);
2850
+ if (!isBundle && !opts.yes) {
2851
+ throw new BizhouError("INVALID_ARG", `\u5220\u9664\u76EE\u5F55 ${path} \u53CA\u5176\u5185\u5BB9\u5C06\u8FDB\u56DE\u6536\u7AD9\uFF0C\u8BF7\u52A0 --yes \u786E\u8BA4`);
2852
+ }
2853
+ await backend.trashPath(path, (/* @__PURE__ */ new Date()).toISOString());
2854
+ if (isBundle) await invalidateManifest(rt.paths.dir, b.id);
2855
+ ok(`\u5DF2\u5220\u9664\u5230\u56DE\u6536\u7AD9\uFF1A${isBundle ? b.id : path}`);
2856
+ }
2857
+ async function cmdTrash(rt, sub, arg, opts) {
2858
+ const backend = await makeBackend(rt, opts.local);
2859
+ if (!sub || sub === "list") {
2860
+ const entries = await backend.listTrash();
2861
+ if (entries.length === 0) {
2862
+ info("\uFF08\u56DE\u6536\u7AD9\u4E3A\u7A7A\uFF09");
2863
+ return;
2864
+ }
2865
+ for (const e of entries) {
2866
+ out(`${e.entryId} ${e.name} ${e.originalPath} ${e.deletedAt}`);
2867
+ }
2868
+ return;
2869
+ }
2870
+ if (sub === "restore") {
2871
+ if (!arg) throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz trash restore <entryId>");
2872
+ await backend.restoreTrash(arg);
2873
+ ok(`\u5DF2\u4ECE\u56DE\u6536\u7AD9\u6062\u590D\uFF1A${arg}`);
2874
+ return;
2875
+ }
2876
+ if (sub === "rm") {
2877
+ if (!arg) throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz trash rm <entryId>");
2878
+ const entries = await backend.listTrash();
2879
+ const entry = entries.find((e) => e.entryId === arg);
2880
+ await backend.deleteTrash(arg);
2881
+ if (entry?.name.endsWith(BUNDLE_SUFFIX)) {
2882
+ await invalidateManifest(rt.paths.dir, entry.name.slice(0, -BUNDLE_SUFFIX.length));
2883
+ }
2884
+ ok(`\u5DF2\u4ECE\u56DE\u6536\u7AD9\u6C38\u4E45\u5220\u9664\uFF1A${arg}`);
2885
+ return;
2886
+ }
2887
+ if (sub === "clear") {
2888
+ await backend.clearTrash();
2889
+ await rm6(join10(rt.paths.dir, ".cache", "manifests"), { recursive: true, force: true });
2890
+ ok("\u56DE\u6536\u7AD9\u5DF2\u6E05\u7A7A");
2891
+ return;
2892
+ }
2893
+ throw new BizhouError("INVALID_ARG", `\u672A\u77E5\u5B50\u547D\u4EE4\uFF1Atrash ${sub}`);
2894
+ }
2895
+ async function cmdMv(rt, src, dstDir, opts) {
2896
+ const backend = await makeBackend(rt, opts.local);
2897
+ const b = await resolveBundleOrNull(rt, src, opts.local, backend);
2898
+ const srcPath = b ? joinCloudPath(b.dir, bundleDirName(b.id)) : normalizeCloudPath(src);
2899
+ const dst = normalizeCloudPath(dstDir);
2900
+ await backend.mkdir(dst);
2901
+ await backend.move(srcPath, dst);
2902
+ ok(`\u5DF2\u79FB\u52A8 ${srcPath} \u2192 ${dst}`);
2903
+ }
2904
+ async function cmdCp(rt, src, dstDir, opts) {
2905
+ const backend = await makeBackend(rt, opts.local);
2906
+ const b = await resolveBundleOrNull(rt, src, opts.local, backend);
2907
+ const isBundle = b !== null;
2908
+ const srcPath = b ? joinCloudPath(b.dir, bundleDirName(b.id)) : normalizeCloudPath(src);
2909
+ if (!isBundle && !opts.recursive) {
2910
+ throw new BizhouError("INVALID_ARG", "\u590D\u5236\u76EE\u5F55\u9700 -r");
2911
+ }
2912
+ const dst = normalizeCloudPath(dstDir);
2913
+ await backend.mkdir(dst);
2914
+ await backend.copy(srcPath, dst);
2915
+ ok(`\u5DF2\u590D\u5236 ${srcPath} \u2192 ${dst}`);
2916
+ }
2917
+ async function cmdRename(rt, src, newName, opts) {
2918
+ const backend = await makeBackend(rt, opts.local);
2919
+ const b = await resolveBundleOrNull(rt, src, opts.local, backend);
2920
+ if (b) {
2921
+ const mk = await rt.resolveMk(opts);
2922
+ const bundleStore = backend.bundleStore(b.id, b.dir);
2923
+ await renameResource(mk, bundleStore, newName);
2924
+ await invalidateManifest(rt.paths.dir, b.id);
2925
+ ok(`\u5DF2\u6539\u540D ${b.id} \u2192 ${newName}`);
2926
+ return;
2927
+ }
2928
+ const srcPath = normalizeCloudPath(src);
2929
+ await backend.rename(srcPath, newName);
2930
+ ok(`\u5DF2\u6539\u540D ${srcPath} \u2192 ${newName}`);
2931
+ }
2932
+ async function cmdShare(rt, id, opts) {
2933
+ if (opts.sevenz) {
2934
+ return cmdShare7z(rt, id, opts);
2935
+ }
2936
+ const mk = await rt.resolveMk(opts);
2937
+ const { id: fullId, dir } = await resolveBundle(rt, id, opts.local);
2938
+ const backend = await makeBackend(rt, opts.local);
2939
+ const store = backend.bundleStore(fullId, dir);
2940
+ const manifest = parseManifest(await store.getManifest());
2941
+ const dek = unwrapDek(mk, manifest.wrappedKey);
2942
+ const shareCode = SHARE_PREFIX + groupBase32(base32Encode(dek));
2943
+ info("\u5206\u4EAB\u7801\uFF08\u8FDE\u540C\u4E91\u7AEF\u6587\u4EF6\u5939\u94FE\u63A5\u4EA4\u7ED9\u5BF9\u65B9\uFF1B\u5BF9\u65B9\u7528\u655D\u5E1A + \u5206\u4EAB\u7801\u89E3\u5BC6\u8BE5\u8D44\u6E90\uFF09\uFF1A");
2944
+ out(c.bold(shareCode));
2945
+ warn("\u5206\u4EAB\u7801\u7B49\u540C\u4E8E\u8BE5\u8D44\u6E90\u7684\u89E3\u5BC6\u94A5\u5319\uFF0C\u8BF7\u901A\u8FC7\u5B89\u5168\u6E20\u9053\u4F20\u9012\u3002");
2946
+ }
2947
+ async function cmdShare7z(rt, id, opts) {
2948
+ const bin = await findSevenZip();
2949
+ if (!bin) {
2950
+ throw new BizhouError(
2951
+ "INVALID_ARG",
2952
+ "\u672A\u627E\u5230 7z \u53EF\u6267\u884C\u6587\u4EF6\uFF1A\u8BF7\u5B89\u88C5 p7zip\uFF08brew install p7zip / apt install p7zip-full\uFF09\u6216\u8BBE\u7F6E BIZHOU_7Z_BIN"
2953
+ );
2954
+ }
2955
+ const mk = await rt.resolveMk(opts);
2956
+ const { id: fullId, dir } = await resolveBundle(rt, id, opts.local);
2957
+ const backend = await makeBackend(rt, opts.local);
2958
+ const store = backend.bundleStore(fullId, dir);
2959
+ const { meta } = await readResourceMeta(mk, store);
2960
+ const work = await mkdtemp2(join10(tmpdir2(), "bizhou-7z-"));
2961
+ try {
2962
+ const restored = join10(work, meta.name);
2963
+ await unpackResource({ mk, store, outPath: restored });
2964
+ const outArchive = join10(opts.out ?? ".", `${meta.name}.7z`);
2965
+ await mkdir8(dirname4(outArchive), { recursive: true });
2966
+ const pw = process.env.BIZHOU_SHARE_PASSWORD ?? await readPassword("\u4E3A 7z \u5305\u8BBE\u7F6E\u5BC6\u7801: ");
2967
+ if (!pw) throw new BizhouError("INVALID_ARG", "7z \u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A");
2968
+ await sevenZipArchive(bin, outArchive, [restored], pw);
2969
+ ok(`\u5DF2\u5BFC\u51FA\u5934\u90E8\u52A0\u5BC6 7z\uFF1A${outArchive}\uFF08\u5BF9\u65B9\u7528 7-Zip/Keka/p7zip + \u5BC6\u7801\u89E3\u5F00\uFF09`);
2970
+ } finally {
2971
+ await rm6(work, { recursive: true, force: true });
2972
+ }
2973
+ }
2974
+ async function cmdPreview(rt, id, opts) {
2975
+ const mk = await rt.resolveMk(opts);
2976
+ const { id: fullId, dir } = await resolveBundle(rt, id, opts.local);
2977
+ const backend = await makeBackend(rt, opts.local);
2978
+ const store = backend.bundleStore(fullId, dir);
2979
+ const { kind, data } = await openPreview(mk, store);
2980
+ if (kind === "text") {
2981
+ out(data.toString("utf8"));
2982
+ return;
2983
+ }
2984
+ const ext = kind === "audio" ? "mp3" : "jpg";
2985
+ const outPath = join10(opts.out ?? ".", `${fullId.slice(0, 12)}-preview.${ext}`);
2986
+ await mkdir8(dirname4(outPath), { recursive: true });
2987
+ await writeFile8(outPath, data);
2988
+ ok(`\u9884\u89C8\uFF08${kind}\uFF0C${formatBytes(data.length)}\uFF09\u5DF2\u4FDD\u5B58\uFF1A${outPath}`);
2989
+ }
2990
+
2991
+ // src/completion.ts
2992
+ var F = (name, takesValue = false, valueArg) => ({
2993
+ name,
2994
+ takesValue,
2995
+ ...valueArg ? { valueArg } : {}
2996
+ });
2997
+ var GLOBAL_FLAGS = [
2998
+ F("--help"),
2999
+ F("--version"),
3000
+ F("--local", true, { kind: "dir" }),
3001
+ F("--password-stdin")
3002
+ ];
3003
+ var FILE = { kind: "file" };
3004
+ var DIR = { kind: "dir" };
3005
+ var CLOUD_ID = { kind: "cloud", ctx: "bundle-id" };
3006
+ var CLOUD_DIR = { kind: "cloud", ctx: "cloud-dir" };
3007
+ var COMMANDS = [
3008
+ { name: "init", flags: [F("--force")], args: [] },
3009
+ { name: "unlock", flags: [F("--ttl", true)], args: [] },
3010
+ { name: "lock", flags: [], args: [] },
3011
+ { name: "passwd", flags: [], args: [] },
3012
+ { name: "recover", flags: [], args: [] },
3013
+ {
3014
+ name: "login",
3015
+ flags: [F("--name", true), F("--device"), F("--port", true)],
3016
+ args: []
3017
+ },
3018
+ { name: "logout", flags: [], args: [] },
3019
+ {
3020
+ name: "account",
3021
+ flags: [],
3022
+ args: [{ kind: "subcommand", names: ["list", "use", "add"] }],
3023
+ subArgs: { use: [{ kind: "dynamic", ctx: "account" }], add: [{ kind: "none" }], list: [] }
3024
+ },
3025
+ {
3026
+ name: "push",
3027
+ flags: [
3028
+ F("--to", true),
3029
+ F("--chunk", true),
3030
+ F("--compress"),
3031
+ F("--no-split"),
3032
+ F("--name", true),
3033
+ F("--preview"),
3034
+ F("--force"),
3035
+ F("--concurrency", true),
3036
+ F("--recursive")
3037
+ ],
3038
+ args: [FILE]
3039
+ },
3040
+ {
3041
+ name: "pull",
3042
+ flags: [F("--out", true, DIR), F("--recursive"), F("--force")],
3043
+ args: [CLOUD_ID]
3044
+ },
3045
+ { name: "mkdir", flags: [], args: [CLOUD_DIR] },
3046
+ { name: "ls", flags: [F("--recursive")], args: [CLOUD_DIR] },
3047
+ { name: "info", flags: [], args: [CLOUD_ID] },
3048
+ { name: "rm", flags: [F("--yes")], args: [CLOUD_ID] },
3049
+ {
3050
+ name: "trash",
3051
+ flags: [],
3052
+ args: [{ kind: "subcommand", names: ["list", "restore", "rm", "clear"] }],
3053
+ // restore/rm 的回收站条目 id 需后端(本轮不补,走 none)
3054
+ subArgs: { restore: [{ kind: "none" }], rm: [{ kind: "none" }], list: [], clear: [] }
3055
+ },
3056
+ { name: "mv", flags: [], args: [CLOUD_ID, CLOUD_DIR] },
3057
+ { name: "cp", flags: [F("--recursive")], args: [CLOUD_ID, CLOUD_DIR] },
3058
+ { name: "rename", flags: [], args: [CLOUD_ID, { kind: "none" }] },
3059
+ {
3060
+ name: "share",
3061
+ // 注:cmdShare 当前只读 --code/--7z/--out(不含 --ttl);补全须贴合实际,避免提示无效 flag。
3062
+ flags: [F("--code"), F("--7z"), F("--out", true, DIR)],
3063
+ args: [CLOUD_ID]
3064
+ },
3065
+ { name: "preview", flags: [F("--out", true, DIR)], args: [CLOUD_ID] },
3066
+ {
3067
+ name: "backup",
3068
+ flags: [F("--to", true)],
3069
+ args: [{ kind: "subcommand", names: ["add", "list", "rm", "run"] }],
3070
+ subArgs: {
3071
+ add: [DIR],
3072
+ list: [],
3073
+ rm: [{ kind: "dynamic", ctx: "backup-id" }],
3074
+ run: [{ kind: "dynamic", ctx: "backup-id" }]
3075
+ }
3076
+ },
3077
+ { name: "daemon", flags: [], args: [] },
3078
+ { name: "completion", flags: [], args: [{ kind: "dynamic", ctx: "shell" }] }
3079
+ ];
3080
+ function allCommandNames() {
3081
+ return COMMANDS.map((c2) => c2.name).join(" ");
3082
+ }
3083
+ function flagsFor(name) {
3084
+ const c2 = COMMANDS.find((x) => x.name === name);
3085
+ const flags = [...c2?.flags ?? [], ...GLOBAL_FLAGS].map((f) => f.name);
3086
+ return [...new Set(flags)].join(" ");
3087
+ }
3088
+ function subNamesFor(name) {
3089
+ const a = COMMANDS.find((x) => x.name === name)?.args[0];
3090
+ return a?.kind === "subcommand" ? a.names.join(" ") : void 0;
3091
+ }
3092
+ function subNameList(name) {
3093
+ const a = COMMANDS.find((x) => x.name === name)?.args[0];
3094
+ return a?.kind === "subcommand" ? a.names : [];
3095
+ }
3096
+ function dynamicCtxFor(cmd, sub) {
3097
+ const c2 = COMMANDS.find((x) => x.name === cmd);
3098
+ if (!c2) return void 0;
3099
+ const arg = sub ? c2.subArgs?.[sub]?.[0] : c2.args[0];
3100
+ if (!arg) return void 0;
3101
+ if (arg.kind === "dynamic") return { kind: "dynamic", ctx: arg.ctx };
3102
+ if (arg.kind === "file") return { kind: "file" };
3103
+ if (arg.kind === "dir") return { kind: "dir" };
3104
+ return void 0;
3105
+ }
3106
+ function genBash() {
3107
+ const cmds = allCommandNames();
3108
+ const perCmd = COMMANDS.map((c2) => {
3109
+ const subs = subNamesFor(c2.name);
3110
+ const dyn = dynamicCtxFor(c2.name);
3111
+ const lines = [];
3112
+ if (subs) {
3113
+ lines.push(
3114
+ ` if [ "$cword" -eq 2 ]; then COMPREPLY=( $(compgen -W "${subs} ${flagsFor(c2.name)}" -- "$cur") ); return; fi`
3115
+ );
3116
+ for (const sub of subNameList(c2.name)) {
3117
+ const d = dynamicCtxFor(c2.name, sub);
3118
+ if (d?.kind === "dynamic")
3119
+ lines.push(
3120
+ ` if [ "\${words[2]}" = "${sub}" ]; then COMPREPLY=( $(compgen -W "$(bz __complete ${d.ctx} "$cur")" -- "$cur") ); return; fi`
3121
+ );
3122
+ else if (d?.kind === "dir")
3123
+ lines.push(` if [ "\${words[2]}" = "${sub}" ]; then _filedir -d; return; fi`);
3124
+ else if (d?.kind === "file")
3125
+ lines.push(` if [ "\${words[2]}" = "${sub}" ]; then _filedir; return; fi`);
3126
+ }
3127
+ } else if (dyn?.kind === "dynamic") {
3128
+ lines.push(
3129
+ ` COMPREPLY=( $(compgen -W "$(bz __complete ${dyn.ctx} "$cur")" -- "$cur") ); return;`
3130
+ );
3131
+ } else if (dyn?.kind === "file") {
3132
+ lines.push(` _filedir; return;`);
3133
+ } else if (dyn?.kind === "dir") {
3134
+ lines.push(` _filedir -d; return;`);
3135
+ }
3136
+ lines.push(` COMPREPLY=( $(compgen -W "${flagsFor(c2.name)}" -- "$cur") ); return;`);
3137
+ return ` ${c2.name})
3138
+ ${lines.join("\n")}
3139
+ ;;`;
3140
+ }).join("\n");
3141
+ return `# bash completion for bz \u2014\u2014 \u5B89\u88C5\uFF1Aeval "$(bz completion bash)"\uFF08\u5199\u5165 ~/.bashrc\uFF09
3142
+ _bz() {
3143
+ local cur cword words
3144
+ _get_comp_words_by_ref -n : cur words cword 2>/dev/null || { cur="\${COMP_WORDS[COMP_CWORD]}"; words=("\${COMP_WORDS[@]}"); cword=$COMP_CWORD; }
3145
+ if [ "$cword" -eq 1 ]; then
3146
+ COMPREPLY=( $(compgen -W "${cmds}" -- "$cur") ); return
3147
+ fi
3148
+ case "\${words[1]}" in
3149
+ ${perCmd}
3150
+ esac
3151
+ }
3152
+ complete -F _bz bz
3153
+ `;
3154
+ }
3155
+ function genZsh() {
3156
+ const cmds = allCommandNames();
3157
+ const perCmd = COMMANDS.map((c2) => {
3158
+ const subs = subNamesFor(c2.name);
3159
+ const dyn = dynamicCtxFor(c2.name);
3160
+ const body = [];
3161
+ if (subs) {
3162
+ body.push(` if (( CURRENT == 3 )); then compadd ${subs}; return; fi`);
3163
+ for (const sub of subNameList(c2.name)) {
3164
+ const d = dynamicCtxFor(c2.name, sub);
3165
+ if (d?.kind === "dynamic")
3166
+ body.push(
3167
+ ` [[ "\${words[3]}" == "${sub}" ]] && { compadd \${(f)"$(bz __complete ${d.ctx})"}; return; }`
3168
+ );
3169
+ else if (d?.kind === "dir")
3170
+ body.push(` [[ "\${words[3]}" == "${sub}" ]] && { _files -/; return; }`);
3171
+ else if (d?.kind === "file")
3172
+ body.push(` [[ "\${words[3]}" == "${sub}" ]] && { _files; return; }`);
3173
+ }
3174
+ } else if (dyn?.kind === "dynamic") {
3175
+ body.push(` compadd \${(f)"$(bz __complete ${dyn.ctx})"}; return;`);
3176
+ } else if (dyn?.kind === "file") {
3177
+ body.push(` _files; return;`);
3178
+ } else if (dyn?.kind === "dir") {
3179
+ body.push(` _files -/; return;`);
3180
+ }
3181
+ body.push(` compadd ${flagsFor(c2.name)};`);
3182
+ return ` ${c2.name})
3183
+ ${body.join("\n")}
3184
+ ;;`;
3185
+ }).join("\n");
3186
+ return `#compdef bz
3187
+ # zsh completion for bz \u2014\u2014 \u5B89\u88C5\uFF1Abz completion zsh > "\${fpath[1]}/_bz"\uFF08\u6216 eval\uFF09
3188
+ _bz() {
3189
+ # $words/$CURRENT \u7531 zsh \u8865\u5168 widget \u63D0\u4F9B\uFF081-\u7D22\u5F15\uFF1Awords[1]=bz words[2]=command\uFF09
3190
+ # \u5207\u52FF local -a words \u91CD\u58F0\u660E\u2014\u2014\u4F1A\u5148\u63A9\u853D\u518D\u8D4B\u503C\u6210\u7A7A\u6570\u7EC4\uFF0C\u4EE4\u4E0B\u65B9 case \u5168\u90E8\u843D\u7A7A\u3002
3191
+ if (( CURRENT == 2 )); then
3192
+ compadd ${cmds}; return
3193
+ fi
3194
+ case "\${words[2]}" in
3195
+ ${perCmd}
3196
+ esac
3197
+ }
3198
+ compdef _bz bz
3199
+ `;
3200
+ }
3201
+ function psArgKindOf(arg) {
3202
+ if (!arg) return void 0;
3203
+ if (arg.kind === "file") return "file";
3204
+ if (arg.kind === "dir") return "dir";
3205
+ if (arg.kind === "dynamic") return "dynamic";
3206
+ return void 0;
3207
+ }
3208
+ function genPowerShell() {
3209
+ const cmds = COMMANDS.map((c2) => `'${c2.name}'`).join(", ");
3210
+ const table = COMMANDS.map((c2) => {
3211
+ const flagsArr = flagsFor(c2.name).split(" ").filter(Boolean).map((f) => `'${f}'`).join(", ");
3212
+ const parts = [`flags = @(${flagsArr})`];
3213
+ const subs = subNamesFor(c2.name);
3214
+ if (subs) {
3215
+ parts.push(
3216
+ `subs = @(${subs.split(" ").map((s) => `'${s}'`).join(", ")})`
3217
+ );
3218
+ const subKindEntries = [];
3219
+ const subCtxEntries = [];
3220
+ for (const sub of subNameList(c2.name)) {
3221
+ const arg = c2.subArgs?.[sub]?.[0];
3222
+ const k = psArgKindOf(arg);
3223
+ if (k) subKindEntries.push(`'${sub}'='${k}'`);
3224
+ if (k === "dynamic" && arg?.kind === "dynamic") subCtxEntries.push(`'${sub}'='${arg.ctx}'`);
3225
+ }
3226
+ if (subKindEntries.length) parts.push(`subKind = @{${subKindEntries.join("; ")}}`);
3227
+ if (subCtxEntries.length) parts.push(`subCtx = @{${subCtxEntries.join("; ")}}`);
3228
+ } else {
3229
+ const arg = c2.args[0];
3230
+ const k = psArgKindOf(arg);
3231
+ if (k) parts.push(`argKind = '${k}'`);
3232
+ if (k === "dynamic" && arg?.kind === "dynamic") parts.push(`ctx = '${arg.ctx}'`);
3233
+ }
3234
+ return ` '${c2.name}' = @{ ${parts.join("; ")} }`;
3235
+ }).join("\n");
3236
+ return `# PowerShell completion for bz \u2014\u2014 \u5B89\u88C5\uFF1Abz completion powershell | Out-String | Invoke-Expression\uFF08\u5199\u5165 $PROFILE\uFF09
3237
+ # \u6CE8\uFF1ARegister-ArgumentCompleter -Native \u4F1A\u6291\u5236 PowerShell \u9ED8\u8BA4\u7684\u8DEF\u5F84\u8865\u5168\uFF0C
3238
+ # \u56E0\u6B64 file/dir \u4F4D\u7F6E\u53C2\u6570\u987B\u7531\u811A\u672C\u5757\u81EA\u5DF1\u8C03\u7528 CompleteFilename \u751F\u6210\u5019\u9009\u3002
3239
+ $script:BzSpec = @{
3240
+ ${table}
3241
+ }
3242
+ Register-ArgumentCompleter -Native -CommandName bz -ScriptBlock {
3243
+ param($wordToComplete, $commandAst, $cursorPosition)
3244
+ $tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
3245
+ # \u53BB\u6389\u547D\u4EE4\u540D 'bz'\uFF1B\u82E5\u672B\u5C3E token \u5C31\u662F\u6B63\u5728\u8865\u5168\u7684\u5F53\u524D\u8BCD\uFF0C\u5148\u5254\u9664\u2014\u2014
3246
+ # \u8FD9\u6837 $rest.Count \u624D\u80FD\u7A33\u5B9A\u8868\u793A"\u5DF2\u7ECF\u5B8C\u6574\u8F93\u5165\u7684\u53C2\u6570\u4E2A\u6570"\uFF0C
3247
+ # \u65E0\u8BBA\u7528\u6237\u662F\u521A\u6572\u5B8C\u4E00\u4E2A\u7A7A\u683C\uFF08wordToComplete \u4E3A\u7A7A\uFF09\u8FD8\u662F\u6B63\u5728\u6253\u534A\u4E2A\u8BCD\uFF08\u5982 'u'\uFF09\u3002
3248
+ $rest = @($tokens | Select-Object -Skip 1)
3249
+ if ($rest.Count -gt 0 -and $rest[$rest.Count - 1] -eq $wordToComplete) {
3250
+ $rest = @($rest | Select-Object -First ($rest.Count - 1))
3251
+ }
3252
+ $cands = @()
3253
+ if ($rest.Count -eq 0) {
3254
+ $cands = @(${cmds})
3255
+ } else {
3256
+ $cmd = $rest[0]
3257
+ $spec = $script:BzSpec[$cmd]
3258
+ if ($spec) {
3259
+ # flag \u8865\u5168\uFF1A\u4EFB\u610F\u4F4D\u7F6E\uFF0C\u53EA\u8981\u5F53\u524D\u8BCD\u4EE5 - \u5F00\u5934
3260
+ if ($wordToComplete -like '-*') {
3261
+ $cands = $spec.flags
3262
+ } elseif ($rest.Count -eq 1 -and $spec.subs) {
3263
+ $cands = $spec.subs
3264
+ } elseif ($rest.Count -eq 1 -and $spec.argKind -eq 'dynamic') {
3265
+ $cands = @(bz __complete $spec.ctx)
3266
+ } elseif ($rest.Count -eq 1 -and ($spec.argKind -eq 'file' -or $spec.argKind -eq 'dir')) {
3267
+ return [System.Management.Automation.CompletionCompleters]::CompleteFilename($wordToComplete)
3268
+ } elseif ($spec.subs -and $rest.Count -ge 2) {
3269
+ $sub = $rest[1]
3270
+ $subKind = $spec.subKind[$sub]
3271
+ if ($subKind -eq 'dynamic') {
3272
+ $cands = @(bz __complete $spec.subCtx[$sub])
3273
+ } elseif ($subKind -eq 'file' -or $subKind -eq 'dir') {
3274
+ return [System.Management.Automation.CompletionCompleters]::CompleteFilename($wordToComplete)
3275
+ }
3276
+ }
3277
+ }
3278
+ }
3279
+ $cands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
3280
+ }
3281
+ `;
3282
+ }
3283
+ function cmdCompletion(shell) {
3284
+ switch (shell) {
3285
+ case "bash":
3286
+ process.stdout.write(genBash());
3287
+ break;
3288
+ case "zsh":
3289
+ process.stdout.write(genZsh());
3290
+ break;
3291
+ case "powershell":
3292
+ process.stdout.write(genPowerShell());
3293
+ break;
3294
+ default:
3295
+ throw new BizhouError(
3296
+ "INVALID_ARG",
3297
+ `\u7528\u6CD5\uFF1Abz completion <bash|zsh|powershell>\uFF08\u4E0D\u652F\u6301\uFF1A${shell ?? "\uFF08\u7A7A\uFF09"}\uFF09`
3298
+ );
3299
+ }
3300
+ }
3301
+ async function cmdComplete(rt, ctx, prefix) {
3302
+ let candidates = [];
3303
+ try {
3304
+ switch (ctx) {
3305
+ case "shell":
3306
+ candidates = ["bash", "zsh", "powershell"];
3307
+ break;
3308
+ case "backup-id":
3309
+ candidates = (await readBackups(rt.paths.dir)).map((j) => j.id);
3310
+ break;
3311
+ case "account":
3312
+ candidates = (await rt.accounts.listAccounts()).names;
3313
+ break;
3314
+ default:
3315
+ candidates = [];
3316
+ }
3317
+ } catch {
3318
+ candidates = [];
3319
+ }
3320
+ const p = prefix ?? "";
3321
+ for (const c2 of candidates) {
3322
+ if (c2.startsWith(p)) process.stdout.write(`${c2}
3323
+ `);
3324
+ }
3325
+ }
3326
+
3327
+ // src/daemon.ts
3328
+ import { stat as stat2 } from "fs/promises";
3329
+ import { basename as basename4, dirname as dirname5, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
3330
+
3331
+ // src/watcher.ts
3332
+ import { watch } from "fs";
3333
+ import { readdir as readdir5 } from "fs/promises";
3334
+ import { join as join11 } from "path";
3335
+ function debounce(fn, ms) {
3336
+ let timer;
3337
+ let lastArgs;
3338
+ return {
3339
+ call: (...args) => {
3340
+ lastArgs = args;
3341
+ if (timer) clearTimeout(timer);
3342
+ timer = setTimeout(() => {
3343
+ timer = void 0;
3344
+ if (lastArgs) fn(...lastArgs);
3345
+ }, ms);
3346
+ },
3347
+ cancel: () => {
3348
+ if (timer) {
3349
+ clearTimeout(timer);
3350
+ timer = void 0;
3351
+ }
3352
+ }
3353
+ };
3354
+ }
3355
+ async function listDirsRecursive(root) {
3356
+ const dirs = [root];
3357
+ const walk = async (dir) => {
3358
+ let entries;
3359
+ try {
3360
+ entries = await readdir5(dir, { withFileTypes: true });
3361
+ } catch {
3362
+ return;
3363
+ }
3364
+ for (const e of entries) {
3365
+ if (e.isDirectory()) {
3366
+ const sub = join11(dir, e.name);
3367
+ dirs.push(sub);
3368
+ await walk(sub);
3369
+ }
3370
+ }
3371
+ };
3372
+ await walk(root);
3373
+ return dirs;
3374
+ }
3375
+ function watchRecursive(dir, onChange, opts) {
3376
+ const plat = opts.platform ?? process.platform;
3377
+ const d = debounce(onChange, opts.debounceMs);
3378
+ const watchers = [];
3379
+ let stopped = false;
3380
+ const safeWatch = (target) => {
3381
+ if (stopped) return;
3382
+ try {
3383
+ watchers.push(watch(target, () => d.call()));
3384
+ } catch {
3385
+ }
3386
+ };
3387
+ if (plat === "darwin" || plat === "win32") {
3388
+ try {
3389
+ watchers.push(watch(dir, { recursive: true }, () => d.call()));
3390
+ } catch {
3391
+ void listDirsRecursive(dir).then((dirs) => {
3392
+ for (const sub of dirs) safeWatch(sub);
3393
+ });
3394
+ }
3395
+ } else {
3396
+ void listDirsRecursive(dir).then((dirs) => {
3397
+ for (const sub of dirs) safeWatch(sub);
3398
+ });
3399
+ }
3400
+ return {
3401
+ stop() {
3402
+ stopped = true;
3403
+ d.cancel();
3404
+ for (const w of watchers) {
3405
+ try {
3406
+ w.close();
3407
+ } catch {
3408
+ }
3409
+ }
3410
+ }
3411
+ };
3412
+ }
3413
+
3414
+ // src/daemon.ts
3415
+ async function sweepJob(rt, backend, mk, contentKey, job, log) {
3416
+ const localDir = resolve4(job.localDir);
3417
+ const st = await stat2(localDir).catch(() => null);
3418
+ if (!st?.isDirectory()) {
3419
+ log(`\u8DF3\u8FC7\uFF08\u6E90\u76EE\u5F55\u4E0D\u5B58\u5728\uFF09\uFF1A${localDir}`);
3420
+ return { uploaded: 0, skipped: 0, failed: 0 };
3421
+ }
3422
+ const baseCloud = job.cloudDir ? normalizeCloudPath(job.cloudDir) : defaultUploadCloudDir(localDir + sep2, rt.fileRoot);
3423
+ const rootCloud = joinCloudPath(baseCloud, basename4(localDir));
3424
+ const files = await walkLocalFiles(localDir);
3425
+ let uploaded = 0;
3426
+ let skipped = 0;
3427
+ let failed = 0;
3428
+ for (const abs of files) {
3429
+ const relDir = dirname5(relative3(localDir, abs));
3430
+ const cloudDir = relDir === "." ? rootCloud : joinCloudPath(rootCloud, relDir);
3431
+ try {
3432
+ const r = await pushOneFile(rt, backend, mk, contentKey, abs, cloudDir, {});
3433
+ if (r.status === "skipped-dup") skipped++;
3434
+ else if (r.status === "locked") {
3435
+ } else {
3436
+ uploaded++;
3437
+ log(`\u5DF2\u5907\u4EFD\uFF1A${abs} \u2192 ${r.bundleId}`);
3438
+ }
3439
+ } catch (err) {
3440
+ failed++;
3441
+ log(`\u5931\u8D25\uFF08\u8DF3\u8FC7\u7EE7\u7EED\uFF09\uFF1A${abs} \u2014 ${err.message}`);
3442
+ }
3443
+ }
3444
+ return { uploaded, skipped, failed };
3445
+ }
3446
+ var SerialJobRunner = class {
3447
+ constructor(run) {
3448
+ this.run = run;
3449
+ }
3450
+ run;
3451
+ running = false;
3452
+ dirty = false;
3453
+ current = Promise.resolve();
3454
+ trigger() {
3455
+ if (this.running) {
3456
+ this.dirty = true;
3457
+ return;
3458
+ }
3459
+ this.running = true;
3460
+ this.current = this.loop();
3461
+ }
3462
+ async loop() {
3463
+ try {
3464
+ do {
3465
+ this.dirty = false;
3466
+ try {
3467
+ await this.run();
3468
+ } catch {
3469
+ }
3470
+ } while (this.dirty);
3471
+ } finally {
3472
+ this.running = false;
3473
+ }
3474
+ }
3475
+ /** 等当前(及补跑)结束。 */
3476
+ async drain() {
3477
+ await this.current;
3478
+ }
3479
+ };
3480
+ async function cmdBackup(rt, sub, arg, opts) {
3481
+ switch (sub) {
3482
+ case "add": {
3483
+ if (!arg) {
3484
+ throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz backup add <\u672C\u5730\u76EE\u5F55> [--to <\u4E91\u7AEF\u76EE\u5F55>]");
3485
+ }
3486
+ const abs = resolve4(arg);
3487
+ const st = await stat2(abs).catch(() => null);
3488
+ if (!st?.isDirectory()) throw new BizhouError("INVALID_ARG", `\u4E0D\u662F\u76EE\u5F55\uFF1A${abs}`);
3489
+ const cloudDir = opts.to ? normalizeCloudPath(opts.to) : void 0;
3490
+ const job = await addBackup(rt.paths.dir, {
3491
+ localDir: abs,
3492
+ ...cloudDir ? { cloudDir } : {},
3493
+ addedAt: new Date(rt.now()).toISOString()
3494
+ });
3495
+ ok(`\u5DF2\u6CE8\u518C\u5907\u4EFD\u4EFB\u52A1 ${job.id}\uFF1A${abs}${cloudDir ? ` \u2192 ${cloudDir}` : ""}`);
3496
+ return;
3497
+ }
3498
+ case "list": {
3499
+ const jobs = await readBackups(rt.paths.dir);
3500
+ if (jobs.length === 0) {
3501
+ info("\uFF08\u65E0\u5907\u4EFD\u4EFB\u52A1\uFF09\u6DFB\u52A0\uFF1Abz backup add <\u76EE\u5F55> [--to <\u4E91\u7AEF\u76EE\u5F55>]");
3502
+ return;
3503
+ }
3504
+ for (const j of jobs) {
3505
+ out(
3506
+ `${j.id} ${j.localDir}${j.cloudDir ? ` \u2192 ${j.cloudDir}` : "\uFF08\u955C\u50CF\uFF09"} \u4E0A\u6B21\uFF1A${j.lastBackupAt ?? "\u4ECE\u672A"}`
3507
+ );
3508
+ }
3509
+ return;
3510
+ }
3511
+ case "rm": {
3512
+ if (!arg) throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz backup rm <id>");
3513
+ const removed = await removeBackup(rt.paths.dir, arg);
3514
+ if (removed) ok(`\u5DF2\u5220\u9664\u5907\u4EFD\u4EFB\u52A1 ${arg}\uFF08\u4E91\u7AEF\u5DF2\u5907\u4EFD\u6570\u636E\u4E0D\u53D7\u5F71\u54CD\uFF09`);
3515
+ else warn(`\u672A\u627E\u5230\u5907\u4EFD\u4EFB\u52A1\uFF1A${arg}`);
3516
+ return;
3517
+ }
3518
+ case "run": {
3519
+ const jobs = await readBackups(rt.paths.dir);
3520
+ const targets = arg ? jobs.filter((j) => j.id === arg) : jobs;
3521
+ if (targets.length === 0) {
3522
+ warn(arg ? `\u672A\u627E\u5230\u5907\u4EFD\u4EFB\u52A1\uFF1A${arg}` : "\uFF08\u65E0\u5907\u4EFD\u4EFB\u52A1\uFF09");
3523
+ return;
3524
+ }
3525
+ const mk = await rt.resolveMk(opts);
3526
+ const contentKey = deriveContentKey(mk);
3527
+ const backend = await makeBackend(rt, opts.local);
3528
+ for (const job of targets) {
3529
+ info(`\u5907\u4EFD ${job.id}\uFF1A${job.localDir}`);
3530
+ const r = await sweepJob(rt, backend, mk, contentKey, job, (m) => info(m));
3531
+ await updateLastBackup(rt.paths.dir, job.id, new Date(rt.now()).toISOString());
3532
+ ok(`\u4EFB\u52A1 ${job.id} \u5B8C\u6210\uFF1A\u4E0A\u4F20 ${r.uploaded}\uFF0C\u8DF3\u8FC7 ${r.skipped}\uFF0C\u5931\u8D25 ${r.failed}`);
3533
+ }
3534
+ return;
3535
+ }
3536
+ default:
3537
+ throw new BizhouError("INVALID_ARG", `\u672A\u77E5\u5B50\u547D\u4EE4\uFF1Abackup ${sub}\uFF08\u7528 add/list/rm\uFF09`);
3538
+ }
3539
+ }
3540
+ async function cmdDaemon(rt, opts) {
3541
+ const jobs = await readBackups(rt.paths.dir);
3542
+ if (jobs.length === 0) {
3543
+ info("\u65E0\u5907\u4EFD\u4EFB\u52A1\uFF0C\u5148\u8FD0\u884C `bz backup add <\u76EE\u5F55>`\u3002");
3544
+ return;
3545
+ }
3546
+ const mk = await rt.resolveMk(opts);
3547
+ try {
3548
+ const contentKey = deriveContentKey(mk);
3549
+ const backend = await makeBackend(rt, opts.local);
3550
+ const runners = /* @__PURE__ */ new Map();
3551
+ for (const job of jobs) {
3552
+ runners.set(
3553
+ job.id,
3554
+ new SerialJobRunner(async () => {
3555
+ try {
3556
+ const r = await sweepJob(rt, backend, mk, contentKey, job, (m) => info(m));
3557
+ await updateLastBackup(rt.paths.dir, job.id, new Date(rt.now()).toISOString());
3558
+ info(`\u4EFB\u52A1 ${job.id}\uFF1A\u4E0A\u4F20 ${r.uploaded}\uFF0C\u8DF3\u8FC7 ${r.skipped}\uFF0C\u5931\u8D25 ${r.failed}`);
3559
+ } catch (err) {
3560
+ warn(`\u4EFB\u52A1 ${job.id} \u672C\u8F6E\u51FA\u9519\uFF08\u4E0B\u6B21\u89E6\u53D1\u91CD\u8BD5\uFF09\uFF1A${err.message}`);
3561
+ }
3562
+ })
3563
+ );
3564
+ }
3565
+ info(`daemon \u542F\u52A8\uFF1A${jobs.length} \u4E2A\u4EFB\u52A1\uFF0C\u542F\u52A8\u5373\u626B...`);
3566
+ for (const job of jobs) runners.get(job.id)?.trigger();
3567
+ await Promise.all([...runners.values()].map((r) => r.drain()));
3568
+ const watchers = jobs.map(
3569
+ (job) => watchRecursive(job.localDir, () => runners.get(job.id)?.trigger(), {
3570
+ debounceMs: rt.daemonDebounceMs
3571
+ })
3572
+ );
3573
+ const timer = setInterval(() => {
3574
+ for (const job of jobs) runners.get(job.id)?.trigger();
3575
+ }, rt.daemonSweepIntervalMs);
3576
+ info(
3577
+ `\u76D1\u542C\u4E2D\uFF08\u9632\u6296 ${rt.daemonDebounceMs}ms\uFF0C\u5B9A\u65F6\u515C\u5E95 ${Math.round(rt.daemonSweepIntervalMs / 6e4)}min\uFF09\u3002Ctrl-C \u9000\u51FA\u3002`
3578
+ );
3579
+ await new Promise((resolve5) => {
3580
+ let shutting = false;
3581
+ const shutdown = (sig) => {
3582
+ if (shutting) return;
3583
+ shutting = true;
3584
+ info(`\u6536\u5230 ${sig}\uFF0C\u505C\u6B62 daemon\uFF08\u7B49\u5728\u98DE\u5907\u4EFD\u5B8C\u6210\uFF09...`);
3585
+ for (const w of watchers) w.stop();
3586
+ clearInterval(timer);
3587
+ void Promise.all([...runners.values()].map((r) => r.drain())).catch(() => {
3588
+ }).then(() => resolve5());
3589
+ };
3590
+ process.once("SIGINT", () => shutdown("SIGINT"));
3591
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
3592
+ });
3593
+ ok("daemon \u5DF2\u9000\u51FA\u3002");
3594
+ } finally {
3595
+ mk.fill(0);
3596
+ }
3597
+ }
3598
+
3599
+ // src/index.ts
3600
+ var HELP = `\u655D\u5E1A bz \u2014\u2014 \u5BA2\u6237\u7AEF\u52A0\u5BC6\u5F15\u64CE CLI
3601
+
3602
+ \u7528\u6CD5: bz <\u547D\u4EE4> [\u53C2\u6570] [\u9009\u9879]
3603
+
3604
+ \u5BC6\u94A5\u4E0E\u4F1A\u8BDD:
3605
+ init \u9996\u6B21\u8BBE\u7F6E\u4E3B\u5BC6\u7801\uFF0C\u751F\u6210\u6062\u590D\u5BC6\u94A5
3606
+ unlock [--ttl <\u79D2>] \u8F93\u5165\u4E3B\u5BC6\u7801\u89E3\u9501\u672C\u8BBE\u5907\u4F1A\u8BDD\uFF08\u7F13\u5B58\u4E3B\u5BC6\u94A5\uFF09
3607
+ lock \u7ACB\u5373\u4E0A\u9501\uFF08\u6E05\u9664\u7F13\u5B58\u4E3B\u5BC6\u94A5\uFF09
3608
+ passwd \u4FEE\u6539\u4E3B\u5BC6\u7801\uFF08\u6062\u590D\u5BC6\u94A5\u4E0D\u53D8\uFF09
3609
+ recover \u7528\u6062\u590D\u5BC6\u94A5\u91CD\u8BBE\u4E3B\u5BC6\u7801
3610
+
3611
+ \u8D26\u53F7:
3612
+ login [--name <n>] [--device] [--port <p>] OAuth \u767B\u5F55\u767E\u5EA6
3613
+ logout \u6CE8\u9500\u5F53\u524D\u8D26\u53F7
3614
+ account [list|use <n>|add <n>] \u591A\u8D26\u53F7\u7BA1\u7406
3615
+
3616
+ \u8D44\u6E90:
3617
+ push <path> [-r] [--chunk 100MB] [--compress] [--no-split] [--name <n>] [--preview] [--to <\u4E91\u7AEF\u76EE\u5F55>] [--force] [--concurrency N]
3618
+ \uFF08--force \u65E0\u89C6\u53BB\u91CD/\u5728\u98DE\u9501\u5F3A\u5236\u4E0A\u4F20\uFF1B--concurrency \u7247\u5185\u5E76\u53D1\u6570\uFF0C\u9ED8\u8BA4 4\uFF0C\u8303\u56F4 1-16\uFF09
3619
+ pull <id|\u4E91\u7AEF\u76EE\u5F55> [-r] [--out <dir>] [--force] \u8FD8\u539F\u5230\u6587\u4EF6\u6839\u4E0B\uFF08--force \u65E0\u89C6\u5E42\u7B49/\u5728\u98DE\u9501\u5F3A\u5236\u4E0B\u8F7D\uFF09
3620
+ mkdir <\u76EE\u5F55> \u521B\u5EFA\u4E91\u7AEF\u76EE\u5F55\uFF08mkdir -p \u8BED\u4E49\uFF09
3621
+ ls [\u76EE\u5F55] [-r] \u5217\u51FA\u76EE\u5F55\uFF08\u663E\u793A\u771F\u540D\uFF0C\u9700\u5DF2\u89E3\u9501\uFF1B-r \u9012\u5F52\uFF09
3622
+ info <id> \u67E5\u770B\u8D44\u6E90\u5143\u6570\u636E
3623
+ rm <\u8DEF\u5F84|id> [--yes] \u5220\u9664\u5230\u56DE\u6536\u7AD9\uFF08\u5220\u9664\u76EE\u5F55\u9700 --yes \u786E\u8BA4\uFF09
3624
+ trash [list|restore <id>|rm <id>|clear] \u56DE\u6536\u7AD9\u7BA1\u7406\uFF08\u5217\u51FA/\u6062\u590D/\u6C38\u4E45\u5220\u9664/\u6E05\u7A7A\uFF09
3625
+ mv <src> <\u76EE\u6807\u76EE\u5F55> \u79FB\u52A8 bundle \u6216\u76EE\u5F55\u5230\u76EE\u6807\u76EE\u5F55\u4E0B
3626
+ cp <src> <\u76EE\u6807\u76EE\u5F55> [-r] \u590D\u5236 bundle \u6216\u76EE\u5F55\uFF08\u76EE\u5F55\u9700 -r\uFF09\u5230\u76EE\u6807\u76EE\u5F55\u4E0B
3627
+ rename <src> <\u65B0\u540D> \u6539\u540D\uFF1Abundle \u6539\u771F\u540D\uFF08\u91CD\u5199 encMeta\uFF09/ \u76EE\u5F55 native \u6539\u540D
3628
+ share <id> [--code|--7z] \u751F\u6210\u5206\u4EAB\u7801 / \u5BFC\u51FA 7z-AES\uFF08--7z \u9700 7z \u4E8C\u8FDB\u5236\uFF09
3629
+ preview <id> [--out <dir>] \u4E0B\u8F7D\u5E76\u89E3\u5BC6\u9884\u89C8\u5305\uFF1A\u56FE\u7247/\u89C6\u9891\u7F29\u7565\u56FE\u3001\u97F3\u9891\u7247\u6BB5\u3001PDF \u9996\u9875\uFF08\u843D\u6587\u4EF6\uFF09\uFF1B
3630
+ \u6587\u672C/\u4EE3\u7801\u524D 32KB\u3001\u538B\u7F29\u5305(zip/tar/tgz)\u6587\u4EF6\u5217\u8868\uFF08\u76F4\u63A5\u6253\u5370\u5230 stdout\uFF09
3631
+
3632
+ \u5907\u4EFD/\u5B88\u62A4:
3633
+ backup add <\u672C\u5730\u76EE\u5F55> [--to <\u4E91\u7AEF\u76EE\u5F55>] \u6CE8\u518C\u52A0\u5BC6\u5907\u4EFD\u4EFB\u52A1
3634
+ backup list \u5217\u51FA\u5907\u4EFD\u4EFB\u52A1
3635
+ backup rm <id> \u5220\u9664\u5907\u4EFD\u4EFB\u52A1\uFF08\u4E0D\u52A8\u4E91\u7AEF\u5DF2\u5907\u4EFD\u6570\u636E\uFF09
3636
+ backup run [<id>] \u624B\u52A8\u6267\u884C\u4E00\u6B21\u5907\u4EFD\uFF08\u7701\u7565 id \u8DD1\u5168\u90E8\uFF09
3637
+ daemon \u524D\u53F0\u5B88\u62A4\uFF1A\u542F\u52A8\u5373\u626B + \u5B9E\u65F6\u76D1\u542C + \u5B9A\u65F6\u515C\u5E95\uFF08Ctrl-C \u9000\u51FA\uFF09
3638
+
3639
+ \u5176\u5B83:
3640
+ completion <bash|zsh|powershell> \u8F93\u51FA shell \u8865\u5168\u811A\u672C\uFF08eval \u6216\u5199\u5165 rc \u6587\u4EF6\uFF09
3641
+
3642
+ \u901A\u7528\u9009\u9879:
3643
+ --local <dir> \u7528\u672C\u5730\u76EE\u5F55\u4EE3\u66FF\u767E\u5EA6\u7F51\u76D8\uFF08\u79BB\u7EBF\u6D4B\u8BD5/\u81EA\u5EFA\u540E\u7AEF\uFF09
3644
+ --password-stdin \u4ECE stdin \u8BFB\u4E3B\u5BC6\u7801\uFF08\u811A\u672C\u5316\uFF09
3645
+ -h, --help \u663E\u793A\u5E2E\u52A9
3646
+ -v, --version \u663E\u793A\u7248\u672C
3647
+
3648
+ \u51ED\u8BC1: \u5728\u9879\u76EE .env \u914D\u7F6E BAIDU_APP_KEY / BAIDU_SECRET_KEY\uFF08\u89C1 .env.example\uFF09\u3002`;
3649
+ function version() {
3650
+ if ("1.0.0") return "1.0.0";
3651
+ try {
3652
+ const here = dirname6(fileURLToPath(import.meta.url));
3653
+ return readFileSync2(join12(here, "..", "..", "..", "VERSION"), "utf8").trim();
3654
+ } catch {
3655
+ return "0.0.0";
3656
+ }
3657
+ }
3658
+ async function main(argv) {
3659
+ const { values, positionals } = parseArgs({
3660
+ args: argv,
3661
+ allowPositionals: true,
3662
+ strict: false,
3663
+ options: {
3664
+ help: { type: "boolean", short: "h" },
3665
+ version: { type: "boolean", short: "v" },
3666
+ local: { type: "string" },
3667
+ "password-stdin": { type: "boolean" },
3668
+ out: { type: "string" },
3669
+ chunk: { type: "string" },
3670
+ compress: { type: "boolean" },
3671
+ "no-split": { type: "boolean" },
3672
+ name: { type: "string" },
3673
+ preview: { type: "boolean" },
3674
+ device: { type: "boolean" },
3675
+ port: { type: "string" },
3676
+ code: { type: "boolean" },
3677
+ "7z": { type: "boolean" },
3678
+ ttl: { type: "string" },
3679
+ force: { type: "boolean" },
3680
+ recursive: { type: "boolean", short: "r" },
3681
+ to: { type: "string" },
3682
+ yes: { type: "boolean" },
3683
+ concurrency: { type: "string" }
3684
+ }
3685
+ });
3686
+ const cmd = positionals[0];
3687
+ if (values.version) {
3688
+ out(version());
3689
+ return 0;
3690
+ }
3691
+ if (!cmd || values.help) {
3692
+ info(HELP);
3693
+ return cmd ? 0 : values.help ? 0 : 1;
3694
+ }
3695
+ const common = {
3696
+ local: values.local,
3697
+ passwordStdin: Boolean(values["password-stdin"])
3698
+ };
3699
+ const rt = createRuntime();
3700
+ switch (cmd) {
3701
+ case "init":
3702
+ await cmdInit(rt, { ...common, force: Boolean(values.force) });
3703
+ return 0;
3704
+ case "unlock":
3705
+ await cmdUnlock(rt, { ...common, ttl: values.ttl ? Number(values.ttl) : void 0 });
3706
+ return 0;
3707
+ case "lock":
3708
+ await cmdLock(rt);
3709
+ return 0;
3710
+ case "passwd":
3711
+ await cmdPasswd(rt, common);
3712
+ return 0;
3713
+ case "recover":
3714
+ await cmdRecover(rt);
3715
+ return 0;
3716
+ case "login":
3717
+ await cmdLogin(rt, {
3718
+ name: values.name,
3719
+ device: Boolean(values.device),
3720
+ port: values.port ? Number(values.port) : void 0
3721
+ });
3722
+ return 0;
3723
+ case "logout":
3724
+ await cmdLogout(rt);
3725
+ return 0;
3726
+ case "account":
3727
+ await cmdAccount(rt, positionals[1], positionals[2]);
3728
+ return 0;
3729
+ case "push":
3730
+ if (!positionals[1]) throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz push <path>");
3731
+ await cmdPush(rt, positionals[1], {
3732
+ ...common,
3733
+ chunk: values.chunk,
3734
+ compress: Boolean(values.compress),
3735
+ noSplit: Boolean(values["no-split"]),
3736
+ name: values.name,
3737
+ preview: Boolean(values.preview),
3738
+ to: values.to,
3739
+ recursive: Boolean(values.recursive),
3740
+ force: Boolean(values.force),
3741
+ concurrency: values.concurrency ? Number(values.concurrency) : void 0
3742
+ });
3743
+ return 0;
3744
+ case "pull":
3745
+ if (!positionals[1]) throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz pull <id>");
3746
+ await cmdPull(rt, positionals[1], {
3747
+ ...common,
3748
+ out: values.out,
3749
+ recursive: Boolean(values.recursive),
3750
+ force: Boolean(values.force)
3751
+ });
3752
+ return 0;
3753
+ case "mkdir":
3754
+ if (!positionals[1]) throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz mkdir <\u76EE\u5F55>");
3755
+ await cmdMkdir(rt, positionals[1], common);
3756
+ return 0;
3757
+ case "ls":
3758
+ await cmdLs(rt, positionals[1], { ...common, recursive: Boolean(values.recursive) });
3759
+ return 0;
3760
+ case "info":
3761
+ if (!positionals[1]) throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz info <id>");
3762
+ await cmdInfo(rt, positionals[1], common);
3763
+ return 0;
3764
+ case "rm":
3765
+ if (!positionals[1]) throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz rm <\u8DEF\u5F84|id> [--yes]");
3766
+ await cmdRm(rt, positionals[1], {
3767
+ ...common,
3768
+ yes: Boolean(values.yes)
3769
+ });
3770
+ return 0;
3771
+ case "trash":
3772
+ await cmdTrash(rt, positionals[1], positionals[2], common);
3773
+ return 0;
3774
+ case "mv":
3775
+ if (!positionals[1] || !positionals[2]) {
3776
+ throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz mv <src> <\u76EE\u6807\u76EE\u5F55>");
3777
+ }
3778
+ await cmdMv(rt, positionals[1], positionals[2], common);
3779
+ return 0;
3780
+ case "cp":
3781
+ if (!positionals[1] || !positionals[2]) {
3782
+ throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz cp <src> <\u76EE\u6807\u76EE\u5F55> [-r]");
3783
+ }
3784
+ await cmdCp(rt, positionals[1], positionals[2], {
3785
+ ...common,
3786
+ recursive: Boolean(values.recursive)
3787
+ });
3788
+ return 0;
3789
+ case "rename":
3790
+ if (!positionals[1] || !positionals[2]) {
3791
+ throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz rename <src> <\u65B0\u540D>");
3792
+ }
3793
+ await cmdRename(rt, positionals[1], positionals[2], common);
3794
+ return 0;
3795
+ case "share":
3796
+ if (!positionals[1]) throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz share <id>");
3797
+ await cmdShare(rt, positionals[1], {
3798
+ ...common,
3799
+ code: Boolean(values.code),
3800
+ sevenz: Boolean(values["7z"]),
3801
+ out: values.out
3802
+ });
3803
+ return 0;
3804
+ case "preview":
3805
+ if (!positionals[1]) throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz preview <id>");
3806
+ await cmdPreview(rt, positionals[1], { ...common, out: values.out });
3807
+ return 0;
3808
+ case "backup":
3809
+ if (!positionals[1]) {
3810
+ throw new BizhouError("INVALID_ARG", "\u7528\u6CD5\uFF1Abz backup <add|list|rm> ...");
3811
+ }
3812
+ await cmdBackup(rt, positionals[1], positionals[2], {
3813
+ ...common,
3814
+ to: values.to
3815
+ });
3816
+ return 0;
3817
+ case "daemon":
3818
+ await cmdDaemon(rt, common);
3819
+ return 0;
3820
+ case "completion":
3821
+ cmdCompletion(positionals[1]);
3822
+ return 0;
3823
+ case "__complete":
3824
+ await cmdComplete(rt, positionals[1] ?? "", positionals[2]);
3825
+ return 0;
3826
+ default:
3827
+ errorLine(`\u672A\u77E5\u547D\u4EE4\uFF1A${cmd}`);
3828
+ info(HELP);
3829
+ return 2;
3830
+ }
3831
+ }
3832
+ main(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
3833
+ if (err instanceof BizhouError) {
3834
+ errorLine(err.message);
3835
+ process.exit(exitCodeFor(err.code));
3836
+ }
3837
+ errorLine(`\u672A\u9884\u671F\u9519\u8BEF\uFF1A${err instanceof Error ? err.message : String(err)}`);
3838
+ process.exit(1);
3839
+ });