@openagentsinc/pylon 0.1.2 → 0.1.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 +24 -3
- package/bin/pylon +26 -0
- package/package.json +2 -2
- package/src/cli.js +112 -36
- package/src/index.js +516 -118
- package/src/telemetry.js +160 -0
package/README.md
CHANGED
|
@@ -10,7 +10,10 @@ available.
|
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx @openagentsinc/pylon
|
|
13
|
-
|
|
13
|
+
bunx @openagentsinc/pylon
|
|
14
|
+
npm install -g @openagentsinc/pylon && pylon
|
|
15
|
+
bun install -g @openagentsinc/pylon && pylon
|
|
16
|
+
npx @openagentsinc/pylon --version 0.0.1-rc10
|
|
14
17
|
npx @openagentsinc/pylon --no-launch
|
|
15
18
|
npx @openagentsinc/pylon --download-curated-cache --model gemma-4-e2b --diagnostic-repeats 2
|
|
16
19
|
npx @openagentsinc/pylon --verbose
|
|
@@ -18,19 +21,29 @@ npx @openagentsinc/pylon --verbose
|
|
|
18
21
|
|
|
19
22
|
The launcher:
|
|
20
23
|
|
|
21
|
-
-
|
|
22
|
-
|
|
24
|
+
- supports direct `npx` / `bunx` execution plus global `npm install -g` /
|
|
25
|
+
`bun install -g` installs with the same `pylon` command
|
|
26
|
+
- checks GitHub for the latest tagged `pylon-v...` release on each default run,
|
|
27
|
+
or resolves a specific tagged `Pylon` version when `--version` is provided
|
|
23
28
|
- resolves the correct `pylon-v<version>-<os>-<arch>.tar.gz` asset for the
|
|
24
29
|
current machine
|
|
25
30
|
- falls back to the exact tagged source checkout and builds `pylon` plus
|
|
26
31
|
`pylon-tui` locally when no matching release asset exists for the machine
|
|
27
32
|
- prompts before installing the Rust toolchain via `rustup` if a source build
|
|
28
33
|
is needed and `cargo` / `rustc` are missing
|
|
34
|
+
- emits best-effort anonymous installer telemetry to `openagents.com` so the
|
|
35
|
+
public stats page can show install starts, completions, source-build fallbacks,
|
|
36
|
+
Rust prompts, and smoke-test outcomes
|
|
29
37
|
- downloads the archive and published SHA-256 checksum
|
|
30
38
|
- verifies the checksum before extracting
|
|
31
39
|
- caches the unpacked binaries under `~/.openagents/pylon/bootstrap/`
|
|
40
|
+
- never links or copies those cached standalone binaries into a shared global
|
|
41
|
+
bin directory, so the package-managed `pylon` launcher remains the command on
|
|
42
|
+
`PATH`
|
|
32
43
|
- prints status lines such as release resolution, runtime checks, and local
|
|
33
44
|
model scanning while it runs
|
|
45
|
+
- ends first run with an explicit verdict such as `fully online`, `runtime
|
|
46
|
+
ready`, or `installed but runtime missing`, plus exact next-step guidance
|
|
34
47
|
- runs `pylon --help`, `init`, `status --json`, and `inventory --json`
|
|
35
48
|
- runs `pylon gemma diagnose <model> --json`
|
|
36
49
|
- only runs `pylon gemma download <model>` when `--download-curated-cache` is
|
|
@@ -39,6 +52,13 @@ The launcher:
|
|
|
39
52
|
- falls back to `curl` for release metadata and asset downloads when the Node
|
|
40
53
|
fetch path fails in constrained network contexts
|
|
41
54
|
- opens `pylon-tui` by default after the smoke path unless `--no-launch` is set
|
|
55
|
+
- does not try to install or register a local runtime automatically; the
|
|
56
|
+
bootstrap stays honest about the separate Ollama-compatible runtime
|
|
57
|
+
prerequisite instead of mutating the host behind the user's back
|
|
58
|
+
|
|
59
|
+
Set `OPENAGENTS_DISABLE_TELEMETRY=1` to disable installer telemetry, or
|
|
60
|
+
`OPENAGENTS_TELEMETRY_URL=http://127.0.0.1:8000/api/telemetry/events` to point
|
|
61
|
+
the launcher at a non-production telemetry endpoint.
|
|
42
62
|
|
|
43
63
|
## Publish
|
|
44
64
|
|
|
@@ -46,5 +66,6 @@ Publish directly from this package directory:
|
|
|
46
66
|
|
|
47
67
|
```bash
|
|
48
68
|
cd packages/pylon-bootstrap
|
|
69
|
+
npm pack --dry-run
|
|
49
70
|
npm publish
|
|
50
71
|
```
|
package/bin/pylon
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
set -eu
|
|
4
|
+
|
|
5
|
+
SCRIPT_PATH="$0"
|
|
6
|
+
while [ -L "$SCRIPT_PATH" ]; do
|
|
7
|
+
LINK_TARGET=$(readlink "$SCRIPT_PATH")
|
|
8
|
+
case "$LINK_TARGET" in
|
|
9
|
+
/*) SCRIPT_PATH="$LINK_TARGET" ;;
|
|
10
|
+
*) SCRIPT_PATH="$(dirname -- "$SCRIPT_PATH")/$LINK_TARGET" ;;
|
|
11
|
+
esac
|
|
12
|
+
done
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)
|
|
15
|
+
LAUNCHER_JS="${SCRIPT_DIR}/pylon.js"
|
|
16
|
+
|
|
17
|
+
if command -v node >/dev/null 2>&1; then
|
|
18
|
+
exec node "$LAUNCHER_JS" "$@"
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
if command -v bun >/dev/null 2>&1; then
|
|
22
|
+
exec bun "$LAUNCHER_JS" "$@"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
echo "pylon launcher requires Node.js or Bun on PATH." >&2
|
|
26
|
+
exit 1
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openagentsinc/pylon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Bootstrap the standalone OpenAgents Pylon release asset and run first-run smoke checks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"pylon": "./bin/pylon
|
|
7
|
+
"pylon": "./bin/pylon"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
package/src/cli.js
CHANGED
|
@@ -7,8 +7,16 @@ import {
|
|
|
7
7
|
bootstrapInstalledPylon,
|
|
8
8
|
ensureReleaseInstall,
|
|
9
9
|
launchInstalledPylonTui,
|
|
10
|
+
resolveBootstrapOutcome,
|
|
11
|
+
resolvePlatformTarget,
|
|
10
12
|
renderBootstrapSummary,
|
|
11
13
|
} from "./index.js";
|
|
14
|
+
import {
|
|
15
|
+
createTelemetryClient,
|
|
16
|
+
detectPackageInvoker,
|
|
17
|
+
installSourceForTelemetry,
|
|
18
|
+
telemetryFailureContext,
|
|
19
|
+
} from "./telemetry.js";
|
|
12
20
|
|
|
13
21
|
function parseIntegerFlag(value, label) {
|
|
14
22
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -67,6 +75,8 @@ function createReporter({ enableColor = process.stdout.isTTY && !process.env.NO_
|
|
|
67
75
|
export function usage() {
|
|
68
76
|
return `Usage:
|
|
69
77
|
npx @openagentsinc/pylon [options]
|
|
78
|
+
bunx @openagentsinc/pylon [options]
|
|
79
|
+
pylon [options]
|
|
70
80
|
|
|
71
81
|
Description:
|
|
72
82
|
Download the latest tagged standalone Pylon release asset for this machine,
|
|
@@ -74,7 +84,9 @@ Description:
|
|
|
74
84
|
asset exists for the local platform, fetch the exact tagged source checkout
|
|
75
85
|
and build it locally instead. Cache the binaries, run the first-run smoke
|
|
76
86
|
path, and then open the Pylon terminal UI by default with live status
|
|
77
|
-
updates.
|
|
87
|
+
updates. The launcher checks GitHub for newer tagged pylon-v... releases on
|
|
88
|
+
each default run, but only caches the standalone binaries under the local
|
|
89
|
+
bootstrap root; it does not replace your global npm or bun pylon command.
|
|
78
90
|
|
|
79
91
|
Options:
|
|
80
92
|
--version <x.y.z> Resolve a specific Pylon release.
|
|
@@ -216,6 +228,7 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
216
228
|
ensureReleaseInstallImpl = ensureReleaseInstall,
|
|
217
229
|
bootstrapInstalledPylonImpl = bootstrapInstalledPylon,
|
|
218
230
|
launchInstalledPylonTuiImpl = launchInstalledPylonTui,
|
|
231
|
+
createTelemetryClientImpl = createTelemetryClient,
|
|
219
232
|
} = dependencies;
|
|
220
233
|
const options = parseArgs(argv);
|
|
221
234
|
if (options.help) {
|
|
@@ -224,46 +237,109 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
224
237
|
}
|
|
225
238
|
|
|
226
239
|
const reporter = options.json ? null : createReporter();
|
|
240
|
+
const startedAt = Date.now();
|
|
241
|
+
const target = (() => {
|
|
242
|
+
try {
|
|
243
|
+
return resolvePlatformTarget(options.platform, options.arch);
|
|
244
|
+
} catch {
|
|
245
|
+
return {
|
|
246
|
+
os: process.platform,
|
|
247
|
+
arch: process.arch,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
})();
|
|
251
|
+
const telemetryClient =
|
|
252
|
+
dependencies.telemetryClient ??
|
|
253
|
+
createTelemetryClientImpl({
|
|
254
|
+
fetchImpl: dependencies.fetchImpl ?? globalThis.fetch,
|
|
255
|
+
});
|
|
256
|
+
const sharedTelemetry = {
|
|
257
|
+
requested_version: options.version ?? "latest",
|
|
258
|
+
os: target.os,
|
|
259
|
+
arch: target.arch,
|
|
260
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
261
|
+
npm_or_bun_invoker: detectPackageInvoker(),
|
|
262
|
+
};
|
|
227
263
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
{
|
|
234
|
-
...options,
|
|
235
|
-
...install,
|
|
236
|
-
version: install.version,
|
|
237
|
-
},
|
|
238
|
-
{
|
|
264
|
+
telemetryClient?.emit?.("installer_started", sharedTelemetry);
|
|
265
|
+
|
|
266
|
+
let install = null;
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
install = await ensureReleaseInstallImpl(options, {
|
|
239
270
|
...dependencies,
|
|
240
271
|
onStatus: reporter?.status,
|
|
241
|
-
|
|
242
|
-
|
|
272
|
+
telemetryClient,
|
|
273
|
+
});
|
|
274
|
+
const summary = await bootstrapInstalledPylonImpl(
|
|
275
|
+
{
|
|
276
|
+
...options,
|
|
277
|
+
...install,
|
|
278
|
+
version: install.version,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
...dependencies,
|
|
282
|
+
onStatus: reporter?.status,
|
|
283
|
+
telemetryClient,
|
|
284
|
+
},
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
telemetryClient?.emit?.("installer_finished", {
|
|
288
|
+
...sharedTelemetry,
|
|
289
|
+
release_tag: summary.tagName,
|
|
290
|
+
release_commit: install.sourceCommit ?? null,
|
|
291
|
+
duration_ms: Date.now() - startedAt,
|
|
292
|
+
result: "success",
|
|
293
|
+
install_source: installSourceForTelemetry(
|
|
294
|
+
summary.installMethod ?? install.installMethod,
|
|
295
|
+
Boolean(summary.cached),
|
|
296
|
+
),
|
|
297
|
+
});
|
|
298
|
+
await telemetryClient?.flush?.();
|
|
243
299
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
} else {
|
|
247
|
-
reporter?.success("Pylon bootstrap complete");
|
|
248
|
-
console.log(renderBootstrapSummary(summary));
|
|
249
|
-
if (!options.noLaunch) {
|
|
250
|
-
await launchInstalledPylonTuiImpl(
|
|
251
|
-
{
|
|
252
|
-
...options,
|
|
253
|
-
...install,
|
|
254
|
-
version: install.version,
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
...dependencies,
|
|
258
|
-
onStatus: reporter?.status,
|
|
259
|
-
},
|
|
260
|
-
);
|
|
300
|
+
if (options.json) {
|
|
301
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
261
302
|
} else {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
303
|
+
const outcome = resolveBootstrapOutcome(summary);
|
|
304
|
+
if (outcome.level === "success") {
|
|
305
|
+
reporter?.success(`Pylon ${outcome.verdict}`, outcome.detail);
|
|
306
|
+
} else {
|
|
307
|
+
reporter?.warning(`Pylon ${outcome.verdict}`, outcome.detail);
|
|
308
|
+
}
|
|
309
|
+
console.log(renderBootstrapSummary(summary));
|
|
310
|
+
if (!options.noLaunch) {
|
|
311
|
+
await launchInstalledPylonTuiImpl(
|
|
312
|
+
{
|
|
313
|
+
...options,
|
|
314
|
+
...install,
|
|
315
|
+
version: install.version,
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
...dependencies,
|
|
319
|
+
onStatus: reporter?.status,
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
} else {
|
|
323
|
+
reporter?.warning(
|
|
324
|
+
"Skipped Pylon terminal UI launch",
|
|
325
|
+
"pass no flag to open pylon-tui by default",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
266
328
|
}
|
|
329
|
+
return summary;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
telemetryClient?.emit?.("installer_finished", {
|
|
332
|
+
...sharedTelemetry,
|
|
333
|
+
release_tag: install?.tagName ?? null,
|
|
334
|
+
release_commit: install?.sourceCommit ?? null,
|
|
335
|
+
duration_ms: Date.now() - startedAt,
|
|
336
|
+
result: "failed",
|
|
337
|
+
install_source: install
|
|
338
|
+
? installSourceForTelemetry(install.installMethod, Boolean(install.cached))
|
|
339
|
+
: null,
|
|
340
|
+
...telemetryFailureContext(error, "launcher"),
|
|
341
|
+
});
|
|
342
|
+
await telemetryClient?.flush?.();
|
|
343
|
+
throw error;
|
|
267
344
|
}
|
|
268
|
-
return summary;
|
|
269
345
|
}
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,12 @@ import fs from "node:fs/promises";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import readline from "node:readline/promises";
|
|
8
|
+
import {
|
|
9
|
+
installSourceForTelemetry,
|
|
10
|
+
telemetryFailureContext,
|
|
11
|
+
} from "./telemetry.js";
|
|
12
|
+
|
|
13
|
+
export { createTelemetryClient } from "./telemetry.js";
|
|
8
14
|
|
|
9
15
|
export const DEFAULT_RELEASE_REPO = "OpenAgentsInc/openagents";
|
|
10
16
|
export const DEFAULT_RELEASE_API_BASE = "https://api.github.com";
|
|
@@ -17,6 +23,7 @@ export const DEFAULT_FETCH_TIMEOUT_MS = 15_000;
|
|
|
17
23
|
const PYLON_RELEASE_TAG_PREFIX = "pylon-v";
|
|
18
24
|
const RELEASE_ASSET_INSTALL_METHOD = "release_asset";
|
|
19
25
|
const SOURCE_BUILD_INSTALL_METHOD = "source_build";
|
|
26
|
+
const PREFERRED_RUNTIME_MODEL_NAME = "gemma4:e4b";
|
|
20
27
|
|
|
21
28
|
function emitStatus(onStatus, message, detail = null) {
|
|
22
29
|
if (typeof onStatus === "function") {
|
|
@@ -30,10 +37,82 @@ function emitVerboseStatus(onStatus, verbose, message, detail = null) {
|
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
function emitTelemetry(telemetryClient, eventName, properties = {}) {
|
|
41
|
+
if (typeof telemetryClient?.emit === "function") {
|
|
42
|
+
void telemetryClient.emit(eventName, properties);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
function normalizeVersion(value) {
|
|
34
47
|
return value.replace(/^pylon-v/, "").replace(/^v/, "");
|
|
35
48
|
}
|
|
36
49
|
|
|
50
|
+
function parseComparableVersion(value) {
|
|
51
|
+
const normalized = normalizeVersion(value).trim();
|
|
52
|
+
const match = normalized.match(
|
|
53
|
+
/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+?)(\d+)?)?$/,
|
|
54
|
+
);
|
|
55
|
+
if (!match) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
normalized,
|
|
61
|
+
major: Number.parseInt(match[1], 10),
|
|
62
|
+
minor: Number.parseInt(match[2], 10),
|
|
63
|
+
patch: Number.parseInt(match[3], 10),
|
|
64
|
+
prereleaseLabel: match[4] ?? null,
|
|
65
|
+
prereleaseNumber:
|
|
66
|
+
match[5] != null ? Number.parseInt(match[5], 10) : null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function comparePylonReleaseTags(leftTagName, rightTagName) {
|
|
71
|
+
const left = parseComparableVersion(leftTagName);
|
|
72
|
+
const right = parseComparableVersion(rightTagName);
|
|
73
|
+
|
|
74
|
+
if (!left && !right) {
|
|
75
|
+
return String(leftTagName).localeCompare(String(rightTagName));
|
|
76
|
+
}
|
|
77
|
+
if (!left) {
|
|
78
|
+
return -1;
|
|
79
|
+
}
|
|
80
|
+
if (!right) {
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
85
|
+
if (left[key] !== right[key]) {
|
|
86
|
+
return left[key] > right[key] ? 1 : -1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (left.prereleaseLabel == null && right.prereleaseLabel == null) {
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
if (left.prereleaseLabel == null) {
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
if (right.prereleaseLabel == null) {
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const labelComparison = left.prereleaseLabel.localeCompare(
|
|
101
|
+
right.prereleaseLabel,
|
|
102
|
+
);
|
|
103
|
+
if (labelComparison !== 0) {
|
|
104
|
+
return labelComparison;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const leftNumber = left.prereleaseNumber ?? 0;
|
|
108
|
+
const rightNumber = right.prereleaseNumber ?? 0;
|
|
109
|
+
if (leftNumber !== rightNumber) {
|
|
110
|
+
return leftNumber > rightNumber ? 1 : -1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return left.normalized.localeCompare(right.normalized);
|
|
114
|
+
}
|
|
115
|
+
|
|
37
116
|
function createBootstrapError(message, context = {}) {
|
|
38
117
|
const error = new Error(message);
|
|
39
118
|
Object.assign(error, context);
|
|
@@ -496,14 +575,31 @@ export function isPylonReleaseTag(tagName) {
|
|
|
496
575
|
);
|
|
497
576
|
}
|
|
498
577
|
|
|
499
|
-
|
|
578
|
+
function releaseHasTargetAssets(release, target) {
|
|
579
|
+
if (!target || !release?.tag_name) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const { archiveName, checksumName } = buildAssetNames(release.tag_name, target);
|
|
584
|
+
const assetNames = new Set(
|
|
585
|
+
(Array.isArray(release.assets) ? release.assets : [])
|
|
586
|
+
.map((asset) => asset?.name)
|
|
587
|
+
.filter(Boolean),
|
|
588
|
+
);
|
|
589
|
+
return assetNames.has(archiveName) && assetNames.has(checksumName);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function selectLatestPylonRelease(releases, target = null) {
|
|
500
593
|
if (!Array.isArray(releases)) {
|
|
501
594
|
throw new Error("GitHub release lookup did not return a release list.");
|
|
502
595
|
}
|
|
503
596
|
|
|
504
|
-
const
|
|
505
|
-
(candidate) => !candidate?.draft && isPylonReleaseTag(candidate?.tag_name)
|
|
506
|
-
|
|
597
|
+
const candidates = releases
|
|
598
|
+
.filter((candidate) => !candidate?.draft && isPylonReleaseTag(candidate?.tag_name))
|
|
599
|
+
.sort((left, right) => comparePylonReleaseTags(right.tag_name, left.tag_name));
|
|
600
|
+
const release =
|
|
601
|
+
candidates.find((candidate) => releaseHasTargetAssets(candidate, target)) ??
|
|
602
|
+
candidates[0];
|
|
507
603
|
if (!release) {
|
|
508
604
|
throw new Error(
|
|
509
605
|
`GitHub release lookup did not find any published ${PYLON_RELEASE_TAG_PREFIX} releases.`,
|
|
@@ -521,6 +617,7 @@ export async function fetchReleaseMetadata({
|
|
|
521
617
|
apiBase = DEFAULT_RELEASE_API_BASE,
|
|
522
618
|
repo = DEFAULT_RELEASE_REPO,
|
|
523
619
|
version = null,
|
|
620
|
+
target = null,
|
|
524
621
|
} = {}) {
|
|
525
622
|
const normalizedVersion = normalizeRequestedVersion(version);
|
|
526
623
|
const endpoint = normalizedVersion
|
|
@@ -537,7 +634,7 @@ export async function fetchReleaseMetadata({
|
|
|
537
634
|
? "GitHub tagged release lookup"
|
|
538
635
|
: "GitHub release list lookup",
|
|
539
636
|
});
|
|
540
|
-
return normalizedVersion ? payload : selectLatestPylonRelease(payload);
|
|
637
|
+
return normalizedVersion ? payload : selectLatestPylonRelease(payload, target);
|
|
541
638
|
}
|
|
542
639
|
|
|
543
640
|
export function selectReleaseAssets(release, target) {
|
|
@@ -712,6 +809,7 @@ async function ensureRustToolchain({
|
|
|
712
809
|
fetchImpl,
|
|
713
810
|
runProcessImpl,
|
|
714
811
|
onStatus,
|
|
812
|
+
telemetryClient,
|
|
715
813
|
promptImpl = promptForApproval,
|
|
716
814
|
commandExistsImpl = commandExists,
|
|
717
815
|
env = process.env,
|
|
@@ -724,44 +822,70 @@ async function ensureRustToolchain({
|
|
|
724
822
|
return toolchainEnv;
|
|
725
823
|
}
|
|
726
824
|
|
|
825
|
+
emitTelemetry(telemetryClient, "installer_rust_missing", {
|
|
826
|
+
os: target.os,
|
|
827
|
+
arch: target.arch,
|
|
828
|
+
});
|
|
829
|
+
|
|
727
830
|
emitStatus(
|
|
728
831
|
onStatus,
|
|
729
832
|
"Rust toolchain required for source build",
|
|
730
833
|
`${target.os}-${target.arch}`,
|
|
731
834
|
);
|
|
732
835
|
|
|
836
|
+
emitTelemetry(telemetryClient, "installer_rust_install_prompt_shown", {
|
|
837
|
+
os: target.os,
|
|
838
|
+
arch: target.arch,
|
|
839
|
+
});
|
|
733
840
|
const approved = await promptImpl(
|
|
734
841
|
`Rust is required to build Pylon from source for ${target.os}-${target.arch}. Install the official Rust toolchain now via rustup?`,
|
|
735
842
|
);
|
|
736
843
|
if (!approved) {
|
|
844
|
+
emitTelemetry(telemetryClient, "installer_rust_install_declined", {
|
|
845
|
+
os: target.os,
|
|
846
|
+
arch: target.arch,
|
|
847
|
+
});
|
|
737
848
|
throw new Error(
|
|
738
849
|
`Rust is required to build Pylon from source.\nInstall it manually and rerun:\n${rustInstallCommand()}`,
|
|
739
850
|
);
|
|
740
851
|
}
|
|
741
852
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
accept: "text/plain",
|
|
746
|
-
"user-agent": "@openagentsinc/pylon bootstrap",
|
|
747
|
-
},
|
|
748
|
-
runProcessImpl,
|
|
749
|
-
onStatus,
|
|
750
|
-
stage: "Rust toolchain installer download",
|
|
853
|
+
emitTelemetry(telemetryClient, "installer_rust_install_approved", {
|
|
854
|
+
os: target.os,
|
|
855
|
+
arch: target.arch,
|
|
751
856
|
});
|
|
752
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pylon-rustup-"));
|
|
753
|
-
const scriptPath = path.join(tempDir, "rustup-init.sh");
|
|
754
857
|
|
|
858
|
+
emitStatus(onStatus, "Installing Rust toolchain", "official rustup installer");
|
|
755
859
|
try {
|
|
860
|
+
const scriptPayload = await fetchText(fetchImpl, rustupInitUrl, {
|
|
861
|
+
headers: {
|
|
862
|
+
accept: "text/plain",
|
|
863
|
+
"user-agent": "@openagentsinc/pylon bootstrap",
|
|
864
|
+
},
|
|
865
|
+
runProcessImpl,
|
|
866
|
+
onStatus,
|
|
867
|
+
stage: "Rust toolchain installer download",
|
|
868
|
+
});
|
|
869
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pylon-rustup-"));
|
|
870
|
+
const scriptPath = path.join(tempDir, "rustup-init.sh");
|
|
756
871
|
await fs.writeFile(scriptPath, scriptPayload);
|
|
757
872
|
await fs.chmod(scriptPath, 0o755);
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
873
|
+
try {
|
|
874
|
+
await runProcessImpl("sh", [scriptPath, "-y"], {
|
|
875
|
+
cwd: tempDir,
|
|
876
|
+
env: toolchainEnv,
|
|
877
|
+
stdio: "inherit",
|
|
878
|
+
});
|
|
879
|
+
} finally {
|
|
880
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
881
|
+
}
|
|
882
|
+
} catch (error) {
|
|
883
|
+
emitTelemetry(telemetryClient, "installer_rust_install_failed", {
|
|
884
|
+
os: target.os,
|
|
885
|
+
arch: target.arch,
|
|
886
|
+
...telemetryFailureContext(error, "rust_install"),
|
|
762
887
|
});
|
|
763
|
-
|
|
764
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
888
|
+
throw error;
|
|
765
889
|
}
|
|
766
890
|
|
|
767
891
|
toolchainEnv = withPrependedPath(env, path.join(os.homedir(), ".cargo", "bin"));
|
|
@@ -778,6 +902,10 @@ async function ensureRustToolchain({
|
|
|
778
902
|
"Rust toolchain installed",
|
|
779
903
|
path.join(os.homedir(), ".cargo", "bin"),
|
|
780
904
|
);
|
|
905
|
+
emitTelemetry(telemetryClient, "installer_rust_install_completed", {
|
|
906
|
+
os: target.os,
|
|
907
|
+
arch: target.arch,
|
|
908
|
+
});
|
|
781
909
|
return toolchainEnv;
|
|
782
910
|
}
|
|
783
911
|
|
|
@@ -792,6 +920,7 @@ async function installSourceBuild(
|
|
|
792
920
|
fetchImpl,
|
|
793
921
|
runProcessImpl,
|
|
794
922
|
onStatus,
|
|
923
|
+
telemetryClient,
|
|
795
924
|
promptImpl = promptForApproval,
|
|
796
925
|
commandExistsImpl = commandExists,
|
|
797
926
|
},
|
|
@@ -811,6 +940,13 @@ async function installSourceBuild(
|
|
|
811
940
|
"Prebuilt asset missing; falling back to source build",
|
|
812
941
|
`${selected.tagName} for ${target.os}-${target.arch}`,
|
|
813
942
|
);
|
|
943
|
+
emitTelemetry(telemetryClient, "installer_prebuilt_asset_missing", {
|
|
944
|
+
release_tag: selected.tagName,
|
|
945
|
+
release_commit: selected.targetCommitish ?? null,
|
|
946
|
+
os: target.os,
|
|
947
|
+
arch: target.arch,
|
|
948
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
949
|
+
});
|
|
814
950
|
|
|
815
951
|
if (!(await commandExistsImpl("git", process.env))) {
|
|
816
952
|
throw new Error(
|
|
@@ -823,6 +959,7 @@ async function installSourceBuild(
|
|
|
823
959
|
fetchImpl,
|
|
824
960
|
runProcessImpl,
|
|
825
961
|
onStatus,
|
|
962
|
+
telemetryClient,
|
|
826
963
|
promptImpl,
|
|
827
964
|
commandExistsImpl,
|
|
828
965
|
});
|
|
@@ -896,6 +1033,13 @@ async function installSourceBuild(
|
|
|
896
1033
|
"Building Pylon from source",
|
|
897
1034
|
`${selected.tagName} (${sourceCommit.slice(0, 12)})`,
|
|
898
1035
|
);
|
|
1036
|
+
emitTelemetry(telemetryClient, "installer_source_build_started", {
|
|
1037
|
+
release_tag: selected.tagName,
|
|
1038
|
+
release_commit: sourceCommit,
|
|
1039
|
+
os: target.os,
|
|
1040
|
+
arch: target.arch,
|
|
1041
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1042
|
+
});
|
|
899
1043
|
await runProcessImpl(buildCommand[0], buildCommand.slice(1), {
|
|
900
1044
|
cwd: repoDir,
|
|
901
1045
|
env: buildEnv,
|
|
@@ -937,6 +1081,17 @@ async function installSourceBuild(
|
|
|
937
1081
|
"Installed source-built binaries",
|
|
938
1082
|
`${selected.tagName} for ${target.os}-${target.arch}`,
|
|
939
1083
|
);
|
|
1084
|
+
emitTelemetry(telemetryClient, "installer_source_build_completed", {
|
|
1085
|
+
release_tag: selected.tagName,
|
|
1086
|
+
release_commit: sourceCommit,
|
|
1087
|
+
os: target.os,
|
|
1088
|
+
arch: target.arch,
|
|
1089
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1090
|
+
install_source: installSourceForTelemetry(
|
|
1091
|
+
SOURCE_BUILD_INSTALL_METHOD,
|
|
1092
|
+
false,
|
|
1093
|
+
),
|
|
1094
|
+
});
|
|
940
1095
|
|
|
941
1096
|
return {
|
|
942
1097
|
...selected,
|
|
@@ -949,6 +1104,14 @@ async function installSourceBuild(
|
|
|
949
1104
|
sourceCommit,
|
|
950
1105
|
};
|
|
951
1106
|
} catch (error) {
|
|
1107
|
+
emitTelemetry(telemetryClient, "installer_source_build_failed", {
|
|
1108
|
+
release_tag: selected.tagName,
|
|
1109
|
+
release_commit: selected.targetCommitish ?? null,
|
|
1110
|
+
os: target.os,
|
|
1111
|
+
arch: target.arch,
|
|
1112
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1113
|
+
...telemetryFailureContext(error, "source_build"),
|
|
1114
|
+
});
|
|
952
1115
|
const message = error instanceof Error ? error.message : String(error);
|
|
953
1116
|
throw new Error(
|
|
954
1117
|
`${message}\nManual source-build fallback:\n${manualBuildInstructions}`,
|
|
@@ -1012,6 +1175,9 @@ async function findLatestCachedInstall(installRoot, target) {
|
|
|
1012
1175
|
pylonTuiPath,
|
|
1013
1176
|
expectedSha256: manifest.sha256 ?? null,
|
|
1014
1177
|
cached: true,
|
|
1178
|
+
installMethod: manifest.installMethod ?? RELEASE_ASSET_INSTALL_METHOD,
|
|
1179
|
+
sourceCloneUrl: manifest.sourceCloneUrl ?? null,
|
|
1180
|
+
sourceCommit: manifest.sourceCommit ?? null,
|
|
1015
1181
|
mtimeMs: manifestStat.mtimeMs,
|
|
1016
1182
|
});
|
|
1017
1183
|
} catch {
|
|
@@ -1123,6 +1289,7 @@ export async function ensureReleaseInstall(
|
|
|
1123
1289
|
fetchImpl = globalThis.fetch,
|
|
1124
1290
|
runProcessImpl = runProcess,
|
|
1125
1291
|
onStatus = null,
|
|
1292
|
+
telemetryClient = null,
|
|
1126
1293
|
promptImpl = promptForApproval,
|
|
1127
1294
|
commandExistsImpl = commandExists,
|
|
1128
1295
|
} = {},
|
|
@@ -1133,32 +1300,45 @@ export async function ensureReleaseInstall(
|
|
|
1133
1300
|
|
|
1134
1301
|
emitStatus(
|
|
1135
1302
|
onStatus,
|
|
1136
|
-
"
|
|
1303
|
+
"Checking for newer tagged Pylon releases",
|
|
1137
1304
|
options.version ? `requested ${options.version}` : "default release track",
|
|
1138
1305
|
);
|
|
1139
1306
|
const target = resolvePlatformTarget(options.platform, options.arch);
|
|
1140
1307
|
const installRoot = options.installRoot ?? defaultInstallRoot();
|
|
1141
1308
|
if (options.version) {
|
|
1142
1309
|
const requestedPaths = buildInstallPaths(installRoot, options.version, target);
|
|
1310
|
+
const requestedManifest = await readInstallManifest(requestedPaths.manifestPath);
|
|
1143
1311
|
const requestedCached =
|
|
1144
1312
|
(await pathExists(requestedPaths.pylonPath)) &&
|
|
1145
1313
|
(await pathExists(requestedPaths.pylonTuiPath));
|
|
1146
1314
|
if (requestedCached) {
|
|
1315
|
+
const installMethod =
|
|
1316
|
+
requestedManifest?.installMethod ?? RELEASE_ASSET_INSTALL_METHOD;
|
|
1147
1317
|
emitStatus(
|
|
1148
1318
|
onStatus,
|
|
1149
|
-
|
|
1319
|
+
installMethod === SOURCE_BUILD_INSTALL_METHOD
|
|
1320
|
+
? "Using cached source-built binaries"
|
|
1321
|
+
: "Using cached standalone binaries",
|
|
1150
1322
|
`pylon-v${normalizeVersion(options.version)} for ${target.os}-${target.arch}`,
|
|
1151
1323
|
);
|
|
1324
|
+
emitTelemetry(telemetryClient, "installer_cached_install_reused", {
|
|
1325
|
+
release_tag: `pylon-v${normalizeVersion(options.version)}`,
|
|
1326
|
+
release_commit: requestedManifest?.sourceCommit ?? null,
|
|
1327
|
+
os: target.os,
|
|
1328
|
+
arch: target.arch,
|
|
1329
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1330
|
+
install_source: installSourceForTelemetry(installMethod, true),
|
|
1331
|
+
});
|
|
1152
1332
|
return {
|
|
1153
1333
|
version: normalizeVersion(options.version),
|
|
1154
1334
|
tagName: `pylon-v${normalizeVersion(options.version)}`,
|
|
1155
1335
|
target,
|
|
1156
1336
|
...requestedPaths,
|
|
1157
|
-
expectedSha256:
|
|
1158
|
-
.readFile(requestedPaths.manifestPath, "utf8")
|
|
1159
|
-
.then((payload) => JSON.parse(payload).sha256)
|
|
1160
|
-
.catch(() => null),
|
|
1337
|
+
expectedSha256: requestedManifest?.sha256 ?? null,
|
|
1161
1338
|
cached: true,
|
|
1339
|
+
installMethod,
|
|
1340
|
+
sourceCloneUrl: requestedManifest?.sourceCloneUrl ?? null,
|
|
1341
|
+
sourceCommit: requestedManifest?.sourceCommit ?? null,
|
|
1162
1342
|
};
|
|
1163
1343
|
}
|
|
1164
1344
|
}
|
|
@@ -1173,6 +1353,14 @@ export async function ensureReleaseInstall(
|
|
|
1173
1353
|
apiBase: options.apiBase ?? DEFAULT_RELEASE_API_BASE,
|
|
1174
1354
|
repo: options.repo ?? DEFAULT_RELEASE_REPO,
|
|
1175
1355
|
version: options.version ?? null,
|
|
1356
|
+
target,
|
|
1357
|
+
});
|
|
1358
|
+
emitTelemetry(telemetryClient, "installer_release_resolved", {
|
|
1359
|
+
release_tag: release?.tag_name ?? null,
|
|
1360
|
+
release_commit: release?.target_commitish ?? null,
|
|
1361
|
+
os: target.os,
|
|
1362
|
+
arch: target.arch,
|
|
1363
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1176
1364
|
});
|
|
1177
1365
|
} catch (error) {
|
|
1178
1366
|
const cached = !options.version
|
|
@@ -1181,9 +1369,19 @@ export async function ensureReleaseInstall(
|
|
|
1181
1369
|
if (cached) {
|
|
1182
1370
|
emitStatus(
|
|
1183
1371
|
onStatus,
|
|
1184
|
-
|
|
1372
|
+
cached.installMethod === SOURCE_BUILD_INSTALL_METHOD
|
|
1373
|
+
? "Using cached source-built binaries"
|
|
1374
|
+
: "Using cached standalone binaries",
|
|
1185
1375
|
`release lookup failed; falling back to ${cached.tagName}`,
|
|
1186
1376
|
);
|
|
1377
|
+
emitTelemetry(telemetryClient, "installer_cached_install_reused", {
|
|
1378
|
+
release_tag: cached.tagName,
|
|
1379
|
+
release_commit: cached.sourceCommit ?? null,
|
|
1380
|
+
os: target.os,
|
|
1381
|
+
arch: target.arch,
|
|
1382
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1383
|
+
install_source: installSourceForTelemetry(cached.installMethod, true),
|
|
1384
|
+
});
|
|
1187
1385
|
return cached;
|
|
1188
1386
|
}
|
|
1189
1387
|
|
|
@@ -1204,6 +1402,14 @@ export async function ensureReleaseInstall(
|
|
|
1204
1402
|
let missingAssetsError = null;
|
|
1205
1403
|
try {
|
|
1206
1404
|
selected = selectReleaseAssets(release, target);
|
|
1405
|
+
emitTelemetry(telemetryClient, "installer_prebuilt_asset_found", {
|
|
1406
|
+
release_tag: selected.tagName,
|
|
1407
|
+
release_commit: release?.target_commitish ?? null,
|
|
1408
|
+
asset_name: selected.archiveAsset.name,
|
|
1409
|
+
os: target.os,
|
|
1410
|
+
arch: target.arch,
|
|
1411
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1412
|
+
});
|
|
1207
1413
|
} catch (error) {
|
|
1208
1414
|
if (!(error instanceof MissingReleaseAssetsError)) {
|
|
1209
1415
|
throw error;
|
|
@@ -1234,6 +1440,14 @@ export async function ensureReleaseInstall(
|
|
|
1234
1440
|
: "Using cached standalone binaries",
|
|
1235
1441
|
`${selected.tagName} for ${target.os}-${target.arch}`,
|
|
1236
1442
|
);
|
|
1443
|
+
emitTelemetry(telemetryClient, "installer_cached_install_reused", {
|
|
1444
|
+
release_tag: selected.tagName,
|
|
1445
|
+
release_commit: manifest?.sourceCommit ?? release?.target_commitish ?? null,
|
|
1446
|
+
os: target.os,
|
|
1447
|
+
arch: target.arch,
|
|
1448
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1449
|
+
install_source: installSourceForTelemetry(installMethod, true),
|
|
1450
|
+
});
|
|
1237
1451
|
return {
|
|
1238
1452
|
...selected,
|
|
1239
1453
|
...paths,
|
|
@@ -1258,6 +1472,7 @@ export async function ensureReleaseInstall(
|
|
|
1258
1472
|
fetchImpl,
|
|
1259
1473
|
runProcessImpl,
|
|
1260
1474
|
onStatus,
|
|
1475
|
+
telemetryClient,
|
|
1261
1476
|
promptImpl,
|
|
1262
1477
|
commandExistsImpl,
|
|
1263
1478
|
},
|
|
@@ -1292,20 +1507,62 @@ export async function ensureReleaseInstall(
|
|
|
1292
1507
|
"Downloading standalone binaries",
|
|
1293
1508
|
selected.archiveAsset.name,
|
|
1294
1509
|
);
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1510
|
+
emitTelemetry(telemetryClient, "installer_prebuilt_download_started", {
|
|
1511
|
+
release_tag: selected.tagName,
|
|
1512
|
+
asset_name: selected.archiveAsset.name,
|
|
1513
|
+
os: target.os,
|
|
1514
|
+
arch: target.arch,
|
|
1515
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1300
1516
|
});
|
|
1517
|
+
try {
|
|
1518
|
+
await downloadFile(fetchImpl, selected.archiveAsset.url, paths.archivePath, {
|
|
1519
|
+
runProcessImpl,
|
|
1520
|
+
onStatus,
|
|
1521
|
+
verbose: Boolean(options.verbose),
|
|
1522
|
+
stage: "Release archive download",
|
|
1523
|
+
});
|
|
1524
|
+
emitTelemetry(telemetryClient, "installer_prebuilt_download_completed", {
|
|
1525
|
+
release_tag: selected.tagName,
|
|
1526
|
+
asset_name: selected.archiveAsset.name,
|
|
1527
|
+
os: target.os,
|
|
1528
|
+
arch: target.arch,
|
|
1529
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1530
|
+
});
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
emitTelemetry(telemetryClient, "installer_prebuilt_download_failed", {
|
|
1533
|
+
release_tag: selected.tagName,
|
|
1534
|
+
asset_name: selected.archiveAsset.name,
|
|
1535
|
+
os: target.os,
|
|
1536
|
+
arch: target.arch,
|
|
1537
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1538
|
+
...telemetryFailureContext(error, "prebuilt_download"),
|
|
1539
|
+
});
|
|
1540
|
+
throw error;
|
|
1541
|
+
}
|
|
1301
1542
|
}
|
|
1302
1543
|
|
|
1303
1544
|
const actualSha256 = await sha256File(paths.archivePath);
|
|
1304
1545
|
if (actualSha256 !== expectedSha256) {
|
|
1546
|
+
emitTelemetry(telemetryClient, "installer_checksum_failed", {
|
|
1547
|
+
release_tag: selected.tagName,
|
|
1548
|
+
asset_name: selected.archiveAsset.name,
|
|
1549
|
+
os: target.os,
|
|
1550
|
+
arch: target.arch,
|
|
1551
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1552
|
+
error_stage: "checksum_verify",
|
|
1553
|
+
error_code: "sha256_mismatch",
|
|
1554
|
+
});
|
|
1305
1555
|
throw new Error(
|
|
1306
1556
|
`SHA-256 verification failed for ${selected.archiveAsset.name}: expected ${expectedSha256}, got ${actualSha256}.`,
|
|
1307
1557
|
);
|
|
1308
1558
|
}
|
|
1559
|
+
emitTelemetry(telemetryClient, "installer_checksum_verified", {
|
|
1560
|
+
release_tag: selected.tagName,
|
|
1561
|
+
asset_name: selected.archiveAsset.name,
|
|
1562
|
+
os: target.os,
|
|
1563
|
+
arch: target.arch,
|
|
1564
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1565
|
+
});
|
|
1309
1566
|
|
|
1310
1567
|
emitStatus(
|
|
1311
1568
|
onStatus,
|
|
@@ -1347,6 +1604,9 @@ export async function ensureReleaseInstall(
|
|
|
1347
1604
|
target,
|
|
1348
1605
|
expectedSha256,
|
|
1349
1606
|
cached: false,
|
|
1607
|
+
installMethod: RELEASE_ASSET_INSTALL_METHOD,
|
|
1608
|
+
sourceCloneUrl: null,
|
|
1609
|
+
sourceCommit: null,
|
|
1350
1610
|
};
|
|
1351
1611
|
}
|
|
1352
1612
|
|
|
@@ -1355,6 +1615,7 @@ export async function bootstrapInstalledPylon(
|
|
|
1355
1615
|
{
|
|
1356
1616
|
runProcessImpl = runProcess,
|
|
1357
1617
|
onStatus = null,
|
|
1618
|
+
telemetryClient = null,
|
|
1358
1619
|
} = {},
|
|
1359
1620
|
) {
|
|
1360
1621
|
const pylonPath = path.resolve(options.pylonPath);
|
|
@@ -1364,108 +1625,157 @@ export async function bootstrapInstalledPylon(
|
|
|
1364
1625
|
options.diagnosticRepeats ?? DEFAULT_DIAGNOSTIC_REPEATS;
|
|
1365
1626
|
const diagnosticMaxOutputTokens =
|
|
1366
1627
|
options.diagnosticMaxOutputTokens ?? DEFAULT_DIAGNOSTIC_MAX_OUTPUT_TOKENS;
|
|
1628
|
+
emitTelemetry(telemetryClient, "installer_smoke_test_started", {
|
|
1629
|
+
release_tag: options.tagName ?? `pylon-v${options.version}`,
|
|
1630
|
+
release_commit: options.sourceCommit ?? null,
|
|
1631
|
+
os: options.target?.os ?? null,
|
|
1632
|
+
arch: options.target?.arch ?? null,
|
|
1633
|
+
platform_key:
|
|
1634
|
+
options.target?.os && options.target?.arch
|
|
1635
|
+
? `${options.target.os}-${options.target.arch}`
|
|
1636
|
+
: null,
|
|
1637
|
+
install_source: installSourceForTelemetry(
|
|
1638
|
+
options.installMethod,
|
|
1639
|
+
Boolean(options.cached),
|
|
1640
|
+
),
|
|
1641
|
+
});
|
|
1367
1642
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
["status", "--json"],
|
|
1376
|
-
options,
|
|
1377
|
-
runProcessImpl,
|
|
1378
|
-
);
|
|
1379
|
-
emitStatus(onStatus, "Scanning for local models");
|
|
1380
|
-
const inventory = await runPylonJson(
|
|
1381
|
-
pylonPath,
|
|
1382
|
-
["inventory", "--json"],
|
|
1383
|
-
options,
|
|
1384
|
-
runProcessImpl,
|
|
1385
|
-
);
|
|
1386
|
-
|
|
1387
|
-
let download = null;
|
|
1388
|
-
if (!options.skipModelDownload) {
|
|
1389
|
-
emitStatus(onStatus, "Downloading curated model bundle", model);
|
|
1390
|
-
download = await runPylonJson(
|
|
1643
|
+
try {
|
|
1644
|
+
emitStatus(onStatus, "Verifying Pylon binary", path.basename(pylonPath));
|
|
1645
|
+
await runPylonCommand(pylonPath, ["--help"], options, runProcessImpl);
|
|
1646
|
+
emitStatus(onStatus, "Bootstrapping local Pylon identity");
|
|
1647
|
+
const init = await runPylonJson(pylonPath, ["init"], options, runProcessImpl);
|
|
1648
|
+
emitStatus(onStatus, "Checking runtime health");
|
|
1649
|
+
const status = await runPylonJson(
|
|
1391
1650
|
pylonPath,
|
|
1392
|
-
["
|
|
1651
|
+
["status", "--json"],
|
|
1393
1652
|
options,
|
|
1394
1653
|
runProcessImpl,
|
|
1395
1654
|
);
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
"
|
|
1400
|
-
|
|
1655
|
+
emitStatus(onStatus, "Scanning for local models");
|
|
1656
|
+
const inventory = await runPylonJson(
|
|
1657
|
+
pylonPath,
|
|
1658
|
+
["inventory", "--json"],
|
|
1659
|
+
options,
|
|
1660
|
+
runProcessImpl,
|
|
1401
1661
|
);
|
|
1402
|
-
}
|
|
1403
1662
|
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
diagnostic = await runPylonJson(
|
|
1663
|
+
let download = null;
|
|
1664
|
+
if (!options.skipModelDownload) {
|
|
1665
|
+
emitStatus(onStatus, "Downloading curated model bundle", model);
|
|
1666
|
+
download = await runPylonJson(
|
|
1409
1667
|
pylonPath,
|
|
1410
|
-
[
|
|
1411
|
-
"gemma",
|
|
1412
|
-
"diagnose",
|
|
1413
|
-
model,
|
|
1414
|
-
"--max-output-tokens",
|
|
1415
|
-
String(diagnosticMaxOutputTokens),
|
|
1416
|
-
"--repeats",
|
|
1417
|
-
String(diagnosticRepeats),
|
|
1418
|
-
"--json",
|
|
1419
|
-
],
|
|
1668
|
+
["gemma", "download", model, "--json"],
|
|
1420
1669
|
options,
|
|
1421
1670
|
runProcessImpl,
|
|
1422
1671
|
);
|
|
1423
|
-
}
|
|
1424
|
-
if (!isUnsupportedGemmaDiagnoseError(error)) {
|
|
1425
|
-
throw error;
|
|
1426
|
-
}
|
|
1672
|
+
} else {
|
|
1427
1673
|
emitStatus(
|
|
1428
1674
|
onStatus,
|
|
1429
|
-
"Skipping
|
|
1430
|
-
"
|
|
1675
|
+
"Skipping optional curated GGUF cache",
|
|
1676
|
+
"use --download-curated-cache to prefetch Hugging Face weights",
|
|
1431
1677
|
);
|
|
1432
1678
|
}
|
|
1433
|
-
} else {
|
|
1434
|
-
emitStatus(onStatus, "Skipping first-run diagnostic", model);
|
|
1435
|
-
}
|
|
1436
1679
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1680
|
+
let diagnostic = null;
|
|
1681
|
+
if (!options.skipDiagnostics) {
|
|
1682
|
+
emitStatus(onStatus, "Running first-run diagnostic", model);
|
|
1683
|
+
try {
|
|
1684
|
+
diagnostic = await runPylonJson(
|
|
1685
|
+
pylonPath,
|
|
1686
|
+
[
|
|
1687
|
+
"gemma",
|
|
1688
|
+
"diagnose",
|
|
1689
|
+
model,
|
|
1690
|
+
"--max-output-tokens",
|
|
1691
|
+
String(diagnosticMaxOutputTokens),
|
|
1692
|
+
"--repeats",
|
|
1693
|
+
String(diagnosticRepeats),
|
|
1694
|
+
"--json",
|
|
1695
|
+
],
|
|
1696
|
+
options,
|
|
1697
|
+
runProcessImpl,
|
|
1698
|
+
);
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
if (!isUnsupportedGemmaDiagnoseError(error)) {
|
|
1701
|
+
throw error;
|
|
1702
|
+
}
|
|
1703
|
+
emitStatus(
|
|
1704
|
+
onStatus,
|
|
1705
|
+
"Skipping first-run diagnostic",
|
|
1706
|
+
"installed Pylon release does not expose gemma diagnose",
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
} else {
|
|
1710
|
+
emitStatus(onStatus, "Skipping first-run diagnostic", model);
|
|
1711
|
+
}
|
|
1441
1712
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
? `diagnostic ${diagnosticResult.status}`
|
|
1447
|
-
: "smoke path complete",
|
|
1448
|
-
);
|
|
1713
|
+
const diagnosticResult =
|
|
1714
|
+
diagnostic?.results?.find((result) => result.model_id === model) ??
|
|
1715
|
+
diagnostic?.results?.[0] ??
|
|
1716
|
+
null;
|
|
1449
1717
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1718
|
+
emitStatus(
|
|
1719
|
+
onStatus,
|
|
1720
|
+
"Bootstrap complete",
|
|
1721
|
+
diagnosticResult?.status
|
|
1722
|
+
? `diagnostic ${diagnosticResult.status}`
|
|
1723
|
+
: "smoke path complete",
|
|
1724
|
+
);
|
|
1725
|
+
emitTelemetry(telemetryClient, "installer_smoke_test_completed", {
|
|
1726
|
+
release_tag: options.tagName ?? `pylon-v${options.version}`,
|
|
1727
|
+
release_commit: options.sourceCommit ?? null,
|
|
1728
|
+
os: options.target?.os ?? null,
|
|
1729
|
+
arch: options.target?.arch ?? null,
|
|
1730
|
+
platform_key:
|
|
1731
|
+
options.target?.os && options.target?.arch
|
|
1732
|
+
? `${options.target.os}-${options.target.arch}`
|
|
1733
|
+
: null,
|
|
1734
|
+
install_source: installSourceForTelemetry(
|
|
1735
|
+
options.installMethod,
|
|
1736
|
+
Boolean(options.cached),
|
|
1737
|
+
),
|
|
1738
|
+
diagnostic_status: diagnosticResult?.status ?? null,
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
return {
|
|
1742
|
+
version: options.version,
|
|
1743
|
+
tagName: options.tagName ?? `pylon-v${options.version}`,
|
|
1744
|
+
target: options.target,
|
|
1745
|
+
cached: Boolean(options.cached),
|
|
1746
|
+
installMethod: options.installMethod ?? RELEASE_ASSET_INSTALL_METHOD,
|
|
1747
|
+
binaries: {
|
|
1748
|
+
pylon: pylonPath,
|
|
1749
|
+
pylonTui: pylonTuiPath,
|
|
1750
|
+
},
|
|
1751
|
+
configPath: init?.config_path ?? options.configPath ?? null,
|
|
1752
|
+
pylonHome: options.pylonHome ? path.resolve(options.pylonHome) : null,
|
|
1753
|
+
init,
|
|
1754
|
+
status,
|
|
1755
|
+
inventory,
|
|
1756
|
+
model,
|
|
1757
|
+
download,
|
|
1758
|
+
diagnostic,
|
|
1759
|
+
diagnosticResult,
|
|
1760
|
+
};
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
emitTelemetry(telemetryClient, "installer_smoke_test_failed", {
|
|
1763
|
+
release_tag: options.tagName ?? `pylon-v${options.version}`,
|
|
1764
|
+
release_commit: options.sourceCommit ?? null,
|
|
1765
|
+
os: options.target?.os ?? null,
|
|
1766
|
+
arch: options.target?.arch ?? null,
|
|
1767
|
+
platform_key:
|
|
1768
|
+
options.target?.os && options.target?.arch
|
|
1769
|
+
? `${options.target.os}-${options.target.arch}`
|
|
1770
|
+
: null,
|
|
1771
|
+
install_source: installSourceForTelemetry(
|
|
1772
|
+
options.installMethod,
|
|
1773
|
+
Boolean(options.cached),
|
|
1774
|
+
),
|
|
1775
|
+
...telemetryFailureContext(error, "smoke_test"),
|
|
1776
|
+
});
|
|
1777
|
+
throw error;
|
|
1778
|
+
}
|
|
1469
1779
|
}
|
|
1470
1780
|
|
|
1471
1781
|
export async function launchInstalledPylonTui(
|
|
@@ -1483,14 +1793,100 @@ export async function launchInstalledPylonTui(
|
|
|
1483
1793
|
});
|
|
1484
1794
|
}
|
|
1485
1795
|
|
|
1796
|
+
export function resolveBootstrapOutcome(summary) {
|
|
1797
|
+
const runtimeState =
|
|
1798
|
+
summary.status?.snapshot?.runtime?.authoritative_status ?? "unknown";
|
|
1799
|
+
const localGemma = summary.status?.snapshot?.availability?.local_gemma ?? {};
|
|
1800
|
+
const readyModel = localGemma.ready_model ?? null;
|
|
1801
|
+
const localGemmaError = localGemma.last_error ?? null;
|
|
1802
|
+
const diagnosticStatus = summary.diagnosticResult?.status ?? null;
|
|
1803
|
+
|
|
1804
|
+
if (runtimeState === "online") {
|
|
1805
|
+
return {
|
|
1806
|
+
level: "success",
|
|
1807
|
+
verdict: "fully online",
|
|
1808
|
+
detail: readyModel
|
|
1809
|
+
? `loaded runtime model ${readyModel}`
|
|
1810
|
+
: "eligible local Gemma supply is online",
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
if (readyModel) {
|
|
1815
|
+
return {
|
|
1816
|
+
level: "success",
|
|
1817
|
+
verdict: "runtime ready",
|
|
1818
|
+
detail: `loaded runtime model ${readyModel}`,
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
const loweredError = (localGemmaError ?? "").toLowerCase();
|
|
1823
|
+
if (loweredError.includes("/api/tags") || loweredError.includes("not reachable")) {
|
|
1824
|
+
return {
|
|
1825
|
+
level: "warning",
|
|
1826
|
+
verdict: "installed but runtime missing",
|
|
1827
|
+
detail:
|
|
1828
|
+
"no Ollama-compatible local runtime is answering /api/tags yet",
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
if (
|
|
1833
|
+
diagnosticStatus &&
|
|
1834
|
+
diagnosticStatus !== "completed" &&
|
|
1835
|
+
diagnosticStatus !== "passed" &&
|
|
1836
|
+
diagnosticStatus !== "healthy"
|
|
1837
|
+
) {
|
|
1838
|
+
return {
|
|
1839
|
+
level: "warning",
|
|
1840
|
+
verdict: "installed but runtime not yet usable",
|
|
1841
|
+
detail: diagnosticStatus,
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
return {
|
|
1846
|
+
level: "warning",
|
|
1847
|
+
verdict: "installed",
|
|
1848
|
+
detail: "complete the local runtime setup before bringing the node online",
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function renderBootstrapNextSteps(summary, outcome) {
|
|
1853
|
+
const lines = [
|
|
1854
|
+
"Launcher path: use the same npx/bunx command again, or install globally and run `pylon`.",
|
|
1855
|
+
];
|
|
1856
|
+
|
|
1857
|
+
if (outcome.verdict === "fully online" || outcome.verdict === "runtime ready") {
|
|
1858
|
+
lines.push("Next step: open the TUI with `pylon`, or keep using the package-managed launcher.");
|
|
1859
|
+
return lines;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
if (summary.target?.os === "darwin") {
|
|
1863
|
+
lines.push(
|
|
1864
|
+
"Runtime setup (macOS default): `brew install ollama`, `brew services start ollama`, `ollama pull gemma4:e4b`.",
|
|
1865
|
+
);
|
|
1866
|
+
} else {
|
|
1867
|
+
lines.push(
|
|
1868
|
+
"Runtime setup: start an Ollama-compatible local runtime at `local_gemma_base_url` and load `gemma4:e4b`.",
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
lines.push(
|
|
1872
|
+
"Persistent PATH command: `npm install -g @openagentsinc/pylon` or `bun install -g @openagentsinc/pylon`, then run `pylon`.",
|
|
1873
|
+
);
|
|
1874
|
+
return lines;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1486
1877
|
export function renderBootstrapSummary(summary) {
|
|
1878
|
+
const outcome = resolveBootstrapOutcome(summary);
|
|
1487
1879
|
const lines = [
|
|
1880
|
+
`Onboarding verdict: ${outcome.verdict}`,
|
|
1881
|
+
`Verdict detail: ${outcome.detail}`,
|
|
1488
1882
|
`Pylon release: ${summary.version} (${summary.target.os}-${summary.target.arch})`,
|
|
1489
1883
|
`Archive source: ${summary.tagName}`,
|
|
1884
|
+
`Install source: ${installSourceForTelemetry(summary.installMethod, summary.cached).replaceAll("_", " ")}`,
|
|
1490
1885
|
`Installed from cache: ${summary.cached ? "yes" : "no"}`,
|
|
1491
1886
|
`Pylon binary: ${summary.binaries.pylon}`,
|
|
1492
1887
|
`Pylon TUI: ${summary.binaries.pylonTui}`,
|
|
1493
1888
|
`Config path: ${summary.configPath ?? "unknown"}`,
|
|
1889
|
+
`Preferred runtime model name: ${PREFERRED_RUNTIME_MODEL_NAME}`,
|
|
1494
1890
|
];
|
|
1495
1891
|
|
|
1496
1892
|
const statusState =
|
|
@@ -1549,5 +1945,7 @@ export function renderBootstrapSummary(summary) {
|
|
|
1549
1945
|
}
|
|
1550
1946
|
}
|
|
1551
1947
|
|
|
1948
|
+
lines.push(...renderBootstrapNextSteps(summary, outcome));
|
|
1949
|
+
|
|
1552
1950
|
return lines.join("\n");
|
|
1553
1951
|
}
|
package/src/telemetry.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_TELEMETRY_ENDPOINT =
|
|
4
|
+
"https://openagents.com/api/telemetry/events";
|
|
5
|
+
const DEFAULT_TELEMETRY_TIMEOUT_MS = 2_000;
|
|
6
|
+
|
|
7
|
+
function timedSignal(timeoutMs = DEFAULT_TELEMETRY_TIMEOUT_MS) {
|
|
8
|
+
if (typeof AbortSignal?.timeout === "function") {
|
|
9
|
+
return {
|
|
10
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
11
|
+
dispose() {},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
17
|
+
return {
|
|
18
|
+
signal: controller.signal,
|
|
19
|
+
dispose() {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cleanString(value, fallback = null) {
|
|
26
|
+
if (typeof value !== "string") {
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
return trimmed.length > 0 ? trimmed : fallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeProperties(properties = {}) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(JSON.stringify(properties ?? {}));
|
|
37
|
+
} catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function detectPackageInvoker(env = process.env) {
|
|
43
|
+
const userAgent = cleanString(env.npm_config_user_agent);
|
|
44
|
+
if (userAgent?.includes("bun/")) {
|
|
45
|
+
return "bun";
|
|
46
|
+
}
|
|
47
|
+
if (userAgent?.includes("npm/")) {
|
|
48
|
+
return "npm";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const execPath = cleanString(env.npm_execpath);
|
|
52
|
+
if (execPath?.includes("bun")) {
|
|
53
|
+
return "bun";
|
|
54
|
+
}
|
|
55
|
+
if (execPath?.includes("npm")) {
|
|
56
|
+
return "npm";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return "unknown";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function installSourceForTelemetry(installMethod, cached) {
|
|
63
|
+
if (installMethod === "source_build") {
|
|
64
|
+
return cached ? "cached_source_build" : "source_build";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return cached ? "cached_prebuilt" : "prebuilt";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function telemetryFailureContext(error, fallbackStage = "unknown") {
|
|
71
|
+
const cause = error?.cause ?? null;
|
|
72
|
+
const stage = cleanString(error?.stage)?.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
73
|
+
const code =
|
|
74
|
+
cleanString(error?.code) ??
|
|
75
|
+
cleanString(error?.errno) ??
|
|
76
|
+
cleanString(cause?.code) ??
|
|
77
|
+
cleanString(cause?.errno) ??
|
|
78
|
+
(typeof error?.httpStatus === "number" ? `http_${error.httpStatus}` : null) ??
|
|
79
|
+
"unknown";
|
|
80
|
+
const message =
|
|
81
|
+
(error instanceof Error ? error.message : String(error)).split("\n")[0] ??
|
|
82
|
+
"unknown error";
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
error_stage: stage || fallbackStage,
|
|
86
|
+
error_code: code,
|
|
87
|
+
error_message: message.slice(0, 240),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createTelemetryClient({
|
|
92
|
+
endpoint = process.env.OPENAGENTS_TELEMETRY_URL ?? DEFAULT_TELEMETRY_ENDPOINT,
|
|
93
|
+
fetchImpl = globalThis.fetch,
|
|
94
|
+
anonymousActorId = randomUUID(),
|
|
95
|
+
sessionId = anonymousActorId,
|
|
96
|
+
installId = anonymousActorId,
|
|
97
|
+
appVersion = null,
|
|
98
|
+
sourceSurface = "installer",
|
|
99
|
+
} = {}) {
|
|
100
|
+
const pending = new Set();
|
|
101
|
+
const enabled =
|
|
102
|
+
typeof fetchImpl === "function" &&
|
|
103
|
+
Boolean(cleanString(endpoint)) &&
|
|
104
|
+
process.env.OPENAGENTS_DISABLE_TELEMETRY !== "1";
|
|
105
|
+
|
|
106
|
+
async function post(payload) {
|
|
107
|
+
if (!enabled) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const timeout = timedSignal();
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetchImpl(endpoint, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
accept: "application/json",
|
|
117
|
+
"content-type": "application/json",
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify(payload),
|
|
120
|
+
signal: timeout.signal,
|
|
121
|
+
});
|
|
122
|
+
return response.ok;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
} finally {
|
|
126
|
+
timeout.dispose();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
endpoint,
|
|
132
|
+
anonymousActorId,
|
|
133
|
+
sessionId,
|
|
134
|
+
installId,
|
|
135
|
+
emit(eventName, properties = {}) {
|
|
136
|
+
const promise = post({
|
|
137
|
+
event_name: eventName,
|
|
138
|
+
source_surface: sourceSurface,
|
|
139
|
+
occurred_at: new Date().toISOString(),
|
|
140
|
+
anonymous_actor_id: anonymousActorId,
|
|
141
|
+
session_id: sessionId,
|
|
142
|
+
install_id: installId,
|
|
143
|
+
app_version: cleanString(appVersion),
|
|
144
|
+
properties: normalizeProperties(properties),
|
|
145
|
+
});
|
|
146
|
+
pending.add(promise);
|
|
147
|
+
promise.finally(() => {
|
|
148
|
+
pending.delete(promise);
|
|
149
|
+
});
|
|
150
|
+
return promise;
|
|
151
|
+
},
|
|
152
|
+
async flush() {
|
|
153
|
+
const current = [...pending];
|
|
154
|
+
if (current.length === 0) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
await Promise.allSettled(current);
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|