@pab-iprg/huohuo 1.0.0-preview.2 → 1.0.0-preview.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,11 @@
6
6
 
7
7
  发布后的预览入口为 `npx --yes @pab-iprg/huohuo@next`。首版验收基线为 macOS Apple Silicon、Node 24.8.0 和 npm 11.6.0。默认持久目录为 `~/.huohuo`,普通重开保留已安装版本,安装中断后继续原目标。
8
8
 
9
- 命令支持 `huohuo [start|doctor|stop|upgrade|uninstall] [--home 目录] [--no-open]`。
9
+ 安装前会列出磁盘预留量、内存要求、固定下载项和缓存位置。输入 `y` 继续,直接回车取消。自动化环境可用 `npx --yes @pab-iprg/huohuo@next --yes` 显式同意安装。固定资源显示实际接收量,下载失败可重跑并复用已经校验的文件。
10
+
11
+ 发行清单可为同一份固定资源配置备用 HTTPS 地址。确认安装后会探测这些地址,优先从响应较快的可用地址下载,并核对同一个 SHA-256。只有一个已核验来源时,网络中断会自动重试一次。npm 和 pip 仍会按各自的依赖清单安装。
12
+
13
+ 命令支持 `huohuo [start|status|doctor|stop|upgrade|uninstall] [--home 目录] [--no-open]`。在另一个终端运行 `npx --yes @pab-iprg/huohuo@next status --watch` 可打开实时状态看板,按 Ctrl+C 退出看板。
10
14
 
11
15
  显式升级检查旧迁移不可变、追加迁移顺序、执行库格式与 DSH 会话兼容范围,先一致性备份再迁移和验证,成功后切换;不自动恢复旧备份覆盖新数据。默认卸载保留实例、凭据、会话和备份,不提供直接彻底删除业务数据的快捷入口。
12
16
 
package/cli.js CHANGED
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // distribution/bootstrap/entry.ts
4
- import { readFile as readFile7, realpath as realpath5 } from "node:fs/promises";
5
- import { createReadStream as createReadStream2 } from "node:fs";
4
+ import { open as open3, readFile as readFile8, realpath as realpath5 } from "node:fs/promises";
6
5
  import { Readable } from "node:stream";
7
6
  import { homedir } from "node:os";
8
- import { dirname as dirname5, join as join6, resolve as resolve5 } from "node:path";
7
+ import { dirname as dirname5, join as join7, resolve as resolve5 } from "node:path";
9
8
  import { fileURLToPath } from "node:url";
10
9
 
11
10
  // distribution/bootstrap/install.ts
@@ -133,20 +132,24 @@ var hash = /^[a-f0-9]{64}$/;
133
132
  function integer(n) {
134
133
  return Number.isSafeInteger(n) && Number(n) >= 0;
135
134
  }
