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.
- package/README.md +24 -0
- package/package.json +2 -1
- package/src/assets/web-panel/assets/{ActionButton-Dme4LGax.js → ActionButton-Cs4QdjYb.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-B3-5BjRm.js → Analytics-Xot0e9TT.js} +1 -1
- package/src/assets/web-panel/assets/{AppLayout-DvVLRyPs.js → AppLayout-3qsE1-pz.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-wLmjCc9u.js → BaseInput-Tg40P4JM.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BfbEUJDW.js → Checkbox-CheA2Ety.js} +1 -1
- package/src/assets/web-panel/assets/{Col-HJI40OzO.js → Col-Cdfsmnaq.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-ADVAwcbQ.js → Compact-D3LSgEpW.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-DcNB5TYu.js → Cron-9MV6k-MV.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-p_Wuj0Un.js → Dashboard-A1TZC5_t.js} +1 -1
- package/src/assets/web-panel/assets/{Dropdown-CrXGzreQ.js → Dropdown-DZxUTZvw.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-B97Dibo2.js → FormItemContext-C3_-j_SR.js} +1 -1
- package/src/assets/web-panel/assets/{Git-90CPsOOr.js → Git-Cw-gW-kh.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-B_3zNQNB.js → Memory-CMweTJyn.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DNQz9UXh.js → Notes-B_W3BfZF.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CgXUnp-W.js → Organization-Dz_jGbAM.js} +1 -1
- package/src/assets/web-panel/assets/{Overflow-BVsn6SM5.js → Overflow-Dka3nWV9.js} +1 -1
- package/src/assets/web-panel/assets/{Permissions-DIFqcnjU.js → Permissions-DvXVIlHX.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-mscN7CK5.js → Providers-DUfX_ynl.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BFUWxIkx.js → Row-DZhDSo2Q.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Dpa4h-q_.js → RssFeed-CHQpUl3h.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DR6HKo_S.js → Security-FUSOn89T.js} +1 -1
- package/src/assets/web-panel/assets/{Skeleton-VNikEgM4.js → Skeleton-DxmZ7zRw.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Ny_4GO6a.js → Templates-BVbmyn38.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-C7MTh_xj.js → Trigger-xAvohiq9.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BNRFHgJ9.js → VideoEditing-BUWYQv2y.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BUfg4IAx.js → Wallet-BDYdEwFf.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-Cia89OyQ.js → WebAuthn-CvpuagtK.js} +1 -1
- package/src/assets/web-panel/assets/{WorkflowEditor-C1OsMtqv.js → WorkflowEditor-BR7W5cjw.js} +1 -1
- package/src/assets/web-panel/assets/{colors-C_wDMX2Q.js → colors-C5kDbQCi.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-C1ikzEN-.js → compact-item-Bo_1zDrX.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-XExBTk9v.js → createContext-CniPpJsG.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-mXvd_Kdq.js → hasIn-ClDc6Sz8.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6Hyy0Bc.js → index--lcO-bOn.js} +1 -1
- package/src/assets/web-panel/assets/{index-lPIeHtHE.js → index-6tQekF0Y.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfncNR8d.js → index-8qWxPHSb.js} +1 -1
- package/src/assets/web-panel/assets/{index-C53dnYiq.js → index-B7nGNm_C.js} +1 -1
- package/src/assets/web-panel/assets/{index-CMYADk0v.js → index-B8y0NO-M.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMcLOtIo.js → index-BAlSSCbs.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJkIheU6.js → index-BCXFoTAw.js} +1 -1
- package/src/assets/web-panel/assets/{index-kLUQdSDJ.js → index-BHruTebo.js} +1 -1
- package/src/assets/web-panel/assets/{index-1ZqkTPt2.js → index-BJx6C3J8.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbpKJ2W0.js → index-BUTCJTbj.js} +1 -1
- package/src/assets/web-panel/assets/{index-B6P9mWuk.js → index-BYShDlZ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9tzxSFs.js → index-Bv2OmZAS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFFb9yPd.js → index-C92K4iDE.js} +1 -1
- package/src/assets/web-panel/assets/{index-v4Oi0d0l.js → index-CCdb36il.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-TI0cZ2.js → index-CKR2ITFk.js} +1 -1
- package/src/assets/web-panel/assets/{index-fLUJs2Sr.js → index-CMcGcbea.js} +1 -1
- package/src/assets/web-panel/assets/{index-BirLVqrC.js → index-CTetsi8W.js} +1 -1
- package/src/assets/web-panel/assets/{index-LpE6Six-.js → index-CXxLp7Aw.js} +1 -1
- package/src/assets/web-panel/assets/{index-qtDQSqTG.js → index-CeSV8f3b.js} +1 -1
- package/src/assets/web-panel/assets/{index-DwMlStra.js → index-Ch5mAXeh.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOqmUcij.js → index-CwhWEkmA.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_oSE2Nk.js → index-D2fe9a6f.js} +1 -1
- package/src/assets/web-panel/assets/index-D3UDIt7h.js +1 -0
- package/src/assets/web-panel/assets/{index-CxwU-EjS.js → index-D90sLw5Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1eekAaa.js → index-D9bolkbl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BL27IhbN.js → index-DNY0K7iI.js} +1 -1
- package/src/assets/web-panel/assets/{index-Du7KGlCP.js → index-DSiHmo4b.js} +1 -1
- package/src/assets/web-panel/assets/{index-CttcpCq_.js → index-DTYnvYqB.js} +1 -1
- package/src/assets/web-panel/assets/{index-jg5cpQg9.js → index-DaLYbr0E.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYLE4bnY.js → index-DkSNIJhM.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZjQgmBq.js → index-DnQkqOZj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DaMG8ksh.js → index-Dn_OQQaV.js} +3 -3
- package/src/assets/web-panel/assets/{index-Dz6RDRcu.js → index-Dtfrhky9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTle6zcb.js → index-JNbd08FN.js} +1 -1
- package/src/assets/web-panel/assets/index-PT376OZM.js +1 -0
- package/src/assets/web-panel/assets/{index-BBOVB9YK.js → index-cIgCeEqo.js} +1 -1
- package/src/assets/web-panel/assets/{index-a0qENb5U.js → index-vBi4x_6g.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5Zv4fBx.js → index-xL8gcpmy.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DOj2K4bh.js → initDefaultProps-DMfJaUzk.js} +1 -1
- package/src/assets/web-panel/assets/{motion-joGf7r-l.js → motion-sEbWmOWo.js} +1 -1
- package/src/assets/web-panel/assets/{move-Cwb6tumJ.js → move-DIWXVs--.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CPycjJ8C.js → omit-D7mkMPhu.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CnibXC3T.js → pickAttrs-B25NUX4k.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DWcIO1y4.js → placementArrow-By1Bkq1d.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-C5giLhLf.js → responsiveObserve-B3aCQz5r.js} +1 -1
- package/src/assets/web-panel/assets/{slide-zwgmm7vM.js → slide-eR-f56FQ.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CK8tJSHq.js → statusUtils-zcNWczhN.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BzLSEXyu.js → styleChecker-u9Z0IfRy.js} +1 -1
- package/src/assets/web-panel/assets/{transition-D4AbuDdO.js → transition-gRK4XSlW.js} +1 -1
- package/src/assets/web-panel/assets/{useConfigInject-ImjEZhXr.js → useConfigInject-ZEunuNHN.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Cd-PoTMl.js → useFlexGapSupport-BpbEJfeh.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DAWimP6X.js → vnode-DVHvXn9F.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-u6SXbmzZ.js → zoom-ByzgJIn6.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/pack.js +463 -0
- package/src/commands/ui.js +6 -0
- package/src/gateways/ws/ws-server.js +29 -3
- package/src/index.js +34 -1
- package/src/lib/governance-v2-helpers.js +109 -0
- package/src/lib/packer/config-template-builder.js +151 -0
- package/src/lib/packer/errors.js +30 -0
- package/src/lib/packer/index.js +383 -0
- package/src/lib/packer/manifest-writer.js +93 -0
- package/src/lib/packer/native-prebuild-collector.js +305 -0
- package/src/lib/packer/pack-update-applier.js +203 -0
- package/src/lib/packer/pack-update-checker.js +185 -0
- package/src/lib/packer/pack-update-downloader.js +162 -0
- package/src/lib/packer/pkg-config-generator.js +325 -0
- package/src/lib/packer/pkg-runner.js +139 -0
- package/src/lib/packer/precheck.js +273 -0
- package/src/lib/packer/project-assets-collector.js +0 -0
- package/src/lib/packer/smoke-runner.js +339 -0
- package/src/lib/packer/web-panel-builder.js +157 -0
- package/src/lib/web-ui-server.js +95 -2
- package/src/lib/ws-server.js +1 -0
- package/src/runtime/agent-runtime.js +1 -0
- package/src/runtime/policies/agent-policy.js +1 -0
- package/src/assets/web-panel/assets/index-DQgS_8Fd.js +0 -1
- 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
|
+
}
|