chainlesschain 0.156.6 → 0.156.7

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 (113) hide show
  1. package/README.md +24 -0
  2. package/package.json +2 -1
  3. package/src/assets/web-panel/assets/{ActionButton-Dme4LGax.js → ActionButton-Cs4QdjYb.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-B3-5BjRm.js → Analytics-Xot0e9TT.js} +1 -1
  5. package/src/assets/web-panel/assets/{AppLayout-DvVLRyPs.js → AppLayout-3qsE1-pz.js} +1 -1
  6. package/src/assets/web-panel/assets/{BaseInput-wLmjCc9u.js → BaseInput-Tg40P4JM.js} +1 -1
  7. package/src/assets/web-panel/assets/{Checkbox-BfbEUJDW.js → Checkbox-CheA2Ety.js} +1 -1
  8. package/src/assets/web-panel/assets/{Col-HJI40OzO.js → Col-Cdfsmnaq.js} +1 -1
  9. package/src/assets/web-panel/assets/{Compact-ADVAwcbQ.js → Compact-D3LSgEpW.js} +1 -1
  10. package/src/assets/web-panel/assets/{Cron-DcNB5TYu.js → Cron-9MV6k-MV.js} +1 -1
  11. package/src/assets/web-panel/assets/{Dashboard-p_Wuj0Un.js → Dashboard-A1TZC5_t.js} +1 -1
  12. package/src/assets/web-panel/assets/{Dropdown-CrXGzreQ.js → Dropdown-DZxUTZvw.js} +1 -1
  13. package/src/assets/web-panel/assets/{FormItemContext-B97Dibo2.js → FormItemContext-C3_-j_SR.js} +1 -1
  14. package/src/assets/web-panel/assets/{Git-90CPsOOr.js → Git-Cw-gW-kh.js} +1 -1
  15. package/src/assets/web-panel/assets/{Memory-B_3zNQNB.js → Memory-CMweTJyn.js} +1 -1
  16. package/src/assets/web-panel/assets/{Notes-DNQz9UXh.js → Notes-B_W3BfZF.js} +1 -1
  17. package/src/assets/web-panel/assets/{Organization-CgXUnp-W.js → Organization-Dz_jGbAM.js} +1 -1
  18. package/src/assets/web-panel/assets/{Overflow-BVsn6SM5.js → Overflow-Dka3nWV9.js} +1 -1
  19. package/src/assets/web-panel/assets/{Permissions-DIFqcnjU.js → Permissions-DvXVIlHX.js} +1 -1
  20. package/src/assets/web-panel/assets/{Providers-mscN7CK5.js → Providers-DUfX_ynl.js} +1 -1
  21. package/src/assets/web-panel/assets/{Row-BFUWxIkx.js → Row-DZhDSo2Q.js} +1 -1
  22. package/src/assets/web-panel/assets/{RssFeed-Dpa4h-q_.js → RssFeed-CHQpUl3h.js} +1 -1
  23. package/src/assets/web-panel/assets/{Security-DR6HKo_S.js → Security-FUSOn89T.js} +1 -1
  24. package/src/assets/web-panel/assets/{Skeleton-VNikEgM4.js → Skeleton-DxmZ7zRw.js} +1 -1
  25. package/src/assets/web-panel/assets/{Templates-Ny_4GO6a.js → Templates-BVbmyn38.js} +1 -1
  26. package/src/assets/web-panel/assets/{Trigger-C7MTh_xj.js → Trigger-xAvohiq9.js} +1 -1
  27. package/src/assets/web-panel/assets/{VideoEditing-BNRFHgJ9.js → VideoEditing-BUWYQv2y.js} +1 -1
  28. package/src/assets/web-panel/assets/{Wallet-BUfg4IAx.js → Wallet-BDYdEwFf.js} +1 -1
  29. package/src/assets/web-panel/assets/{WebAuthn-Cia89OyQ.js → WebAuthn-CvpuagtK.js} +1 -1
  30. package/src/assets/web-panel/assets/{WorkflowEditor-C1OsMtqv.js → WorkflowEditor-BR7W5cjw.js} +1 -1
  31. package/src/assets/web-panel/assets/{colors-C_wDMX2Q.js → colors-C5kDbQCi.js} +1 -1
  32. package/src/assets/web-panel/assets/{compact-item-C1ikzEN-.js → compact-item-Bo_1zDrX.js} +1 -1
  33. package/src/assets/web-panel/assets/{createContext-XExBTk9v.js → createContext-CniPpJsG.js} +1 -1
  34. package/src/assets/web-panel/assets/{hasIn-mXvd_Kdq.js → hasIn-ClDc6Sz8.js} +1 -1
  35. package/src/assets/web-panel/assets/{index-D6Hyy0Bc.js → index--lcO-bOn.js} +1 -1
  36. package/src/assets/web-panel/assets/{index-lPIeHtHE.js → index-6tQekF0Y.js} +1 -1
  37. package/src/assets/web-panel/assets/{index-BfncNR8d.js → index-8qWxPHSb.js} +1 -1
  38. package/src/assets/web-panel/assets/{index-C53dnYiq.js → index-B7nGNm_C.js} +1 -1
  39. package/src/assets/web-panel/assets/{index-CMYADk0v.js → index-B8y0NO-M.js} +1 -1
  40. package/src/assets/web-panel/assets/{index-DMcLOtIo.js → index-BAlSSCbs.js} +1 -1
  41. package/src/assets/web-panel/assets/{index-DJkIheU6.js → index-BCXFoTAw.js} +1 -1
  42. package/src/assets/web-panel/assets/{index-kLUQdSDJ.js → index-BHruTebo.js} +1 -1
  43. package/src/assets/web-panel/assets/{index-1ZqkTPt2.js → index-BJx6C3J8.js} +1 -1
  44. package/src/assets/web-panel/assets/{index-CbpKJ2W0.js → index-BUTCJTbj.js} +1 -1
  45. package/src/assets/web-panel/assets/{index-B6P9mWuk.js → index-BYShDlZ0.js} +1 -1
  46. package/src/assets/web-panel/assets/{index-D9tzxSFs.js → index-Bv2OmZAS.js} +1 -1
  47. package/src/assets/web-panel/assets/{index-BFFb9yPd.js → index-C92K4iDE.js} +1 -1
  48. package/src/assets/web-panel/assets/{index-v4Oi0d0l.js → index-CCdb36il.js} +1 -1
  49. package/src/assets/web-panel/assets/{index-B-TI0cZ2.js → index-CKR2ITFk.js} +1 -1
  50. package/src/assets/web-panel/assets/{index-fLUJs2Sr.js → index-CMcGcbea.js} +1 -1
  51. package/src/assets/web-panel/assets/{index-BirLVqrC.js → index-CTetsi8W.js} +1 -1
  52. package/src/assets/web-panel/assets/{index-LpE6Six-.js → index-CXxLp7Aw.js} +1 -1
  53. package/src/assets/web-panel/assets/{index-qtDQSqTG.js → index-CeSV8f3b.js} +1 -1
  54. package/src/assets/web-panel/assets/{index-DwMlStra.js → index-Ch5mAXeh.js} +1 -1
  55. package/src/assets/web-panel/assets/{index-BOqmUcij.js → index-CwhWEkmA.js} +1 -1
  56. package/src/assets/web-panel/assets/{index-D_oSE2Nk.js → index-D2fe9a6f.js} +1 -1
  57. package/src/assets/web-panel/assets/index-D3UDIt7h.js +1 -0
  58. package/src/assets/web-panel/assets/{index-CxwU-EjS.js → index-D90sLw5Q.js} +1 -1
  59. package/src/assets/web-panel/assets/{index-D1eekAaa.js → index-D9bolkbl.js} +1 -1
  60. package/src/assets/web-panel/assets/{index-BL27IhbN.js → index-DNY0K7iI.js} +1 -1
  61. package/src/assets/web-panel/assets/{index-Du7KGlCP.js → index-DSiHmo4b.js} +1 -1
  62. package/src/assets/web-panel/assets/{index-CttcpCq_.js → index-DTYnvYqB.js} +1 -1
  63. package/src/assets/web-panel/assets/{index-jg5cpQg9.js → index-DaLYbr0E.js} +1 -1
  64. package/src/assets/web-panel/assets/{index-DYLE4bnY.js → index-DkSNIJhM.js} +1 -1
  65. package/src/assets/web-panel/assets/{index-DZjQgmBq.js → index-DnQkqOZj.js} +1 -1
  66. package/src/assets/web-panel/assets/{index-DaMG8ksh.js → index-Dn_OQQaV.js} +3 -3
  67. package/src/assets/web-panel/assets/{index-Dz6RDRcu.js → index-Dtfrhky9.js} +1 -1
  68. package/src/assets/web-panel/assets/{index-CTle6zcb.js → index-JNbd08FN.js} +1 -1
  69. package/src/assets/web-panel/assets/index-PT376OZM.js +1 -0
  70. package/src/assets/web-panel/assets/{index-BBOVB9YK.js → index-cIgCeEqo.js} +1 -1
  71. package/src/assets/web-panel/assets/{index-a0qENb5U.js → index-vBi4x_6g.js} +1 -1
  72. package/src/assets/web-panel/assets/{index-C5Zv4fBx.js → index-xL8gcpmy.js} +1 -1
  73. package/src/assets/web-panel/assets/{initDefaultProps-DOj2K4bh.js → initDefaultProps-DMfJaUzk.js} +1 -1
  74. package/src/assets/web-panel/assets/{motion-joGf7r-l.js → motion-sEbWmOWo.js} +1 -1
  75. package/src/assets/web-panel/assets/{move-Cwb6tumJ.js → move-DIWXVs--.js} +1 -1
  76. package/src/assets/web-panel/assets/{omit-CPycjJ8C.js → omit-D7mkMPhu.js} +1 -1
  77. package/src/assets/web-panel/assets/{pickAttrs-CnibXC3T.js → pickAttrs-B25NUX4k.js} +1 -1
  78. package/src/assets/web-panel/assets/{placementArrow-DWcIO1y4.js → placementArrow-By1Bkq1d.js} +1 -1
  79. package/src/assets/web-panel/assets/{responsiveObserve-C5giLhLf.js → responsiveObserve-B3aCQz5r.js} +1 -1
  80. package/src/assets/web-panel/assets/{slide-zwgmm7vM.js → slide-eR-f56FQ.js} +1 -1
  81. package/src/assets/web-panel/assets/{statusUtils-CK8tJSHq.js → statusUtils-zcNWczhN.js} +1 -1
  82. package/src/assets/web-panel/assets/{styleChecker-BzLSEXyu.js → styleChecker-u9Z0IfRy.js} +1 -1
  83. package/src/assets/web-panel/assets/{transition-D4AbuDdO.js → transition-gRK4XSlW.js} +1 -1
  84. package/src/assets/web-panel/assets/{useConfigInject-ImjEZhXr.js → useConfigInject-ZEunuNHN.js} +1 -1
  85. package/src/assets/web-panel/assets/{useFlexGapSupport-Cd-PoTMl.js → useFlexGapSupport-BpbEJfeh.js} +1 -1
  86. package/src/assets/web-panel/assets/{vnode-DAWimP6X.js → vnode-DVHvXn9F.js} +1 -1
  87. package/src/assets/web-panel/assets/{zoom-u6SXbmzZ.js → zoom-ByzgJIn6.js} +1 -1
  88. package/src/assets/web-panel/index.html +1 -1
  89. package/src/commands/pack.js +463 -0
  90. package/src/commands/ui.js +6 -0
  91. package/src/gateways/ws/ws-server.js +29 -3
  92. package/src/index.js +34 -1
  93. package/src/lib/governance-v2-helpers.js +109 -0
  94. package/src/lib/packer/config-template-builder.js +151 -0
  95. package/src/lib/packer/errors.js +30 -0
  96. package/src/lib/packer/index.js +383 -0
  97. package/src/lib/packer/manifest-writer.js +93 -0
  98. package/src/lib/packer/native-prebuild-collector.js +305 -0
  99. package/src/lib/packer/pack-update-applier.js +203 -0
  100. package/src/lib/packer/pack-update-checker.js +185 -0
  101. package/src/lib/packer/pack-update-downloader.js +162 -0
  102. package/src/lib/packer/pkg-config-generator.js +325 -0
  103. package/src/lib/packer/pkg-runner.js +139 -0
  104. package/src/lib/packer/precheck.js +273 -0
  105. package/src/lib/packer/project-assets-collector.js +0 -0
  106. package/src/lib/packer/smoke-runner.js +339 -0
  107. package/src/lib/packer/web-panel-builder.js +157 -0
  108. package/src/lib/web-ui-server.js +95 -2
  109. package/src/lib/ws-server.js +1 -0
  110. package/src/runtime/agent-runtime.js +1 -0
  111. package/src/runtime/policies/agent-policy.js +1 -0
  112. package/src/assets/web-panel/assets/index-DQgS_8Fd.js +0 -1
  113. package/src/assets/web-panel/assets/index-f4W8Sok0.js +0 -1
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Phase 4: native-prebuild-collector
3
+ *
4
+ * Locate prebuilt .node files for the optional native SQLite drivers
5
+ * (better-sqlite3-multiple-ciphers, better-sqlite3) and copy them into a
6
+ * per-target prebuilds/ folder that pkg will embed as assets.
7
+ *
8
+ * None of these are required: the runtime falls back to sql.js (WASM) —
9
+ * see packages/core-db/lib/database-manager.js `loadSQLiteDriver`. That
10
+ * fallback is what makes the pack runnable on platforms where the native
11
+ * module was never built (non-standard macOS/Linux hardware, CI images,
12
+ * ABI skew). We still try hard to include a native copy because it is
13
+ * 10-100x faster for typical workloads.
14
+ *
15
+ * Target identifier format mirrors @yao-pkg/pkg: nodeXX-<os>-<arch>
16
+ * - nodeXX -> module ABI (node20=115, node22=127)
17
+ * - os -> win, macos, linux, alpine
18
+ * - arch -> x64, arm64
19
+ */
20
+
21
+ import fs from "node:fs";
22
+ import path from "node:path";
23
+ import { createRequire } from "node:module";
24
+ import { PackError, EXIT } from "./errors.js";
25
+
26
+ /**
27
+ * Modules we attempt to locate; all optional because sql.js covers the
28
+ * fallback. Missing ones are reported but never abort the build.
29
+ */
30
+ const TARGET_MODULES = [
31
+ { name: "better-sqlite3-multiple-ciphers", required: false },
32
+ { name: "better-sqlite3", required: false },
33
+ ];
34
+
35
+ /**
36
+ * @param {object} ctx
37
+ * @param {string} ctx.cliRoot
38
+ * @param {string[]} ctx.targets e.g. ["node20-win-x64"]
39
+ * @param {string} ctx.tempDir build temp dir
40
+ * @returns {{ prebuildsDir: string|null, collected: Array, missing: Array }}
41
+ */
42
+ export function collectPrebuilds(ctx) {
43
+ const { cliRoot, targets, tempDir } = ctx;
44
+ const collected = [];
45
+ const missing = [];
46
+
47
+ if (!targets || targets.length === 0) {
48
+ return { prebuildsDir: null, collected, missing };
49
+ }
50
+
51
+ const prebuildsDir = path.join(tempDir, "prebuilds");
52
+ fs.mkdirSync(prebuildsDir, { recursive: true });
53
+
54
+ for (const target of targets) {
55
+ const platformKey = pkgTargetToPlatformKey(target);
56
+ if (!platformKey) {
57
+ throw new PackError(`Unrecognized pkg target: "${target}"`, EXIT.NATIVE);
58
+ }
59
+ const targetDir = path.join(prebuildsDir, platformKey);
60
+ fs.mkdirSync(targetDir, { recursive: true });
61
+
62
+ for (const mod of TARGET_MODULES) {
63
+ const sourceNode = findInstalledNodeFile(cliRoot, mod.name);
64
+ if (!sourceNode) {
65
+ missing.push({ target, module: mod.name, required: mod.required });
66
+ continue;
67
+ }
68
+ const destFile = path.join(targetDir, `${mod.name}.node`);
69
+ fs.copyFileSync(sourceNode, destFile);
70
+ collected.push({
71
+ target,
72
+ module: mod.name,
73
+ from: sourceNode,
74
+ to: destFile,
75
+ });
76
+ }
77
+ }
78
+
79
+ // Nothing is hard-required anymore — the runtime falls back to sql.js
80
+ // (WASM) when no native driver loads. Callers should surface `missing`
81
+ // to the user so they understand the performance trade-off.
82
+ const sqlJs = collectSqlJsAssets(cliRoot, prebuildsDir);
83
+
84
+ return { prebuildsDir, collected, missing, sqlJs };
85
+ }
86
+
87
+ /**
88
+ * Copy sql.js runtime assets (sql-wasm.js + sql-wasm.wasm) into
89
+ * prebuildsDir/sqljs/ so the packed binary can find them. pkg itself
90
+ * cannot embed .wasm via dynamic require, so we treat it as an asset
91
+ * and load it from a disk path at runtime (database-manager falls back
92
+ * to sql.js automatically when natives are absent).
93
+ *
94
+ * Returns { assetDir, copied:[{from,to}] } or null when sql.js isn't
95
+ * installed — in that case the runtime will hit the "no SQLite driver"
96
+ * error, which the user will have seen flagged during pack.
97
+ */
98
+ function collectSqlJsAssets(cliRoot, prebuildsDir) {
99
+ for (const dir of listCandidateModuleDirs(cliRoot, "sql.js")) {
100
+ const dist = path.join(dir, "dist");
101
+ if (!fs.existsSync(dist)) continue;
102
+ const js = path.join(dist, "sql-wasm.js");
103
+ const wasm = path.join(dist, "sql-wasm.wasm");
104
+ if (!fs.existsSync(js) || !fs.existsSync(wasm)) continue;
105
+
106
+ const assetDir = path.join(prebuildsDir, "sqljs");
107
+ fs.mkdirSync(assetDir, { recursive: true });
108
+ const copied = [];
109
+ for (const src of [js, wasm]) {
110
+ const to = path.join(assetDir, path.basename(src));
111
+ fs.copyFileSync(src, to);
112
+ copied.push({ from: src, to });
113
+ }
114
+ return { assetDir, copied };
115
+ }
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Map "node20-win-x64" -> "win32-x64".
121
+ * Returns null for unrecognized targets.
122
+ */
123
+ export function pkgTargetToPlatformKey(target) {
124
+ // node20-win-x64
125
+ const m = /^node\d+-([a-z]+)-([a-z0-9]+)$/.exec(target);
126
+ if (!m) return null;
127
+ const osMap = {
128
+ win: "win32",
129
+ macos: "darwin",
130
+ linux: "linux",
131
+ alpine: "linux",
132
+ };
133
+ const os = osMap[m[1]];
134
+ if (!os) return null;
135
+ return `${os}-${m[2]}`;
136
+ }
137
+
138
+ /**
139
+ * Collect ALL candidate install dirs for a module reachable from cliRoot.
140
+ * In a monorepo a module can exist in several places:
141
+ * - the hoisted copy at <repoRoot>/node_modules/<mod> (often source-only,
142
+ * no compiled binary)
143
+ * - a transitive copy inside a sibling workspace package such as
144
+ * <repoRoot>/packages/core-db/node_modules/<mod> (this one has the
145
+ * actual compiled .node)
146
+ * - the canonical resolved path returned by createRequire
147
+ *
148
+ * We return the union (in priority order) so the caller can pick the
149
+ * first candidate that actually has a usable .node binary, sidestepping
150
+ * the "resolver returns the source-only hoisted copy" trap.
151
+ */
152
+ function listCandidateModuleDirs(cliRoot, moduleName) {
153
+ const out = [];
154
+ const seen = new Set();
155
+ const push = (dir) => {
156
+ if (!dir) return;
157
+ const norm = path.resolve(dir);
158
+ if (seen.has(norm)) return;
159
+ if (!fs.existsSync(path.join(norm, "package.json"))) return;
160
+ seen.add(norm);
161
+ out.push(norm);
162
+ };
163
+
164
+ // 1. Standard node resolution
165
+ try {
166
+ const req = createRequire(path.join(cliRoot, "package.json"));
167
+ push(path.dirname(req.resolve(`${moduleName}/package.json`)));
168
+ } catch {
169
+ /* skip */
170
+ }
171
+
172
+ // 2. Walk up parent chain
173
+ let cur = cliRoot;
174
+ while (true) {
175
+ push(path.join(cur, "node_modules", moduleName));
176
+ const parent = path.dirname(cur);
177
+ if (parent === cur) break;
178
+ cur = parent;
179
+ }
180
+
181
+ // 3. Sibling workspace packages may carry a nested copy with the
182
+ // binary (common for monorepos with optional deps that the root
183
+ // install hoisted but never built).
184
+ const repoRoot = findRepoRoot(cliRoot);
185
+ if (repoRoot) {
186
+ const packagesDir = path.join(repoRoot, "packages");
187
+ if (fs.existsSync(packagesDir)) {
188
+ let workspaces;
189
+ try {
190
+ workspaces = fs.readdirSync(packagesDir, { withFileTypes: true });
191
+ } catch {
192
+ workspaces = [];
193
+ }
194
+ for (const w of workspaces) {
195
+ if (!w.isDirectory()) continue;
196
+ push(path.join(packagesDir, w.name, "node_modules", moduleName));
197
+ }
198
+ }
199
+ }
200
+
201
+ // 4. Direct deps' nested node_modules (covers any other layout)
202
+ const directDeps = path.join(cliRoot, "node_modules");
203
+ if (fs.existsSync(directDeps)) {
204
+ let entries;
205
+ try {
206
+ entries = fs.readdirSync(directDeps, { withFileTypes: true });
207
+ } catch {
208
+ entries = [];
209
+ }
210
+ for (const e of entries) {
211
+ if (!e.isDirectory()) continue;
212
+ if (e.name.startsWith("@")) {
213
+ const scopeDir = path.join(directDeps, e.name);
214
+ let scoped;
215
+ try {
216
+ scoped = fs.readdirSync(scopeDir, { withFileTypes: true });
217
+ } catch {
218
+ scoped = [];
219
+ }
220
+ for (const s of scoped) {
221
+ if (!s.isDirectory()) continue;
222
+ push(path.join(scopeDir, s.name, "node_modules", moduleName));
223
+ }
224
+ } else {
225
+ push(path.join(directDeps, e.name, "node_modules", moduleName));
226
+ }
227
+ }
228
+ }
229
+
230
+ return out;
231
+ }
232
+
233
+ /** Crude monorepo root detection: nearest ancestor with a `packages/` dir. */
234
+ function findRepoRoot(start) {
235
+ let cur = start;
236
+ while (true) {
237
+ if (fs.existsSync(path.join(cur, "packages"))) return cur;
238
+ const parent = path.dirname(cur);
239
+ if (parent === cur) return null;
240
+ cur = parent;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Return the path to a usable .node file inside the given module dir,
246
+ * or null if this copy has no compiled binary (e.g. source-only hoist).
247
+ */
248
+ function nodeFileInModuleDir(moduleDir, moduleName) {
249
+ const candidates = [
250
+ path.join(moduleDir, "build", "Release", `${moduleName}.node`),
251
+ path.join(
252
+ moduleDir,
253
+ "build",
254
+ "Release",
255
+ `${moduleName.replace(/-/g, "_")}.node`,
256
+ ),
257
+ // Generic fallback — a native fork (e.g. better-sqlite3-multiple-ciphers)
258
+ // often keeps its upstream binary name (better_sqlite3.node). Walk the
259
+ // whole build/Release dir before giving up.
260
+ path.join(moduleDir, "build", "Release"),
261
+ path.join(moduleDir, "prebuilds"),
262
+ ];
263
+
264
+ for (const c of candidates) {
265
+ if (!fs.existsSync(c)) continue;
266
+ const st = fs.statSync(c);
267
+ if (st.isFile()) return c;
268
+ if (st.isDirectory()) {
269
+ const nodeFile = findFirstNodeFile(c);
270
+ if (nodeFile) return nodeFile;
271
+ }
272
+ }
273
+ return null;
274
+ }
275
+
276
+ /**
277
+ * Walk the candidate install dirs and return the first .node file found.
278
+ * This is what callers actually want.
279
+ */
280
+ function findInstalledNodeFile(cliRoot, moduleName) {
281
+ for (const dir of listCandidateModuleDirs(cliRoot, moduleName)) {
282
+ const nodeFile = nodeFileInModuleDir(dir, moduleName);
283
+ if (nodeFile) return nodeFile;
284
+ }
285
+ return null;
286
+ }
287
+
288
+ function findFirstNodeFile(dir) {
289
+ const stack = [dir];
290
+ while (stack.length) {
291
+ const cur = stack.pop();
292
+ let entries;
293
+ try {
294
+ entries = fs.readdirSync(cur, { withFileTypes: true });
295
+ } catch {
296
+ continue;
297
+ }
298
+ for (const e of entries) {
299
+ const full = path.join(cur, e.name);
300
+ if (e.isDirectory()) stack.push(full);
301
+ else if (e.name.endsWith(".node")) return full;
302
+ }
303
+ }
304
+ return null;
305
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Phase 5c: replace the currently-running packed exe with a downloaded new
3
+ * artifact. This is the third step of the OTA chain:
4
+ * Phase 5a (check) → Phase 5b (download+verify) → Phase 5c (apply)
5
+ *
6
+ * The two platform flavors behave very differently:
7
+ *
8
+ * POSIX (Linux / macOS): the kernel keeps the running exe's inode open
9
+ * independently of its directory entry. We can `rename(new, target)`
10
+ * while the old process is still executing — the new bytes are on disk
11
+ * under the target path, the old process continues running the old
12
+ * inode until it exits, and a fresh spawn of `target` picks up the new
13
+ * code. Clean, atomic, no sidecar.
14
+ *
15
+ * Windows: the OS refuses to overwrite a running `.exe`. The only
16
+ * reliable pattern is a sidecar script that (1) waits for the parent
17
+ * PID to exit, (2) moves `<new>` → `<target>`, (3) optionally restarts.
18
+ * We write a tiny `.cmd` to `%TEMP%` and spawn it detached; the parent
19
+ * then exits on its own.
20
+ *
21
+ * All file-system mutations can be gated by `dryRun: true`, which just
22
+ * returns the plan (commands / paths the applier would have executed).
23
+ * Tests use `platform: 'win32' | 'posix'` to exercise both branches on any
24
+ * host without touching the running exe.
25
+ */
26
+
27
+ import fs from "node:fs";
28
+ import os from "node:os";
29
+ import path from "node:path";
30
+ import { spawn } from "node:child_process";
31
+
32
+ /**
33
+ * @param {object} ctx
34
+ * @param {string} ctx.newExePath the verified artifact from Phase 5b
35
+ * @param {string} ctx.targetExePath the exe to replace (usually process.execPath)
36
+ * @param {boolean} [ctx.restart=false] spawn the new exe after replacement
37
+ * @param {boolean} [ctx.dryRun=false] return the plan, do not mutate disk
38
+ * @param {"win32"|"posix"} [ctx.platform] override host detection for tests
39
+ * @param {number} [ctx.parentPid] PID to wait on (Windows sidecar); defaults to process.pid
40
+ * @param {(cmd:string,args:string[])=>object} [ctx.spawnImpl] injected for tests
41
+ * @returns {Promise<{
42
+ * platform: "win32"|"posix",
43
+ * action: "replace-in-place"|"sidecar-cmd"|"dry-run",
44
+ * targetExePath: string,
45
+ * newExePath: string,
46
+ * sidecarPath: string|null,
47
+ * restartRequested: boolean,
48
+ * }>}
49
+ */
50
+ export async function scheduleReplace(ctx) {
51
+ const {
52
+ newExePath,
53
+ targetExePath,
54
+ restart = false,
55
+ dryRun = false,
56
+ platform = process.platform === "win32" ? "win32" : "posix",
57
+ parentPid = process.pid,
58
+ spawnImpl = spawn,
59
+ } = ctx;
60
+
61
+ if (!newExePath || typeof newExePath !== "string") {
62
+ throw new ApplyError("newExePath is required", "NO_NEW_EXE");
63
+ }
64
+ if (!targetExePath || typeof targetExePath !== "string") {
65
+ throw new ApplyError("targetExePath is required", "NO_TARGET_EXE");
66
+ }
67
+ if (!fs.existsSync(newExePath)) {
68
+ throw new ApplyError(
69
+ `new exe does not exist: ${newExePath}`,
70
+ "NEW_EXE_MISSING",
71
+ );
72
+ }
73
+
74
+ if (dryRun) {
75
+ return {
76
+ platform,
77
+ action: "dry-run",
78
+ targetExePath,
79
+ newExePath,
80
+ sidecarPath: null,
81
+ restartRequested: Boolean(restart),
82
+ };
83
+ }
84
+
85
+ if (platform === "win32") {
86
+ const sidecarPath = writeWindowsSidecar({
87
+ newExePath,
88
+ targetExePath,
89
+ parentPid,
90
+ restart: Boolean(restart),
91
+ });
92
+ // Detach so the sidecar survives our process exit. `windowsHide: true`
93
+ // keeps the cmd window from flashing — the replace itself is silent.
94
+ const child = spawnImpl("cmd.exe", ["/c", sidecarPath], {
95
+ detached: true,
96
+ stdio: "ignore",
97
+ windowsHide: true,
98
+ });
99
+ if (child && typeof child.unref === "function") child.unref();
100
+
101
+ return {
102
+ platform,
103
+ action: "sidecar-cmd",
104
+ targetExePath,
105
+ newExePath,
106
+ sidecarPath,
107
+ restartRequested: Boolean(restart),
108
+ };
109
+ }
110
+
111
+ // POSIX: atomic rename works even if targetExePath is the running exe.
112
+ // Preserve executable bit — a downloaded file from fetch defaults to 644.
113
+ try {
114
+ fs.chmodSync(newExePath, 0o755);
115
+ } catch {
116
+ /* best effort — non-fatal if chmod fails (e.g. on FAT32 volumes) */
117
+ }
118
+ try {
119
+ fs.renameSync(newExePath, targetExePath);
120
+ } catch (err) {
121
+ throw new ApplyError(`rename failed: ${err.message}`, "RENAME_FAILED");
122
+ }
123
+
124
+ if (restart) {
125
+ const child = spawnImpl(targetExePath, process.argv.slice(2), {
126
+ detached: true,
127
+ stdio: "ignore",
128
+ });
129
+ if (child && typeof child.unref === "function") child.unref();
130
+ }
131
+
132
+ return {
133
+ platform,
134
+ action: "replace-in-place",
135
+ targetExePath,
136
+ newExePath,
137
+ sidecarPath: null,
138
+ restartRequested: Boolean(restart),
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Build the Windows sidecar `.cmd` that waits for the parent, moves the new
144
+ * exe over the target, and optionally restarts. Exposed (rather than inlined)
145
+ * so tests can read it back and assert on the shape without actually spawning.
146
+ *
147
+ * @param {object} ctx
148
+ * @param {string} ctx.newExePath
149
+ * @param {string} ctx.targetExePath
150
+ * @param {number} ctx.parentPid
151
+ * @param {boolean} ctx.restart
152
+ * @returns {string} absolute path to the generated .cmd (lives in os.tmpdir())
153
+ */
154
+ export function writeWindowsSidecar(ctx) {
155
+ const { newExePath, targetExePath, parentPid, restart } = ctx;
156
+
157
+ // `%TEMP%` is writable, survives across Explorer double-click launches, and
158
+ // gets auto-cleaned by Windows eventually. The unique name prevents two
159
+ // concurrent applies from clobbering each other.
160
+ const sidecarName = `cc-pack-apply-${Date.now()}-${Math.floor(Math.random() * 1e6)}.cmd`;
161
+ const sidecarPath = path.join(os.tmpdir(), sidecarName);
162
+
163
+ // tasklist /FI "PID eq <pid>" prints "INFO: No tasks..." when the PID is
164
+ // gone. We loop every 500ms up to ~10s; that's long enough for the parent
165
+ // to close its Electron/UI server gracefully but short enough to avoid
166
+ // stalling forever if the detection oddly fails.
167
+ const cmd = [
168
+ "@echo off",
169
+ "setlocal",
170
+ `set PARENT_PID=${parentPid}`,
171
+ `set NEW_EXE="${newExePath}"`,
172
+ `set TARGET_EXE="${targetExePath}"`,
173
+ "set /a ATTEMPTS=0",
174
+ ":waitloop",
175
+ 'tasklist /FI "PID eq %PARENT_PID%" 2>NUL | find /I "%PARENT_PID%" >NUL',
176
+ "if errorlevel 1 goto doreplace",
177
+ "set /a ATTEMPTS=%ATTEMPTS%+1",
178
+ "if %ATTEMPTS% GEQ 20 goto doreplace",
179
+ // Timeout with /T /NOBREAK is the only idle-wait available in pure cmd.
180
+ "timeout /T 1 /NOBREAK >NUL",
181
+ "goto waitloop",
182
+ ":doreplace",
183
+ "move /Y %NEW_EXE% %TARGET_EXE% >NUL",
184
+ "if errorlevel 1 (",
185
+ " echo cc-pack-apply: move failed & exit /b 1",
186
+ ")",
187
+ restart ? 'start "" %TARGET_EXE%' : "REM restart not requested",
188
+ // Self-delete — best effort. Leaving the .cmd in %TEMP% is harmless if
189
+ // this fails; Windows will reap it on the next disk-cleanup cycle.
190
+ '(goto) 2>NUL & del "%~f0"',
191
+ ].join("\r\n");
192
+
193
+ fs.writeFileSync(sidecarPath, cmd, { encoding: "utf-8" });
194
+ return sidecarPath;
195
+ }
196
+
197
+ export class ApplyError extends Error {
198
+ constructor(message, code) {
199
+ super(message);
200
+ this.name = "ApplyError";
201
+ this.code = code;
202
+ }
203
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Phase 5a: cc pack check-update — manifest-based version check for packed exes.
3
+ *
4
+ * Unlike `cc update` (which runs `npm install -g chainlesschain@<v>`), a packed
5
+ * exe has no Node/npm to rely on. Instead, we publish a small JSON manifest at
6
+ * a known URL and have the exe fetch + compare + surface the diff. Download +
7
+ * self-replace are the job of Phase 5b/5c; this module only does the check.
8
+ *
9
+ * See docs/design/CC_PACK_打包指令设计文档.md §17.5-17.7.
10
+ *
11
+ * The expected manifest shape is:
12
+ * {
13
+ * schema: 1,
14
+ * channel: "stable" | "beta",
15
+ * latest: {
16
+ * cliVersion: "0.157.0",
17
+ * productVersion: "v5.0.3.0",
18
+ * publishedAt: "2026-04-25T00:00:00Z",
19
+ * releaseNotes: "https://…",
20
+ * artifacts: [{ target, url, sha256 }, …]
21
+ * }
22
+ * }
23
+ *
24
+ * `target` matches pkg's `node20-<os>-<arch>` convention.
25
+ */
26
+
27
+ import semver from "semver";
28
+
29
+ const SUPPORTED_SCHEMA = 1;
30
+ const DEFAULT_TIMEOUT_MS = 10_000;
31
+
32
+ /**
33
+ * Fetch a manifest URL and compare against the current version.
34
+ *
35
+ * @param {object} ctx
36
+ * @param {string} ctx.manifestUrl absolute http(s) URL
37
+ * @param {string} ctx.currentVersion e.g. BAKED.packedCliVersion or VERSION
38
+ * @param {string} [ctx.target] e.g. "node20-win-x64"; if set, the
39
+ * returned artifact matches it
40
+ * @param {number} [ctx.timeoutMs=10000]
41
+ * @param {typeof fetch} [ctx.fetchImpl] injected for tests
42
+ * @returns {Promise<{
43
+ * updateAvailable: boolean,
44
+ * currentVersion: string,
45
+ * latestVersion: string,
46
+ * artifact: {target:string,url:string,sha256:string}|null,
47
+ * releaseNotes: string|null,
48
+ * channel: string,
49
+ * publishedAt: string|null,
50
+ * }>}
51
+ */
52
+ export async function checkPackUpdate(ctx) {
53
+ const {
54
+ manifestUrl,
55
+ currentVersion,
56
+ target,
57
+ timeoutMs = DEFAULT_TIMEOUT_MS,
58
+ fetchImpl = fetch,
59
+ } = ctx;
60
+
61
+ if (!manifestUrl || typeof manifestUrl !== "string") {
62
+ throw new PackUpdateError("manifestUrl is required", "NO_MANIFEST_URL");
63
+ }
64
+ if (!currentVersion || typeof currentVersion !== "string") {
65
+ throw new PackUpdateError(
66
+ "currentVersion is required",
67
+ "NO_CURRENT_VERSION",
68
+ );
69
+ }
70
+
71
+ let body;
72
+ try {
73
+ const controller = new AbortController();
74
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
75
+ try {
76
+ const response = await fetchImpl(manifestUrl, {
77
+ headers: { Accept: "application/json" },
78
+ signal: controller.signal,
79
+ });
80
+ if (!response.ok) {
81
+ throw new PackUpdateError(
82
+ `manifest fetch failed: HTTP ${response.status}`,
83
+ "FETCH_FAILED",
84
+ );
85
+ }
86
+ body = await response.text();
87
+ } finally {
88
+ clearTimeout(timer);
89
+ }
90
+ } catch (err) {
91
+ if (err instanceof PackUpdateError) throw err;
92
+ const kind = err?.name === "AbortError" ? "TIMEOUT" : "NETWORK_ERROR";
93
+ throw new PackUpdateError(`manifest fetch failed: ${err.message}`, kind);
94
+ }
95
+
96
+ let manifest;
97
+ try {
98
+ manifest = JSON.parse(body);
99
+ } catch (err) {
100
+ throw new PackUpdateError(
101
+ `manifest JSON parse failed: ${err.message}`,
102
+ "PARSE_FAILED",
103
+ );
104
+ }
105
+
106
+ return parseAndCompare(manifest, { currentVersion, target });
107
+ }
108
+
109
+ /**
110
+ * Pure parser — separated so tests can pass a manifest object directly
111
+ * without stubbing fetch.
112
+ *
113
+ * @param {object} manifest
114
+ * @param {object} ctx
115
+ * @param {string} ctx.currentVersion
116
+ * @param {string} [ctx.target]
117
+ */
118
+ export function parseAndCompare(manifest, ctx) {
119
+ const { currentVersion, target } = ctx;
120
+
121
+ if (!manifest || typeof manifest !== "object") {
122
+ throw new PackUpdateError("manifest must be an object", "SCHEMA_MISMATCH");
123
+ }
124
+ if (manifest.schema !== SUPPORTED_SCHEMA) {
125
+ throw new PackUpdateError(
126
+ `unsupported manifest schema ${manifest.schema} (expected ${SUPPORTED_SCHEMA})`,
127
+ "SCHEMA_MISMATCH",
128
+ );
129
+ }
130
+ const latest = manifest.latest;
131
+ if (!latest || typeof latest.cliVersion !== "string") {
132
+ throw new PackUpdateError(
133
+ "manifest.latest.cliVersion missing",
134
+ "SCHEMA_MISMATCH",
135
+ );
136
+ }
137
+
138
+ const latestVersion = latest.cliVersion;
139
+ if (!semver.valid(latestVersion)) {
140
+ throw new PackUpdateError(
141
+ `manifest.latest.cliVersion "${latestVersion}" is not a valid semver`,
142
+ "INVALID_VERSION",
143
+ );
144
+ }
145
+ if (!semver.valid(currentVersion)) {
146
+ throw new PackUpdateError(
147
+ `currentVersion "${currentVersion}" is not a valid semver`,
148
+ "INVALID_VERSION",
149
+ );
150
+ }
151
+
152
+ const updateAvailable = semver.gt(latestVersion, currentVersion);
153
+
154
+ let artifact = null;
155
+ if (target && Array.isArray(latest.artifacts)) {
156
+ artifact =
157
+ latest.artifacts.find(
158
+ (a) => a && typeof a.target === "string" && a.target === target,
159
+ ) || null;
160
+ }
161
+
162
+ return {
163
+ updateAvailable,
164
+ currentVersion,
165
+ latestVersion,
166
+ artifact,
167
+ releaseNotes:
168
+ typeof latest.releaseNotes === "string" ? latest.releaseNotes : null,
169
+ channel: typeof manifest.channel === "string" ? manifest.channel : "stable",
170
+ publishedAt:
171
+ typeof latest.publishedAt === "string" ? latest.publishedAt : null,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Typed error with a machine-readable `code`. The `cc pack check-update`
177
+ * command turns these into friendly messages and non-zero exit codes.
178
+ */
179
+ export class PackUpdateError extends Error {
180
+ constructor(message, code) {
181
+ super(message);
182
+ this.name = "PackUpdateError";
183
+ this.code = code;
184
+ }
185
+ }