136
- function artifact(a) {
137
- if (!a || !hash.test(a.sha256) || !integer(a.downloadBytes) || !integer(a.expandedBytes) || !a.downloadBytes || !a.expandedBytes) return false;
138
- if (a.redirectHosts !== void 0 && (!Array.isArray(a.redirectHosts) || a.redirectHosts.length > 4 || !a.redirectHosts.every((s) => typeof s === "string" && /^[a-z0-9.-]+$/.test(s)))) return false;
139
- if (a.bundled !== void 0) return a.bundled === true && a.url === `https://bundled.invalid/${a.sha256}.tgz` && a.redirectHosts === void 0;
135
+ function httpsURL(value) {
136
+ if (typeof value !== "string") return false;
140
137
  try {
141
- const u = new URL(a.url);
138
+ const u = new URL(value);
142
139
  return u.protocol === "https:" && !u.username && !u.password && !u.hash;
143
140
  } catch {
144
141
  return false;
145
142
  }
146
143
  }
144
+ function artifact(a) {
145
+ if (!a || !hash.test(a.sha256) || !integer(a.downloadBytes) || !integer(a.expandedBytes) || !a.downloadBytes || !a.expandedBytes) return false;
146
+ if (a.redirectHosts !== void 0 && (!Array.isArray(a.redirectHosts) || a.redirectHosts.length > 4 || !a.redirectHosts.every((s) => typeof s === "string" && /^[a-z0-9.-]+$/.test(s)))) return false;
147
+ if (a.bundled !== void 0) return a.bundled === true && a.url === `https://bundled.invalid/${a.sha256}.tgz` && a.redirectHosts === void 0 && a.mirrors === void 0;
148
+ return httpsURL(a.url) && (a.mirrors === void 0 || Array.isArray(a.mirrors) && a.mirrors.length <= 3 && a.mirrors.every(httpsURL));
149
+ }
147
150
  function validateRelease(input) {
148
151
  const r = input;
149
- if (!r || r.format !== 1 || r.bootstrapFormat !== 1 || !version.test(r.version) || !integer(r.nodeMajor) || r.nodeMajor < 24 || !integer(r.npmMajor) || r.npmMajor < 11 || !r.platform?.minimumOsRelease || !["darwin", "linux", "win32"].includes(r.platform.os) || !["arm64", "x64"].includes(r.platform.arch) || !artifact(r.runtime) || r.runtime.name !== "@huohuo/research-agent" || r.runtime.version !== r.version || !/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(r.runtime.integrity) || !artifact(r.python) || !artifact(r.pythonDependencies) || !r.python.build || !r.python.license || !r.python.executable || r.python.executable.startsWith("/") || r.python.executable.split(/[\\/]/).some((p) => p === ".." || !p) || !version.test(r.dsh?.version) || !hash.test(r.dsh?.assemblySha256) || !hash.test(r.requirementsSha256) || !hash.test(r.profilesSha256) || !r.budget || ![
152
+ if (!r || r.format !== 1 || r.bootstrapFormat !== 1 || !version.test(r.version) || !integer(r.nodeMajor) || r.nodeMajor < 24 || !integer(r.npmMajor) || r.npmMajor < 11 || !r.platform?.minimumOsRelease || !["darwin", "linux", "win32"].includes(r.platform.os) || !["arm64", "x64"].includes(r.platform.arch) || !artifact(r.runtime) || r.runtime.name !== "@huohuo/research-agent" || r.runtime.version !== r.version || !/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(r.runtime.integrity) || !artifact(r.python) || !artifact(r.pythonDependencies) || !r.python.build || !r.python.license || r.pythonDependencies.upstreamWheelCount !== void 0 && !integer(r.pythonDependencies.upstreamWheelCount) || !r.python.executable || r.python.executable.startsWith("/") || r.python.executable.split(/[\\/]/).some((p) => p === ".." || !p) || !version.test(r.dsh?.version) || !hash.test(r.dsh?.assemblySha256) || !hash.test(r.requirementsSha256) || !hash.test(r.profilesSha256) || !r.budget || ![
150
153
  "minimumEffectiveMemoryBytes",
151
154
  "startupHeadroomBytes",
152
155
  "heavyJobHeadroomBytes",
@@ -158,7 +161,7 @@ function validateRelease(input) {
158
161
  "processTreeBudgetBytes",
159
162
  "sampleIntervalMs",
160
163
  "sampleMaxAgeMs"
161
- ].every((k) => integer(r.budget[k])) || r.budget.startupHeadroomBytes <= 0 || r.budget.minimumEffectiveMemoryBytes <= 0 || r.budget.diskReserveBytes <= 0 || r.budget.recoverySamples < 2 || r.budget.heavyJobHeadroomBytes < r.budget.startupHeadroomBytes || r.budget.processTreeBudgetBytes <= 0 || r.budget.sampleIntervalMs < 1e3 || r.budget.sampleMaxAgeMs < r.budget.sampleIntervalMs * 2 || r.budget.sampleMaxAgeMs > 3e4 || !r.schema || !integer(r.schema.minimum) || !integer(r.schema.maximum) || r.schema.minimum > r.schema.maximum) {
164
+ ].every((k) => integer(r.budget[k])) || r.budget.observedInstallRssBytes !== void 0 && (!integer(r.budget.observedInstallRssBytes) || r.budget.observedInstallRssBytes <= 0) || r.budget.startupHeadroomBytes <= 0 || r.budget.minimumEffectiveMemoryBytes <= 0 || r.budget.diskReserveBytes <= 0 || r.budget.recoverySamples < 2 || r.budget.heavyJobHeadroomBytes < r.budget.startupHeadroomBytes || r.budget.processTreeBudgetBytes <= 0 || r.budget.sampleIntervalMs < 1e3 || r.budget.sampleMaxAgeMs < r.budget.sampleIntervalMs * 2 || r.budget.sampleMaxAgeMs > 3e4 || !r.schema || !integer(r.schema.minimum) || !integer(r.schema.maximum) || r.schema.minimum > r.schema.maximum) {
162
165
  throw new Error("\u53D1\u884C\u6E05\u5355\u65E0\u6548\u6216\u5C1A\u672A\u5B8C\u6210\u5E73\u53F0\u3001\u6458\u8981\u4E0E\u8D44\u6E90\u6D4B\u91CF\uFF1B\u4E0D\u80FD\u5B89\u88C5");
163
166
  }
164
167
  if (!hash.test(r.schema.migrationsSha256)) throw new Error("\u7F3A\u5C11\u6570\u636E\u5E93\u8FC1\u79FB\u88C5\u914D\u6458\u8981");
@@ -182,45 +185,102 @@ async function verifyArtifact(path, artifact2) {
182
185
  }
183
186
  if (bytes !== artifact2.downloadBytes || digest2.digest("hex") !== artifact2.sha256 || "integrity" in artifact2 && artifact2.integrity !== `sha512-${integrity.digest("base64")}`) throw new Error("\u4E0B\u8F7D\u4EA7\u7269\u5927\u5C0F\u6216\u6458\u8981\u4E0D\u7B26");
184
187
  }
185
- async function downloadArtifact(artifact2, path, fetcher = fetch) {
188
+ var RetryableDownload = class extends Error {
189
+ };
190
+ async function downloadArtifact(artifact2, path, fetcher = fetch, progress) {
186
191
  try {
187
192
  await verifyArtifact(path, artifact2);
193
+ progress?.({ status: "cached", bytes: artifact2.downloadBytes });
188
194
  return;
189
195
  } catch (e) {
190
196
  if (e.code !== "ENOENT") throw e;
191
197
  }
192
198
  await mkdir2(dirname2(path), { recursive: true, mode: 448 });
199
+ const sources = [.../* @__PURE__ */ new Set([artifact2.url, ...artifact2.mirrors ?? []])];
200
+ if (sources.length > 1) {
201
+ progress?.({ status: "probing", bytes: 0 });
202
+ const results = await Promise.all(sources.map(async (source) => {
203
+ const started = performance.now();
204
+ try {
205
+ const response = await requestSource(artifact2, source, fetcher, AbortSignal.timeout(5e3), "HEAD");
206
+ await response.body?.cancel();
207
+ return { source, latency: response.ok ? performance.now() - started : Infinity };
208
+ } catch {
209
+ return { source, latency: Infinity };
210
+ }
211
+ }));
212
+ results.sort((a, b) => a.latency - b.latency);
213
+ sources.splice(0, sources.length, ...results.map((result) => result.source));
214
+ } else if (!artifact2.bundled) sources.push(artifact2.url);
215
+ for (const [index, source] of sources.entries()) {
216
+ const host = artifact2.bundled ? void 0 : new URL(source).hostname;
217
+ progress?.({ status: "start", bytes: 0, host });
218
+ try {
219
+ await transfer(artifact2, path, source, fetcher, (bytes) => progress?.({ status: "receiving", bytes, host }));
220
+ progress?.({ status: "verified", bytes: artifact2.downloadBytes, host });
221
+ return;
222
+ } catch (error) {
223
+ if (!(error instanceof RetryableDownload)) throw error;
224
+ if (index === sources.length - 1) throw new Error(`${host} \u4E0B\u8F7D\u672A\u5B8C\u6210\uFF08${error.message}\uFF09\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8DD1\uFF0C\u5DF2\u6821\u9A8C\u7684\u6587\u4EF6\u4F1A\u590D\u7528\u3002`, { cause: error });
225
+ progress?.({ status: "retry", bytes: 0, host });
226
+ }
227
+ }
228
+ }
229
+ async function requestSource(artifact2, source, fetcher, signal, method, onResponse) {
230
+ const allowed = /* @__PURE__ */ new Set([new URL(source).hostname, ...artifact2.redirectHosts ?? []]);
231
+ let url = source;
232
+ for (let redirects = 0; redirects <= 4; redirects++) {
233
+ const next = new URL(url);
234
+ if (next.protocol !== "https:" || next.username || next.password || !allowed.has(next.hostname)) throw new Error("\u4E0B\u8F7D\u91CD\u5B9A\u5411\u4E0D\u5728\u56FA\u5B9A\u53D1\u884C\u5141\u8BB8\u6765\u6E90\u5185");
235
+ const response = await fetcher(url, { signal, method, redirect: "manual" });
236
+ onResponse?.();
237
+ if (![301, 302, 303, 307, 308].includes(response.status)) return response;
238
+ const location = response.headers.get("location");
239
+ await response.body?.cancel();
240
+ if (!location || redirects === 4) throw new Error("\u4E0B\u8F7D\u91CD\u5B9A\u5411\u65E0\u6548\u6216\u8FC7\u591A");
241
+ url = new URL(location, url).href;
242
+ }
243
+ throw new Error("\u4E0B\u8F7D\u672A\u8FD4\u56DE\u54CD\u5E94");
244
+ }
245
+ async function transfer(artifact2, path, source, fetcher, onBytes) {
193
246
  const pending = `${path}.${randomUUID2()}.partial`;
194
247
  const handle = await open2(pending, "wx", 384);
248
+ const idle = new AbortController();
249
+ let timer;
250
+ const keepAlive = () => {
251
+ clearTimeout(timer);
252
+ timer = setTimeout(() => idle.abort(), 45e3);
253
+ timer.unref();
254
+ };
255
+ const signal = AbortSignal.any([idle.signal, AbortSignal.timeout(30 * 6e4)]);
256
+ keepAlive();
195
257
  try {
196
- const allowed = /* @__PURE__ */ new Set([new URL(artifact2.url).hostname, ...artifact2.redirectHosts ?? []]);
197
- let url = artifact2.url;
198
- let response;
199
- const transferMs = Math.ceil(artifact2.downloadBytes / (64 * 1024)) * 1e3;
200
- const signal = AbortSignal.timeout(Math.min(9e5, Math.max(12e4, 3e4 + transferMs)));
201
- for (let redirects = 0; redirects <= 4; redirects++) {
202
- const next = new URL(url);
203
- if (next.protocol !== "https:" || next.username || next.password || !allowed.has(next.hostname)) throw new Error("\u4E0B\u8F7D\u91CD\u5B9A\u5411\u4E0D\u5728\u56FA\u5B9A\u53D1\u884C\u5141\u8BB8\u6765\u6E90\u5185");
204
- response = await fetcher(url, { signal, redirect: "manual" });
205
- if (![301, 302, 303, 307, 308].includes(response.status)) break;
206
- const location = response.headers.get("location");
258
+ const response = await requestSource(artifact2, source, fetcher, signal, "GET", keepAlive);
259
+ if (!response.ok || !response.body) {
207
260
  await response.body?.cancel();
208
- if (!location || redirects === 4) throw new Error("\u4E0B\u8F7D\u91CD\u5B9A\u5411\u65E0\u6548\u6216\u8FC7\u591A");
209
- url = new URL(location, url).href;
261
+ if (!artifact2.bundled && (response.status === 408 || response.status === 429 || response.status >= 500 || artifact2.mirrors?.length && [403, 404].includes(response.status))) {
262
+ throw new RetryableDownload(`HTTP ${response.status}`);
263
+ }
264
+ throw new Error(`\u4E0B\u8F7D\u5931\u8D25\uFF1AHTTP ${response.status}`);
210
265
  }
211
- if (!response) throw new Error("\u4E0B\u8F7D\u672A\u8FD4\u56DE\u54CD\u5E94");
212
- if (!response.ok || !response.body) throw new Error(`\u4E0B\u8F7D\u5931\u8D25\uFF1AHTTP ${response.status}`);
213
266
  let received = 0;
214
267
  for await (const chunk of response.body) {
215
268
  received += chunk.byteLength;
269
+ keepAlive();
216
270
  if (received > artifact2.downloadBytes) throw new Error("\u4E0B\u8F7D\u8D85\u8FC7\u53D1\u884C\u6E05\u5355\u5927\u5C0F");
217
271
  await handle.writeFile(chunk);
272
+ onBytes(received);
218
273
  }
219
274
  await handle.sync();
220
275
  await handle.close();
221
276
  await verifyArtifact(pending, artifact2);
222
277
  await rename2(pending, path);
278
+ } catch (error) {
279
+ if (!artifact2.bundled && signal.aborted) throw new RetryableDownload(idle.signal.aborted ? "\u8FDE\u7EED 45 \u79D2\u672A\u6536\u5230\u6570\u636E" : "\u8D85\u8FC7\u5355\u6B21\u4E0B\u8F7D\u65F6\u9650", { cause: error });
280
+ if (!artifact2.bundled && error instanceof TypeError) throw new RetryableDownload("\u7F51\u7EDC\u8FDE\u63A5\u4E2D\u65AD", { cause: error });
281
+ throw error;
223
282
  } finally {
283
+ clearTimeout(timer);
224
284
  await handle.close().catch(() => {
225
285
  });
226
286
  await rm(pending, { force: true });
@@ -588,6 +648,32 @@ async function install(home, candidate, hooks = {}, programsOnly = false) {
588
648
  if (!programsOnly && !record && (await readdir(home)).some((name) => name !== "state")) throw new Error("\u76EE\u5F55\u542B\u65E0\u5B89\u88C5\u8BB0\u5F55\u7684\u65E2\u6709\u5185\u5BB9\uFF1B\u8BF7\u6838\u67E5\u5F52\u5C5E\u6216\u9009\u62E9\u7A7A\u76EE\u5F55");
589
649
  if (!programsOnly && !record && (await readdir(join3(home, "state"))).some((name) => name !== "install.lock")) throw new Error("\u76EE\u5F55\u542B\u65E0\u6CD5\u786E\u8BA4\u5F52\u5C5E\u7684\u72B6\u6001\uFF1B\u672A\u91CD\u5EFA\u5B89\u88C5");
590
650
  if (programsOnly && (await readInstallation(home))?.status !== "ready") throw new Error("\u5F53\u524D\u5B89\u88C5\u4E0D\u5B8C\u6574\uFF0C\u5148\u4FEE\u590D\u539F\u7248\u672C");
651
+ const bundleDirectory = hooks.bundleDirectory;
652
+ if (!programsOnly && record && bundleDirectory && candidate && ["installing", "blocked", "failed"].includes(record.status) && ["preflight", "download"].includes(record.stage) && Object.keys(record.stages).every((stage2) => stage2 === "preflight" || stage2 === "download")) {
653
+ const hasFile = async (path) => {
654
+ try {
655
+ await access(path);
656
+ return true;
657
+ } catch (error) {
658
+ if (error.code === "ENOENT") return false;
659
+ throw error;
660
+ }
661
+ };
662
+ const bundled = (release2) => [release2.runtime, release2.python, release2.pythonDependencies].filter((artifact2) => artifact2.bundled);
663
+ const oldAssets = bundled(record.release);
664
+ const available = await Promise.all(oldAssets.map(async (artifact2) => {
665
+ const file = artifact2.sha256 + ".tgz";
666
+ return await hasFile(join3(bundleDirectory, file)) || await hasFile(join3(home, "cache", file));
667
+ }));
668
+ if (available.includes(false)) {
669
+ const next = validateRelease(candidate);
670
+ const nextAssets = bundled(next);
671
+ if (next.runtime.bundled && next.version === record.release.version && (await Promise.all(nextAssets.map((artifact2) => hasFile(join3(bundleDirectory, artifact2.sha256 + ".tgz"))))).every(Boolean)) {
672
+ record = void 0;
673
+ hooks.progress?.("resume-current-package");
674
+ }
675
+ }
676
+ }
591
677
  const release = record?.release ?? validateRelease(candidate);
592
678
  if (release.platform.os !== process.platform || release.platform.arch !== process.arch || !versionAtLeast(osRelease(), release.platform.minimumOsRelease) || !versionAtLeast(process.versions.node, "24.8.0") || Number(process.versions.node.split(".")[0]) !== release.nodeMajor) {
593
679
  throw new Error("\u6B64\u53D1\u884C\u5C1A\u672A\u9A8C\u8BC1\u5F53\u524D OS/CPU/Node \u7EC4\u5408\uFF1B\u672A\u5F00\u59CB\u91CD\u4E0B\u8F7D");
@@ -597,6 +683,20 @@ async function install(home, candidate, hooks = {}, programsOnly = false) {
597
683
  const paths = installationPaths(home, release);
598
684
  const execute4 = hooks.run ?? run;
599
685
  const sample = hooks.sample ?? sampleResources;
686
+ const files = [
687
+ { name: "\u706B\u706B\u8FD0\u884C\u5305", artifact: release.runtime, path: paths.archive },
688
+ { name: "Python \u8FD0\u884C\u73AF\u5883", artifact: release.python, path: paths.pythonArchive },
689
+ { name: "Python \u4F9D\u8D56\u6E05\u5355\u6216\u79BB\u7EBF\u5305", artifact: release.pythonDependencies, path: join3(home, "cache", release.pythonDependencies.sha256 + ".tgz") }
690
+ ];
691
+ const withActivity = async (label, action) => {
692
+ hooks.progress?.(label);
693
+ const timer = hooks.progress && setInterval(() => hooks.progress?.(`${label}\u4ECD\u5728\u8FDB\u884C\uFF0C\u8BF7\u7A0D\u5019`), 2e4);
694
+ try {
695
+ await action();
696
+ } finally {
697
+ if (timer) clearInterval(timer);
698
+ }
699
+ };
600
700
  const cached = async (path, artifact2) => {
601
701
  try {
602
702
  await verifyArtifact(path, artifact2);
@@ -631,15 +731,10 @@ async function install(home, candidate, hooks = {}, programsOnly = false) {
631
731
  }
632
732
  };
633
733
  const check = async () => {
634
- const [runtimeCached, pythonCached, wheelsCached, inventory] = await Promise.all([
635
- cached(paths.archive, release.runtime),
636
- cached(paths.pythonArchive, release.python),
637
- cached(join3(home, "cache", release.pythonDependencies.sha256 + ".tgz"), release.pythonDependencies),
638
- installed()
639
- ]);
734
+ const [cacheHits, inventory] = await Promise.all([Promise.all(files.map((file) => cached(file.path, file.artifact))), installed()]);
640
735
  const remainingDependencies = inventory.dependencies ? 0 : release.budget.dependenciesBytes;
641
736
  const costs = [
642
- { path: join3(home, "cache"), bytes: (runtimeCached ? 0 : release.runtime.downloadBytes) + (pythonCached ? 0 : release.python.downloadBytes) + (wheelsCached ? 0 : release.pythonDependencies.downloadBytes) },
737
+ { path: join3(home, "cache"), bytes: files.reduce((sum, file, index) => sum + (cacheHits[index] ? 0 : file.artifact.downloadBytes), 0) },
643
738
  { path: paths.directory, bytes: inventory.dependencies ? 0 : release.runtime.expandedBytes + release.budget.installTemporaryBytes },
644
739
  { path: paths.python, bytes: inventory.python ? 0 : release.python.expandedBytes },
645
740
  { path: paths.environment, bytes: 0 },
@@ -665,6 +760,19 @@ async function install(home, candidate, hooks = {}, programsOnly = false) {
665
760
  ${details}
666
761
  \u5904\u7406\u540E\uFF0C\u91CD\u65B0\u8FD0\u884C\u521A\u624D\u7684\u5B89\u88C5\u547D\u4EE4\u3002`), { code: "RESOURCE_BLOCKED" });
667
762
  }
763
+ const pendingFiles = files.filter((_, index) => !cacheHits[index]);
764
+ return {
765
+ home,
766
+ resources: result,
767
+ cached: files.length - pendingFiles.length,
768
+ bundled: pendingFiles.filter((file) => file.artifact.bundled).length,
769
+ upstreamWheelCount: inventory.dependencies ? void 0 : release.pythonDependencies.upstreamWheelCount,
770
+ downloads: pendingFiles.filter((file) => !file.artifact.bundled).map((file) => ({
771
+ name: file.name,
772
+ size: file.artifact.downloadBytes,
773
+ host: new URL(file.artifact.url).hostname
774
+ }))
775
+ };
668
776
  };
669
777
  const stage = async (name, action) => {
670
778
  hooks.progress?.(name);
@@ -675,8 +783,8 @@ ${details}
675
783
  record.stages[name] = entry;
676
784
  await atomicJSON(recordPath, record);
677
785
  try {
678
- await check();
679
- const output = await action();
786
+ const plan = await check();
787
+ const output = await action(plan);
680
788
  entry.status = "verified";
681
789
  entry.outputDigest = digest(output ?? null);
682
790
  entry.endedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -684,13 +792,13 @@ ${details}
684
792
  entry.status = "failed";
685
793
  entry.endedAt = (/* @__PURE__ */ new Date()).toISOString();
686
794
  entry.errorCode = typeof error.code === "string" && /^[A-Z0-9_]+$/.test(error.code) ? error.code : "STAGE_FAILED";
687
- record.status = entry.errorCode === "RESOURCE_BLOCKED" ? "blocked" : "failed";
795
+ record.status = entry.errorCode === "RESOURCE_BLOCKED" || entry.errorCode === "INSTALL_CANCELLED" ? "blocked" : "failed";
688
796
  await atomicJSON(recordPath, record);
689
797
  throw error;
690
798
  }
691
799
  await atomicJSON(recordPath, record);
692
800
  };
693
- await stage("preflight", async () => {
801
+ await stage("preflight", async (plan) => {
694
802
  const npm = npmCommand();
695
803
  const { stdout } = await promisify3(execFile3)(npm.command, [...npm.prefix, "--version"], { timeout: 5e3 });
696
804
  if (!/^\d+\.\d+\.\d+\s*$/.test(stdout) || Number(stdout.split(".")[0]) !== release.npmMajor) throw new Error("npm \u7248\u672C\u672A\u77E5\u6216\u4E0D\u5728\u6B64\u53D1\u884C\u5DF2\u9A8C\u8BC1\u8303\u56F4\u5185");
@@ -700,11 +808,25 @@ ${details}
700
808
  } finally {
701
809
  await rm2(probe, { force: true });
702
810
  }
811
+ if (hooks.confirm && !await hooks.confirm(plan)) {
812
+ throw Object.assign(new Error("\u5DF2\u53D6\u6D88\u5B89\u88C5\uFF0C\u672C\u6B21\u6CA1\u6709\u8FDB\u5165\u4E0B\u8F7D\u3002"), { code: "INSTALL_CANCELLED" });
813
+ }
703
814
  });
704
815
  await stage("download", async () => {
705
- await downloadArtifact(release.runtime, paths.archive, hooks.fetcher);
706
- await downloadArtifact(release.python, paths.pythonArchive, hooks.fetcher);
707
- await downloadArtifact(release.pythonDependencies, join3(home, "cache", release.pythonDependencies.sha256 + ".tgz"), hooks.fetcher);
816
+ for (const [index, file] of files.entries()) {
817
+ const progress = (event) => hooks.downloadProgress?.({
818
+ name: file.name,
819
+ index: index + 1,
820
+ total: files.length,
821
+ size: file.artifact.downloadBytes
822
+ }, event);
823
+ try {
824
+ await downloadArtifact(file.artifact, file.path, hooks.fetcher, progress);
825
+ } catch (error) {
826
+ progress({ status: "failed", bytes: 0 });
827
+ throw new Error(`${file.name}\u5904\u7406\u5931\u8D25\u3002 ${error instanceof Error ? error.message : "\u8BF7\u91CD\u8BD5\u5B89\u88C5\u3002"}`, { cause: error });
828
+ }
829
+ }
708
830
  return [release.runtime.sha256, release.python.sha256, release.pythonDependencies.sha256];
709
831
  });
710
832
  await stage("install-runtime", async () => {
@@ -737,7 +859,7 @@ ${details}
737
859
  dependencies: { "@huohuo/research-agent": `file:${paths.archive}` }
738
860
  });
739
861
  const npm = npmCommand();
740
- await execute4(npm.command, [...npm.prefix, "install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--prefer-offline", "--cache", join3(home, "cache/npm")], paths.directory);
862
+ await withActivity("\u5B89\u88C5 npm \u4F9D\u8D56", () => execute4(npm.command, [...npm.prefix, "install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--prefer-offline", "--cache", join3(home, "cache/npm")], paths.directory));
741
863
  const runtime = JSON.parse(await readFile4(join3(paths.runtime, "package.json"), "utf8"));
742
864
  if (runtime.name !== release.runtime.name || runtime.version !== release.version) throw new Error("\u8FD0\u884C\u5305\u8EAB\u4EFD\u4E0D\u7B26");
743
865
  const requirements = await readFile4(join3(paths.runtime, "sidecar/requirements.lock"));
@@ -771,7 +893,12 @@ ${details}
771
893
  }
772
894
  if (!environmentReady) {
773
895
  await execute4(join3(paths.python, release.python.executable), ["-m", "venv", paths.environment], home);
774
- await execute4(python, [
896
+ if (release.pythonDependencies.bundled && hooks.progress) {
897
+ const lock = await readFile4(join3(wheels, "requirements.txt"), "utf8");
898
+ const upstream = lock.split("\n").filter((line) => /^[\w.-]+\s+@\s+https:\/\//.test(line)).length;
899
+ if (upstream) hooks.progress?.(`Python \u4F9D\u8D56\u6E05\u5355\u6709 ${upstream} \u4E2A\u4E0A\u6E38\u6587\u4EF6\uFF0C\u4E0B\u65B9\u663E\u793A\u6B63\u5728\u5904\u7406\u7684\u6587\u4EF6`);
900
+ }
901
+ await withActivity("\u5B89\u88C5 Python \u4F9D\u8D56", () => execute4(python, [
775
902
  "-m",
776
903
  "pip",
777
904
  "--isolated",
@@ -784,7 +911,7 @@ ${details}
784
911
  ...release.pythonDependencies.bundled ? ["--no-cache-dir"] : [],
785
912
  "-r",
786
913
  join3(wheels, "requirements.txt")
787
- ], home);
914
+ ], home));
788
915
  }
789
916
  await execute4(python, ["-m", "pip", "--isolated", "check"], home);
790
917
  await atomicJSON(join3(paths.environment, ".dependencies-ready.json"), { sha256: release.pythonDependencies.sha256 });
@@ -797,7 +924,7 @@ ${details}
797
924
  return record;
798
925
  }
799
926
  await stage("assemble-profiles", async () => {
800
- await execute4(process.execPath, [join3(paths.runtime, "lib/distribution/managed.js"), "prepare", home], home);
927
+ await withActivity("\u914D\u7F6E\u5BF9\u8BDD\u4E0E\u540E\u53F0\u8FD0\u884C\u4F9D\u8D56", () => execute4(process.execPath, [join3(paths.runtime, "lib/distribution/managed.js"), "prepare", home], home));
801
928
  return { release: record.releaseDigest };
802
929
  });
803
930
  await stage("verify", async () => {
@@ -1046,18 +1173,185 @@ async function uninstall(home, hooks = {}) {
1046
1173
  });
1047
1174
  }
1048
1175
 
1176
+ // distribution/bootstrap/install-ui.ts
1177
+ import { createInterface } from "node:readline";
1178
+ var gb = (bytes) => bytes === null ? "\u672A\u77E5" : `${(bytes / 1e9).toFixed(2)} GB`;
1179
+ var mb = (bytes) => bytes < 1e5 ? `${(bytes / 1e3).toFixed(1)} KB` : `${(bytes / 1e6).toFixed(1)} MB`;
1180
+ function installUI(assumeYes) {
1181
+ const interactive = process.stdin.isTTY && process.stderr.isTTY;
1182
+ const output = process.stderr;
1183
+ let lastUpdate = 0;
1184
+ let lineActive = false;
1185
+ const clear = () => {
1186
+ if (lineActive) {
1187
+ output.write("\r\x1B[2K");
1188
+ lineActive = false;
1189
+ }
1190
+ };
1191
+ const line = (text) => {
1192
+ clear();
1193
+ output.write(text + "\n");
1194
+ };
1195
+ return {
1196
+ async confirm(plan) {
1197
+ const { sample, policy, disks } = plan.resources;
1198
+ line(`\u5B89\u88C5\u76EE\u5F55 ${plan.home}`);
1199
+ for (const [index, disk] of disks.entries()) {
1200
+ const location = sample.disks.find((item) => item.device === disk.device)?.paths[0] ?? plan.home;
1201
+ line(`\u78C1\u76D8 ${index + 1}\uFF08${location}\uFF09 \u5B89\u88C5\u9884\u7559 ${gb(disk.requiredBytes)}\uFF0C\u5F53\u524D\u53EF\u7528 ${gb(disk.availableBytes)}`);
1202
+ }
1203
+ line(`\u5185\u5B58 \u672C\u673A\u5BB9\u91CF ${gb(sample.effectiveMemoryBytes)}\uFF0C\u5F53\u524D\u53EF\u7528 ${gb(sample.availableMemoryBytes)}`);
1204
+ line(`\u5185\u5B58\u8981\u6C42 \u6700\u4F4E\u5BB9\u91CF ${gb(policy.minimumEffectiveMemoryBytes)}\uFF0C\u542F\u52A8\u9700\u7559\u51FA ${gb(policy.startupHeadroomBytes)}`);
1205
+ line(policy.observedInstallRssBytes ? `\u5B89\u88C5\u5185\u5B58\u53C2\u8003 \u540C\u5E73\u53F0\u5386\u53F2\u8FDB\u7A0B\u5185\u5B58\u5CF0\u503C ${gb(policy.observedInstallRssBytes)}\uFF0C\u5B9E\u9645\u5360\u7528\u4F1A\u53D8\u5316` : "\u5B89\u88C5\u5185\u5B58\u5CF0\u503C\u968F\u4F9D\u8D56\u548C\u672C\u673A\u72B6\u6001\u53D8\u5316\uFF0C\u5F53\u524D\u6E05\u5355\u6682\u65E0\u5B9E\u6D4B\u503C");
1206
+ for (const reason of plan.resources.reasons) line(reason);
1207
+ const bytes = plan.downloads.reduce((total, file) => total + file.size, 0);
1208
+ line(`\u56FA\u5B9A\u8D44\u6E90 \u8054\u7F51\u4E0B\u8F7D ${plan.downloads.length} \u9879\uFF08${mb(bytes)}\uFF09\uFF0C\u968F\u5305 ${plan.bundled} \u9879\uFF0C\u7F13\u5B58 ${plan.cached} \u9879`);
1209
+ for (const file of plan.downloads) line(` ${file.name} ${mb(file.size)} ${file.host}`);
1210
+ line(`\u56FA\u5B9A\u8D44\u6E90\u4FDD\u5B58\u81F3 ${plan.home}/cache`);
1211
+ if (plan.upstreamWheelCount !== void 0) line(`Python \u4F9D\u8D56\u6E05\u5355 ${plan.upstreamWheelCount} \u9879\u56FA\u5B9A\u4E0A\u6E38\u6587\u4EF6\uFF0C\u5DF2\u5B89\u88C5\u9879\u53EF\u80FD\u8DF3\u8FC7\u3002`);
1212
+ line("\u540E\u7EED\u8FD8\u4F1A\u5B89\u88C5 npm \u548C Python \u4F9D\u8D56\uFF0C\u5B9E\u9645\u8054\u7F51\u4E0B\u8F7D\u91CF\u53D6\u51B3\u4E8E\u672C\u673A\u7F13\u5B58\u548C\u4F9D\u8D56\u89E3\u6790\u7ED3\u679C\u3002");
1213
+ if (assumeYes) return true;
1214
+ if (!interactive) throw new Error("\u9700\u8981\u786E\u8BA4\u5B89\u88C5\u3002\u8BF7\u5728\u7EC8\u7AEF\u8FD0\u884C\uFF0C\u6216\u5728\u706B\u706B\u547D\u4EE4\u672B\u5C3E\u52A0\u4E0A --yes\u3002");
1215
+ const terminal = createInterface({ input: process.stdin, output, terminal: true });
1216
+ try {
1217
+ const answers = terminal[Symbol.asyncIterator]();
1218
+ output.write("\u7EE7\u7EED\u5B89\u88C5\uFF1F[y/N] ");
1219
+ const answer = await answers.next();
1220
+ return !answer.done && /^(y|yes|是)$/i.test(answer.value.trim());
1221
+ } finally {
1222
+ terminal.close();
1223
+ }
1224
+ },
1225
+ downloadProgress(file, event) {
1226
+ const label = `[${file.index}/${file.total}] ${file.name}`;
1227
+ if (event.status === "probing") {
1228
+ line(`${label} \u6B63\u5728\u68C0\u67E5\u4E0B\u8F7D\u6E90\u662F\u5426\u53EF\u8FBE`);
1229
+ } else if (event.status === "start") {
1230
+ lastUpdate = Date.now();
1231
+ line(`${label} ${event.host ? `\u6B63\u5728\u8FDE\u63A5 ${event.host}` : "\u6B63\u5728\u8BFB\u53D6\u968F\u5305\u8D44\u6E90"} ${mb(file.size)}`);
1232
+ } else if (event.status === "cached") {
1233
+ line(`${label} \u5DF2\u6821\u9A8C\u5E76\u590D\u7528\u7F13\u5B58`);
1234
+ } else if (event.status === "verified") {
1235
+ line(`${label} \u5DF2\u5B8C\u6210\u5E76\u6821\u9A8C ${mb(file.size)}`);
1236
+ } else if (event.status === "retry") {
1237
+ line(`${label} ${event.host} \u4E0B\u8F7D\u672A\u5B8C\u6210\uFF0C\u51C6\u5907\u91CD\u8BD5`);
1238
+ } else if (event.status === "failed") {
1239
+ clear();
1240
+ } else {
1241
+ const now = Date.now();
1242
+ if (now - lastUpdate < (interactive ? 180 : 15e3)) return;
1243
+ lastUpdate = now;
1244
+ const percent = Math.min(99, Math.floor(100 * event.bytes / file.size));
1245
+ const filled = Math.floor(percent / 10);
1246
+ const value = `[${"#".repeat(filled)}${"-".repeat(10 - filled)}] ${percent}% ${mb(event.bytes)} / ${mb(file.size)}`;
1247
+ if (interactive && (output.columns ?? 0) >= value.length) {
1248
+ clear();
1249
+ output.write(value);
1250
+ lineActive = true;
1251
+ } else line(`${label} ${value}`);
1252
+ }
1253
+ }
1254
+ };
1255
+ }
1256
+
1257
+ // distribution/bootstrap/status-ui.ts
1258
+ import { readFile as readFile7 } from "node:fs/promises";
1259
+ import { join as join6 } from "node:path";
1260
+ import { setTimeout as delay2 } from "node:timers/promises";
1261
+ var mib = (bytes) => typeof bytes === "number" && Number.isFinite(bytes) ? `${Math.round(bytes / 2 ** 20)} MiB` : "\u672A\u77E5";
1262
+ var pressureLabels = { normal: "\u6B63\u5E38", high: "\u504F\u9AD8", critical: "\u4E25\u91CD", unknown: "\u672A\u77E5" };
1263
+ async function statusLines(home) {
1264
+ const title = "\u706B\u706B \xB7 \u8FD0\u884C\u72B6\u6001";
1265
+ const installed = await readInstallation(home);
1266
+ if (!installed) return [title, "\u5C1A\u672A\u5B89\u88C5\u706B\u706B\u3002"];
1267
+ if (installed.status !== "ready") return [title, "\u5B89\u88C5\u5C1A\u672A\u5B8C\u6210\u3002"];
1268
+ let admission;
1269
+ try {
1270
+ admission = JSON.parse(await readFile7(join6(home, "state/default-admission.json"), "utf8"));
1271
+ } catch (error) {
1272
+ if (error.code !== "ENOENT") return [title, "\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\u8D44\u6E90\u72B6\u6001\u3002"];
1273
+ }
1274
+ const now = Date.now() / 1e3;
1275
+ if (!admission || admission.format !== 1 || admission.releaseDigest !== installed.releaseDigest || !Number.isFinite(admission.sampledAt) || !(admission.validUntil > now) || typeof admission.allowHeavy !== "boolean" || !admission.resources?.sample) {
1276
+ return [title, "\u6682\u65E0\u6709\u6548\u7684\u8FD0\u884C\u91C7\u6837\u3002\u8BF7\u68C0\u67E5\u706B\u706B\u670D\u52A1\u662F\u5426\u6B63\u5728\u8FD0\u884C\u3002"];
1277
+ }
1278
+ const { sample, reasons } = admission.resources;
1279
+ const needed = installed.release.budget.heavyJobHeadroomBytes;
1280
+ const gap = sample.availableMemoryBytes === null ? null : Math.max(0, needed - sample.availableMemoryBytes);
1281
+ const lines = [
1282
+ title,
1283
+ `\u540E\u53F0\u7814\u7A76 ${admission.allowHeavy ? "\u53EF\u63A5\u6536\u4EFB\u52A1" : "\u6682\u7F13\u65B0\u4EFB\u52A1"}`,
1284
+ `\u7CFB\u7EDF\u5185\u5B58 \u53EF\u7528 ${mib(sample.availableMemoryBytes)} / \u603B\u5BB9\u91CF ${mib(sample.effectiveMemoryBytes)}`,
1285
+ `\u4EFB\u52A1\u95E8\u69DB \u9884\u7559 ${mib(needed)}${gap ? `\uFF0C\u8FD8\u5DEE ${mib(gap)}` : ""}`,
1286
+ `\u5185\u5B58\u538B\u529B ${pressureLabels[sample.pressure] ?? "\u672A\u77E5"}`,
1287
+ `\u706B\u706B\u8FDB\u7A0B \u4F30\u7B97\u5360\u7528 ${mib(admission.rssSumEstimateBytes)}`
1288
+ ];
1289
+ if (!admission.allowHeavy) lines.push(reasons?.[0] ? `\u6682\u7F13\u539F\u56E0 ${reasons[0]}` : "\u6B63\u5728\u7B49\u5F85\u8FDE\u7EED\u5065\u5EB7\u91C7\u6837\u3002");
1290
+ lines.push(`\u91C7\u6837\u65F6\u95F4 ${new Date(admission.sampledAt * 1e3).toLocaleTimeString()}`);
1291
+ return lines;
1292
+ }
1293
+ async function showStatus(home, watch) {
1294
+ const output = process.stdout;
1295
+ if (!watch || !output.isTTY || process.env.TERM === "dumb") {
1296
+ output.write((await statusLines(home)).join("\n") + "\n");
1297
+ return;
1298
+ }
1299
+ const stop = new AbortController();
1300
+ const close = () => stop.abort();
1301
+ const input = process.stdin;
1302
+ const raw = input.isTTY;
1303
+ const wasRaw = input.isRaw;
1304
+ const onKey = (chunk) => {
1305
+ if (chunk.includes(3)) close();
1306
+ };
1307
+ process.once("SIGINT", close);
1308
+ process.once("SIGTERM", close);
1309
+ try {
1310
+ if (raw) {
1311
+ input.setRawMode(true);
1312
+ input.on("data", onKey);
1313
+ input.resume();
1314
+ }
1315
+ output.write("\x1B[?1049h\x1B[?25l");
1316
+ while (!stop.signal.aborted) {
1317
+ const lines = await statusLines(home);
1318
+ if (stop.signal.aborted) break;
1319
+ output.write("\x1B[H\x1B[0J" + [...lines, "", "\u6BCF 2 \u79D2\u5237\u65B0 \xB7 Ctrl+C \u5173\u95ED\u770B\u677F"].join("\n"));
1320
+ await delay2(2e3, void 0, { signal: stop.signal }).catch((error) => {
1321
+ if (!stop.signal.aborted) throw error;
1322
+ });
1323
+ }
1324
+ } finally {
1325
+ output.write("\x1B[?25h\x1B[?1049l");
1326
+ if (raw) {
1327
+ input.off("data", onKey);
1328
+ input.setRawMode(Boolean(wasRaw));
1329
+ input.pause();
1330
+ }
1331
+ process.off("SIGINT", close);
1332
+ process.off("SIGTERM", close);
1333
+ }
1334
+ }
1335
+
1049
1336
  // distribution/bootstrap/entry.ts
1050
1337
  function artifactSource(source, directory) {
1051
1338
  return async (url, options) => {
1339
+ const bundled = /^https:\/\/bundled\.invalid\/([a-f0-9]{64})\.tgz$/.exec(String(url));
1340
+ if (!directory && !bundled) return fetch(url, options);
1052
1341
  const artifact2 = source && [source.runtime, source.python, source.pythonDependencies].find((item) => item.url === String(url));
1053
- if (!directory && !artifact2?.bundled) return fetch(url, options);
1054
- if (!artifact2) throw new Error("\u672C\u673A\u4EA7\u7269\u76EE\u5F55\u7F3A\u5C11\u8BE5\u56FA\u5B9A\u53D1\u884C\u8D44\u6E90");
1055
- const root = directory ?? join6(dirname5(fileURLToPath(import.meta.url)), "assets");
1056
- return new Response(Readable.toWeb(createReadStream2(join6(root, artifact2.sha256 + ".tgz"), { signal: options?.signal ?? void 0 })));
1342
+ const sha256 = bundled?.[1] ?? artifact2?.sha256;
1343
+ if (!sha256) throw new Error("\u672C\u673A\u4EA7\u7269\u76EE\u5F55\u7F3A\u5C11\u8BE5\u56FA\u5B9A\u53D1\u884C\u8D44\u6E90");
1344
+ const root = directory ?? join7(dirname5(fileURLToPath(import.meta.url)), "assets");
1345
+ const path = join7(root, sha256 + ".tgz");
1346
+ const file = await open3(path).catch((error) => {
1347
+ if (error.code === "ENOENT" && bundled) throw new Error("\u7F3A\u5C11\u4E0A\u6B21\u5B89\u88C5\u4F7F\u7528\u7684\u6587\u4EF6\u3002\u8BF7\u7528\u4E0A\u6B21\u5B89\u88C5\u65F6\u7684\u7248\u672C\u7EE7\u7EED\u3002");
1348
+ throw error;
1349
+ });
1350
+ return new Response(Readable.toWeb(file.createReadStream({ signal: options?.signal ?? void 0 })));
1057
1351
  };
1058
1352
  }
1059
1353
  async function main(args = process.argv.slice(2)) {
1060
- let home = join6(homedir(), ".huohuo"), noOpen = false, command = "start";
1354
+ let home = join7(homedir(), ".huohuo"), noOpen = false, assumeYes = false, watch = false, command = "start";
1061
1355
  let releaseFile, artifactDirectory;
1062
1356
  for (let i = 0; i < args.length; i++) {
1063
1357
  if (args[i] === "--home") {
@@ -1069,19 +1363,23 @@ async function main(args = process.argv.slice(2)) {
1069
1363
  if (option === "--release") releaseFile = resolve5(args[++i]);
1070
1364
  else artifactDirectory = resolve5(args[++i]);
1071
1365
  } else if (args[i] === "--no-open") noOpen = true;
1366
+ else if (args[i] === "--yes") assumeYes = true;
1367
+ else if (args[i] === "--watch") watch = true;
1072
1368
  else if (args[i] === "--version") {
1073
- const packageFile = join6(dirname5(fileURLToPath(import.meta.url)), "package.json");
1074
- const manifest = JSON.parse(await readFile7(packageFile, "utf8"));
1369
+ const packageFile = join7(dirname5(fileURLToPath(import.meta.url)), "package.json");
1370
+ const manifest = JSON.parse(await readFile8(packageFile, "utf8"));
1075
1371
  console.log(`${manifest.name} ${manifest.version}${manifest.private ? "\uFF08\u672C\u5730\u5019\u9009\uFF09" : ""}`);
1076
1372
  return;
1077
- } else if (["start", "doctor", "stop", "upgrade", "uninstall"].includes(args[i])) command = args[i];
1078
- else throw new Error("\u7528\u6CD5\uFF1Ahuohuo [start|doctor|stop|upgrade|uninstall] [--home \u76EE\u5F55] [--no-open]");
1373
+ } else if (["start", "status", "doctor", "stop", "upgrade", "uninstall"].includes(args[i])) command = args[i];
1374
+ else throw new Error("\u7528\u6CD5\uFF1Ahuohuo [start|status|doctor|stop|upgrade|uninstall] [--home \u76EE\u5F55] [--no-open]");
1079
1375
  }
1376
+ if (watch && command !== "status") throw new Error("--watch \u8BF7\u4E0E status \u4E00\u8D77\u4F7F\u7528");
1080
1377
  try {
1081
1378
  home = await realpath5(home);
1082
1379
  } catch (e) {
1083
1380
  if (e.code !== "ENOENT") throw e;
1084
1381
  }
1382
+ if (command === "status") return showStatus(home, watch);
1085
1383
  let record = await readInstallation(home);
1086
1384
  const labels = {
1087
1385
  preflight: "\u68C0\u67E5\u672C\u673A\u73AF\u5883",
@@ -1089,15 +1387,22 @@ async function main(args = process.argv.slice(2)) {
1089
1387
  "install-runtime": "\u51C6\u5907 Python \u8FD0\u884C\u73AF\u5883",
1090
1388
  "install-dependencies": "\u5B89\u88C5\u706B\u706B\u4E0E\u56FA\u5B9A\u4F9D\u8D56",
1091
1389
  "assemble-profiles": "\u914D\u7F6E\u5BF9\u8BDD\u4E0E\u540E\u53F0\u7814\u7A76",
1092
- verify: "\u68C0\u67E5\u5B89\u88C5\u7ED3\u679C"
1390
+ verify: "\u68C0\u67E5\u5B89\u88C5\u7ED3\u679C",
1391
+ "resume-current-package": "\u4E0A\u6B21\u5B89\u88C5\u5C1A\u672A\u5199\u5165\u8FD0\u884C\u73AF\u5883\uFF0C\u5C06\u4F7F\u7528\u5F53\u524D\u7248\u672C\u91CD\u65B0\u68C0\u67E5"
1093
1392
  };
1094
- const progress = (stage) => console.log(`\u706B\u706B\uFF1A${labels[stage] ?? stage}`);
1393
+ const installStages = ["preflight", "download", "install-runtime", "install-dependencies", "assemble-profiles", "verify"];
1394
+ const progress = (stage) => {
1395
+ const index = command === "start" ? installStages.indexOf(stage) : -1;
1396
+ const step = index < 0 ? "" : `[${index + 1}/${installStages.length}] `;
1397
+ console.log(`\u706B\u706B\uFF1A${step}${labels[stage] ?? stage}`);
1398
+ };
1399
+ const ui = installUI(assumeYes);
1095
1400
  if (command === "upgrade") {
1096
- const candidate = JSON.parse(await readFile7(releaseFile ?? join6(dirname5(fileURLToPath(import.meta.url)), "release.json"), "utf8"));
1097
- await upgrade(home, candidate, { fetcher: artifactSource(candidate, artifactDirectory), progress });
1401
+ const candidate = JSON.parse(await readFile8(releaseFile ?? join7(dirname5(fileURLToPath(import.meta.url)), "release.json"), "utf8"));
1402
+ await upgrade(home, candidate, { fetcher: artifactSource(candidate, artifactDirectory), progress, ...ui });
1098
1403
  const current = await readInstallation(home);
1099
1404
  if (await control(home, current.releaseDigest, noOpen ? "status" : "open")) return;
1100
- await run(process.execPath, [join6(installationPaths(home, current.release).runtime, "lib/distribution/managed.js"), "start", home, ...noOpen ? ["--no-open"] : []], home);
1405
+ await run(process.execPath, [join7(installationPaths(home, current.release).runtime, "lib/distribution/managed.js"), "start", home, ...noOpen ? ["--no-open"] : []], home);
1101
1406
  return;
1102
1407
  }
1103
1408
  if (command === "uninstall") {
@@ -1115,7 +1420,7 @@ async function main(args = process.argv.slice(2)) {
1115
1420
  return;
1116
1421
  }
1117
1422
  const paths = installationPaths(home, record.release);
1118
- const targets = [home, paths.directory, paths.environment, join6(home, "instances/default")];
1423
+ const targets = [home, paths.directory, paths.environment, join7(home, "instances/default")];
1119
1424
  const resources = evaluateResources(await sampleResources(targets), record.release.budget, targets.map((path) => ({ path, bytes: 0 })), "start");
1120
1425
  console.log(JSON.stringify({
1121
1426
  home,
@@ -1136,7 +1441,7 @@ async function main(args = process.argv.slice(2)) {
1136
1441
  }
1137
1442
  const runtime = installationPaths(home, record.release).runtime;
1138
1443
  try {
1139
- await readFile7(join6(runtime, "lib/distribution/managed.js"));
1444
+ await readFile8(join7(runtime, "lib/distribution/managed.js"));
1140
1445
  } catch (e) {
1141
1446
  if (e.code !== "ENOENT") throw e;
1142
1447
  record = void 0;
@@ -1144,24 +1449,25 @@ async function main(args = process.argv.slice(2)) {
1144
1449
  }
1145
1450
  if (!record || record.status !== "ready") {
1146
1451
  let catalog = null;
1147
- if (!record) {
1452
+ if (!record || ["preflight", "download"].includes(record.stage)) {
1148
1453
  try {
1149
- catalog = JSON.parse(await readFile7(releaseFile ?? join6(dirname5(fileURLToPath(import.meta.url)), "release.json"), "utf8"));
1454
+ catalog = JSON.parse(await readFile8(releaseFile ?? join7(dirname5(fileURLToPath(import.meta.url)), "release.json"), "utf8"));
1150
1455
  } catch (e) {
1151
1456
  if (e.code !== "ENOENT") throw e;
1152
1457
  }
1153
1458
  }
1154
1459
  const source = (await readInstallation(home))?.release ?? catalog;
1155
1460
  const fetcher = artifactSource(source, artifactDirectory);
1156
- record = await install(home, catalog, { fetcher, progress });
1461
+ const bundleDirectory = !releaseFile && !artifactDirectory ? join7(dirname5(fileURLToPath(import.meta.url)), "assets") : void 0;
1462
+ record = await install(home, catalog, { fetcher, progress, bundleDirectory, ...ui });
1157
1463
  }
1158
1464
  const installedHome = record.home;
1159
- await run(process.execPath, [join6(installationPaths(installedHome, record.release).runtime, "lib/distribution/managed.js"), "start", installedHome, ...noOpen ? ["--no-open"] : []], installedHome);
1465
+ await run(process.execPath, [join7(installationPaths(installedHome, record.release).runtime, "lib/distribution/managed.js"), "start", installedHome, ...noOpen ? ["--no-open"] : []], installedHome);
1160
1466
  }
1161
1467
  if (process.argv[1] && await realpath5(resolve5(process.argv[1])).catch(() => "") === await realpath5(fileURLToPath(import.meta.url))) {
1162
1468
  main().catch((error) => {
1163
1469
  console.error(error instanceof Error ? error.message : "\u5B89\u88C5\u6216\u542F\u52A8\u5931\u8D25");
1164
- process.exitCode = 1;
1470
+ if (error?.code !== "INSTALL_CANCELLED") process.exitCode = 1;
1165
1471
  });
1166
1472
  }
1167
1473
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pab-iprg/huohuo",
3
- "version": "1.0.0-preview.2",
3
+ "version": "1.0.0-preview.4",
4
4
  "type": "module",
5
5
  "description": "火火投研伙伴安装入口",
6
6
  "bin": {
package/release.json CHANGED
@@ -13,11 +13,11 @@
13
13
  "runtime": {
14
14
  "name": "@huohuo/research-agent",
15
15
  "version": "1.0.0",
16
- "url": "https://bundled.invalid/3126256d1e3ad445b74f67e0a97a339774bc23cdcc5249578f1c9e1b7f3d8f6d.tgz",
17
- "sha256": "3126256d1e3ad445b74f67e0a97a339774bc23cdcc5249578f1c9e1b7f3d8f6d",
18
- "integrity": "sha512-KmCItvQmE0LZbNiKdrWeYsUCD4p4YaBfiS+OOwsepOix03nON46tgpFgOeT/effrEWzPROmuwI64IU8g4loTYw==",
19
- "downloadBytes": 2542537,
20
- "expandedBytes": 8237483,
16
+ "url": "https://bundled.invalid/550095205f2f92e0641b286f15fbb01ece0b29339ecf7c5ee18539a2b7f5def8.tgz",
17
+ "sha256": "550095205f2f92e0641b286f15fbb01ece0b29339ecf7c5ee18539a2b7f5def8",
18
+ "integrity": "sha512-AI87lAFcPyGM5qtQ4B2Ji3CJWE10twn48t0tSnuEeIxlVAKVEJBWRe7aSCkOKwLvpeai8CRts50oEMh0juDWrA==",
19
+ "downloadBytes": 2548683,
20
+ "expandedBytes": 8255144,
21
21
  "bundled": true
22
22
  },
23
23
  "python": {
@@ -37,7 +37,8 @@
37
37
  "sha256": "4f008fe4491617729554e056ad59eee686ccc5d95c9f1d5259da705bf054ce21",
38
38
  "downloadBytes": 10235,
39
39
  "expandedBytes": 15007,
40
- "bundled": true
40
+ "bundled": true,
41
+ "upstreamWheelCount": 39
41
42
  },
42
43
  "dsh": {
43
44
  "version": "0.2.1-rc.2",
@@ -56,7 +57,8 @@
56
57
  "sampleMaxAgeMs": 10000,
57
58
  "installTemporaryBytes": 1073741824,
58
59
  "dependenciesBytes": 4294967296,
59
- "backupBytes": 0
60
+ "backupBytes": 0,
61
+ "observedInstallRssBytes": 1904771072
60
62
  },
61
63
  "schema": {
62
64
  "minimum": 1,