@openagentsinc/pylon 0.1.3 → 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 +8 -1
- package/package.json +1 -1
- package/src/cli.js +106 -40
- package/src/index.js +427 -117
- package/src/telemetry.js +160 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npx @openagentsinc/pylon
|
|
|
13
13
|
bunx @openagentsinc/pylon
|
|
14
14
|
npm install -g @openagentsinc/pylon && pylon
|
|
15
15
|
bun install -g @openagentsinc/pylon && pylon
|
|
16
|
-
npx @openagentsinc/pylon --version 0.0.1-
|
|
16
|
+
npx @openagentsinc/pylon --version 0.0.1-rc10
|
|
17
17
|
npx @openagentsinc/pylon --no-launch
|
|
18
18
|
npx @openagentsinc/pylon --download-curated-cache --model gemma-4-e2b --diagnostic-repeats 2
|
|
19
19
|
npx @openagentsinc/pylon --verbose
|
|
@@ -31,6 +31,9 @@ The launcher:
|
|
|
31
31
|
`pylon-tui` locally when no matching release asset exists for the machine
|
|
32
32
|
- prompts before installing the Rust toolchain via `rustup` if a source build
|
|
33
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
|
|
34
37
|
- downloads the archive and published SHA-256 checksum
|
|
35
38
|
- verifies the checksum before extracting
|
|
36
39
|
- caches the unpacked binaries under `~/.openagents/pylon/bootstrap/`
|
|
@@ -53,6 +56,10 @@ The launcher:
|
|
|
53
56
|
bootstrap stays honest about the separate Ollama-compatible runtime
|
|
54
57
|
prerequisite instead of mutating the host behind the user's back
|
|
55
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.
|
|
62
|
+
|
|
56
63
|
## Publish
|
|
57
64
|
|
|
58
65
|
Publish directly from this package directory:
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -8,8 +8,15 @@ import {
|
|
|
8
8
|
ensureReleaseInstall,
|
|
9
9
|
launchInstalledPylonTui,
|
|
10
10
|
resolveBootstrapOutcome,
|
|
11
|
+
resolvePlatformTarget,
|
|
11
12
|
renderBootstrapSummary,
|
|
12
13
|
} from "./index.js";
|
|
14
|
+
import {
|
|
15
|
+
createTelemetryClient,
|
|
16
|
+
detectPackageInvoker,
|
|
17
|
+
installSourceForTelemetry,
|
|
18
|
+
telemetryFailureContext,
|
|
19
|
+
} from "./telemetry.js";
|
|
13
20
|
|
|
14
21
|
function parseIntegerFlag(value, label) {
|
|
15
22
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -221,6 +228,7 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
221
228
|
ensureReleaseInstallImpl = ensureReleaseInstall,
|
|
222
229
|
bootstrapInstalledPylonImpl = bootstrapInstalledPylon,
|
|
223
230
|
launchInstalledPylonTuiImpl = launchInstalledPylonTui,
|
|
231
|
+
createTelemetryClientImpl = createTelemetryClient,
|
|
224
232
|
} = dependencies;
|
|
225
233
|
const options = parseArgs(argv);
|
|
226
234
|
if (options.help) {
|
|
@@ -229,51 +237,109 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
229
237
|
}
|
|
230
238
|
|
|
231
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
|
+
};
|
|
232
263
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
{
|
|
239
|
-
...options,
|
|
240
|
-
...install,
|
|
241
|
-
version: install.version,
|
|
242
|
-
},
|
|
243
|
-
{
|
|
264
|
+
telemetryClient?.emit?.("installer_started", sharedTelemetry);
|
|
265
|
+
|
|
266
|
+
let install = null;
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
install = await ensureReleaseInstallImpl(options, {
|
|
244
270
|
...dependencies,
|
|
245
271
|
onStatus: reporter?.status,
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
);
|
|
248
286
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
version: install.version,
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
...dependencies,
|
|
268
|
-
onStatus: reporter?.status,
|
|
269
|
-
},
|
|
270
|
-
);
|
|
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?.();
|
|
299
|
+
|
|
300
|
+
if (options.json) {
|
|
301
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
271
302
|
} else {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
}
|
|
276
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;
|
|
277
344
|
}
|
|
278
|
-
return summary;
|
|
279
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";
|
|
@@ -31,10 +37,82 @@ function emitVerboseStatus(onStatus, verbose, message, detail = null) {
|
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
function emitTelemetry(telemetryClient, eventName, properties = {}) {
|
|
41
|
+
if (typeof telemetryClient?.emit === "function") {
|
|
42
|
+
void telemetryClient.emit(eventName, properties);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
function normalizeVersion(value) {
|
|
35
47
|
return value.replace(/^pylon-v/, "").replace(/^v/, "");
|
|
36
48
|
}
|
|
37
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
|
+
|
|
38
116
|
function createBootstrapError(message, context = {}) {
|
|
39
117
|
const error = new Error(message);
|
|
40
118
|
Object.assign(error, context);
|
|
@@ -497,14 +575,31 @@ export function isPylonReleaseTag(tagName) {
|
|
|
497
575
|
);
|
|
498
576
|
}
|
|
499
577
|
|
|
500
|
-
|
|
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) {
|
|
501
593
|
if (!Array.isArray(releases)) {
|
|
502
594
|
throw new Error("GitHub release lookup did not return a release list.");
|
|
503
595
|
}
|
|
504
596
|
|
|
505
|
-
const
|
|
506
|
-
(candidate) => !candidate?.draft && isPylonReleaseTag(candidate?.tag_name)
|
|
507
|
-
|
|
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];
|
|
508
603
|
if (!release) {
|
|
509
604
|
throw new Error(
|
|
510
605
|
`GitHub release lookup did not find any published ${PYLON_RELEASE_TAG_PREFIX} releases.`,
|
|
@@ -522,6 +617,7 @@ export async function fetchReleaseMetadata({
|
|
|
522
617
|
apiBase = DEFAULT_RELEASE_API_BASE,
|
|
523
618
|
repo = DEFAULT_RELEASE_REPO,
|
|
524
619
|
version = null,
|
|
620
|
+
target = null,
|
|
525
621
|
} = {}) {
|
|
526
622
|
const normalizedVersion = normalizeRequestedVersion(version);
|
|
527
623
|
const endpoint = normalizedVersion
|
|
@@ -538,7 +634,7 @@ export async function fetchReleaseMetadata({
|
|
|
538
634
|
? "GitHub tagged release lookup"
|
|
539
635
|
: "GitHub release list lookup",
|
|
540
636
|
});
|
|
541
|
-
return normalizedVersion ? payload : selectLatestPylonRelease(payload);
|
|
637
|
+
return normalizedVersion ? payload : selectLatestPylonRelease(payload, target);
|
|
542
638
|
}
|
|
543
639
|
|
|
544
640
|
export function selectReleaseAssets(release, target) {
|
|
@@ -713,6 +809,7 @@ async function ensureRustToolchain({
|
|
|
713
809
|
fetchImpl,
|
|
714
810
|
runProcessImpl,
|
|
715
811
|
onStatus,
|
|
812
|
+
telemetryClient,
|
|
716
813
|
promptImpl = promptForApproval,
|
|
717
814
|
commandExistsImpl = commandExists,
|
|
718
815
|
env = process.env,
|
|
@@ -725,44 +822,70 @@ async function ensureRustToolchain({
|
|
|
725
822
|
return toolchainEnv;
|
|
726
823
|
}
|
|
727
824
|
|
|
825
|
+
emitTelemetry(telemetryClient, "installer_rust_missing", {
|
|
826
|
+
os: target.os,
|
|
827
|
+
arch: target.arch,
|
|
828
|
+
});
|
|
829
|
+
|
|
728
830
|
emitStatus(
|
|
729
831
|
onStatus,
|
|
730
832
|
"Rust toolchain required for source build",
|
|
731
833
|
`${target.os}-${target.arch}`,
|
|
732
834
|
);
|
|
733
835
|
|
|
836
|
+
emitTelemetry(telemetryClient, "installer_rust_install_prompt_shown", {
|
|
837
|
+
os: target.os,
|
|
838
|
+
arch: target.arch,
|
|
839
|
+
});
|
|
734
840
|
const approved = await promptImpl(
|
|
735
841
|
`Rust is required to build Pylon from source for ${target.os}-${target.arch}. Install the official Rust toolchain now via rustup?`,
|
|
736
842
|
);
|
|
737
843
|
if (!approved) {
|
|
844
|
+
emitTelemetry(telemetryClient, "installer_rust_install_declined", {
|
|
845
|
+
os: target.os,
|
|
846
|
+
arch: target.arch,
|
|
847
|
+
});
|
|
738
848
|
throw new Error(
|
|
739
849
|
`Rust is required to build Pylon from source.\nInstall it manually and rerun:\n${rustInstallCommand()}`,
|
|
740
850
|
);
|
|
741
851
|
}
|
|
742
852
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
accept: "text/plain",
|
|
747
|
-
"user-agent": "@openagentsinc/pylon bootstrap",
|
|
748
|
-
},
|
|
749
|
-
runProcessImpl,
|
|
750
|
-
onStatus,
|
|
751
|
-
stage: "Rust toolchain installer download",
|
|
853
|
+
emitTelemetry(telemetryClient, "installer_rust_install_approved", {
|
|
854
|
+
os: target.os,
|
|
855
|
+
arch: target.arch,
|
|
752
856
|
});
|
|
753
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pylon-rustup-"));
|
|
754
|
-
const scriptPath = path.join(tempDir, "rustup-init.sh");
|
|
755
857
|
|
|
858
|
+
emitStatus(onStatus, "Installing Rust toolchain", "official rustup installer");
|
|
756
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");
|
|
757
871
|
await fs.writeFile(scriptPath, scriptPayload);
|
|
758
872
|
await fs.chmod(scriptPath, 0o755);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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"),
|
|
763
887
|
});
|
|
764
|
-
|
|
765
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
888
|
+
throw error;
|
|
766
889
|
}
|
|
767
890
|
|
|
768
891
|
toolchainEnv = withPrependedPath(env, path.join(os.homedir(), ".cargo", "bin"));
|
|
@@ -779,6 +902,10 @@ async function ensureRustToolchain({
|
|
|
779
902
|
"Rust toolchain installed",
|
|
780
903
|
path.join(os.homedir(), ".cargo", "bin"),
|
|
781
904
|
);
|
|
905
|
+
emitTelemetry(telemetryClient, "installer_rust_install_completed", {
|
|
906
|
+
os: target.os,
|
|
907
|
+
arch: target.arch,
|
|
908
|
+
});
|
|
782
909
|
return toolchainEnv;
|
|
783
910
|
}
|
|
784
911
|
|
|
@@ -793,6 +920,7 @@ async function installSourceBuild(
|
|
|
793
920
|
fetchImpl,
|
|
794
921
|
runProcessImpl,
|
|
795
922
|
onStatus,
|
|
923
|
+
telemetryClient,
|
|
796
924
|
promptImpl = promptForApproval,
|
|
797
925
|
commandExistsImpl = commandExists,
|
|
798
926
|
},
|
|
@@ -812,6 +940,13 @@ async function installSourceBuild(
|
|
|
812
940
|
"Prebuilt asset missing; falling back to source build",
|
|
813
941
|
`${selected.tagName} for ${target.os}-${target.arch}`,
|
|
814
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
|
+
});
|
|
815
950
|
|
|
816
951
|
if (!(await commandExistsImpl("git", process.env))) {
|
|
817
952
|
throw new Error(
|
|
@@ -824,6 +959,7 @@ async function installSourceBuild(
|
|
|
824
959
|
fetchImpl,
|
|
825
960
|
runProcessImpl,
|
|
826
961
|
onStatus,
|
|
962
|
+
telemetryClient,
|
|
827
963
|
promptImpl,
|
|
828
964
|
commandExistsImpl,
|
|
829
965
|
});
|
|
@@ -897,6 +1033,13 @@ async function installSourceBuild(
|
|
|
897
1033
|
"Building Pylon from source",
|
|
898
1034
|
`${selected.tagName} (${sourceCommit.slice(0, 12)})`,
|
|
899
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
|
+
});
|
|
900
1043
|
await runProcessImpl(buildCommand[0], buildCommand.slice(1), {
|
|
901
1044
|
cwd: repoDir,
|
|
902
1045
|
env: buildEnv,
|
|
@@ -938,6 +1081,17 @@ async function installSourceBuild(
|
|
|
938
1081
|
"Installed source-built binaries",
|
|
939
1082
|
`${selected.tagName} for ${target.os}-${target.arch}`,
|
|
940
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
|
+
});
|
|
941
1095
|
|
|
942
1096
|
return {
|
|
943
1097
|
...selected,
|
|
@@ -950,6 +1104,14 @@ async function installSourceBuild(
|
|
|
950
1104
|
sourceCommit,
|
|
951
1105
|
};
|
|
952
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
|
+
});
|
|
953
1115
|
const message = error instanceof Error ? error.message : String(error);
|
|
954
1116
|
throw new Error(
|
|
955
1117
|
`${message}\nManual source-build fallback:\n${manualBuildInstructions}`,
|
|
@@ -1013,6 +1175,9 @@ async function findLatestCachedInstall(installRoot, target) {
|
|
|
1013
1175
|
pylonTuiPath,
|
|
1014
1176
|
expectedSha256: manifest.sha256 ?? null,
|
|
1015
1177
|
cached: true,
|
|
1178
|
+
installMethod: manifest.installMethod ?? RELEASE_ASSET_INSTALL_METHOD,
|
|
1179
|
+
sourceCloneUrl: manifest.sourceCloneUrl ?? null,
|
|
1180
|
+
sourceCommit: manifest.sourceCommit ?? null,
|
|
1016
1181
|
mtimeMs: manifestStat.mtimeMs,
|
|
1017
1182
|
});
|
|
1018
1183
|
} catch {
|
|
@@ -1124,6 +1289,7 @@ export async function ensureReleaseInstall(
|
|
|
1124
1289
|
fetchImpl = globalThis.fetch,
|
|
1125
1290
|
runProcessImpl = runProcess,
|
|
1126
1291
|
onStatus = null,
|
|
1292
|
+
telemetryClient = null,
|
|
1127
1293
|
promptImpl = promptForApproval,
|
|
1128
1294
|
commandExistsImpl = commandExists,
|
|
1129
1295
|
} = {},
|
|
@@ -1141,25 +1307,38 @@ export async function ensureReleaseInstall(
|
|
|
1141
1307
|
const installRoot = options.installRoot ?? defaultInstallRoot();
|
|
1142
1308
|
if (options.version) {
|
|
1143
1309
|
const requestedPaths = buildInstallPaths(installRoot, options.version, target);
|
|
1310
|
+
const requestedManifest = await readInstallManifest(requestedPaths.manifestPath);
|
|
1144
1311
|
const requestedCached =
|
|
1145
1312
|
(await pathExists(requestedPaths.pylonPath)) &&
|
|
1146
1313
|
(await pathExists(requestedPaths.pylonTuiPath));
|
|
1147
1314
|
if (requestedCached) {
|
|
1315
|
+
const installMethod =
|
|
1316
|
+
requestedManifest?.installMethod ?? RELEASE_ASSET_INSTALL_METHOD;
|
|
1148
1317
|
emitStatus(
|
|
1149
1318
|
onStatus,
|
|
1150
|
-
|
|
1319
|
+
installMethod === SOURCE_BUILD_INSTALL_METHOD
|
|
1320
|
+
? "Using cached source-built binaries"
|
|
1321
|
+
: "Using cached standalone binaries",
|
|
1151
1322
|
`pylon-v${normalizeVersion(options.version)} for ${target.os}-${target.arch}`,
|
|
1152
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
|
+
});
|
|
1153
1332
|
return {
|
|
1154
1333
|
version: normalizeVersion(options.version),
|
|
1155
1334
|
tagName: `pylon-v${normalizeVersion(options.version)}`,
|
|
1156
1335
|
target,
|
|
1157
1336
|
...requestedPaths,
|
|
1158
|
-
expectedSha256:
|
|
1159
|
-
.readFile(requestedPaths.manifestPath, "utf8")
|
|
1160
|
-
.then((payload) => JSON.parse(payload).sha256)
|
|
1161
|
-
.catch(() => null),
|
|
1337
|
+
expectedSha256: requestedManifest?.sha256 ?? null,
|
|
1162
1338
|
cached: true,
|
|
1339
|
+
installMethod,
|
|
1340
|
+
sourceCloneUrl: requestedManifest?.sourceCloneUrl ?? null,
|
|
1341
|
+
sourceCommit: requestedManifest?.sourceCommit ?? null,
|
|
1163
1342
|
};
|
|
1164
1343
|
}
|
|
1165
1344
|
}
|
|
@@ -1174,6 +1353,14 @@ export async function ensureReleaseInstall(
|
|
|
1174
1353
|
apiBase: options.apiBase ?? DEFAULT_RELEASE_API_BASE,
|
|
1175
1354
|
repo: options.repo ?? DEFAULT_RELEASE_REPO,
|
|
1176
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}`,
|
|
1177
1364
|
});
|
|
1178
1365
|
} catch (error) {
|
|
1179
1366
|
const cached = !options.version
|
|
@@ -1182,9 +1369,19 @@ export async function ensureReleaseInstall(
|
|
|
1182
1369
|
if (cached) {
|
|
1183
1370
|
emitStatus(
|
|
1184
1371
|
onStatus,
|
|
1185
|
-
|
|
1372
|
+
cached.installMethod === SOURCE_BUILD_INSTALL_METHOD
|
|
1373
|
+
? "Using cached source-built binaries"
|
|
1374
|
+
: "Using cached standalone binaries",
|
|
1186
1375
|
`release lookup failed; falling back to ${cached.tagName}`,
|
|
1187
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
|
+
});
|
|
1188
1385
|
return cached;
|
|
1189
1386
|
}
|
|
1190
1387
|
|
|
@@ -1205,6 +1402,14 @@ export async function ensureReleaseInstall(
|
|
|
1205
1402
|
let missingAssetsError = null;
|
|
1206
1403
|
try {
|
|
1207
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
|
+
});
|
|
1208
1413
|
} catch (error) {
|
|
1209
1414
|
if (!(error instanceof MissingReleaseAssetsError)) {
|
|
1210
1415
|
throw error;
|
|
@@ -1235,6 +1440,14 @@ export async function ensureReleaseInstall(
|
|
|
1235
1440
|
: "Using cached standalone binaries",
|
|
1236
1441
|
`${selected.tagName} for ${target.os}-${target.arch}`,
|
|
1237
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
|
+
});
|
|
1238
1451
|
return {
|
|
1239
1452
|
...selected,
|
|
1240
1453
|
...paths,
|
|
@@ -1259,6 +1472,7 @@ export async function ensureReleaseInstall(
|
|
|
1259
1472
|
fetchImpl,
|
|
1260
1473
|
runProcessImpl,
|
|
1261
1474
|
onStatus,
|
|
1475
|
+
telemetryClient,
|
|
1262
1476
|
promptImpl,
|
|
1263
1477
|
commandExistsImpl,
|
|
1264
1478
|
},
|
|
@@ -1293,20 +1507,62 @@ export async function ensureReleaseInstall(
|
|
|
1293
1507
|
"Downloading standalone binaries",
|
|
1294
1508
|
selected.archiveAsset.name,
|
|
1295
1509
|
);
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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}`,
|
|
1301
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
|
+
}
|
|
1302
1542
|
}
|
|
1303
1543
|
|
|
1304
1544
|
const actualSha256 = await sha256File(paths.archivePath);
|
|
1305
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
|
+
});
|
|
1306
1555
|
throw new Error(
|
|
1307
1556
|
`SHA-256 verification failed for ${selected.archiveAsset.name}: expected ${expectedSha256}, got ${actualSha256}.`,
|
|
1308
1557
|
);
|
|
1309
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
|
+
});
|
|
1310
1566
|
|
|
1311
1567
|
emitStatus(
|
|
1312
1568
|
onStatus,
|
|
@@ -1348,6 +1604,9 @@ export async function ensureReleaseInstall(
|
|
|
1348
1604
|
target,
|
|
1349
1605
|
expectedSha256,
|
|
1350
1606
|
cached: false,
|
|
1607
|
+
installMethod: RELEASE_ASSET_INSTALL_METHOD,
|
|
1608
|
+
sourceCloneUrl: null,
|
|
1609
|
+
sourceCommit: null,
|
|
1351
1610
|
};
|
|
1352
1611
|
}
|
|
1353
1612
|
|
|
@@ -1356,6 +1615,7 @@ export async function bootstrapInstalledPylon(
|
|
|
1356
1615
|
{
|
|
1357
1616
|
runProcessImpl = runProcess,
|
|
1358
1617
|
onStatus = null,
|
|
1618
|
+
telemetryClient = null,
|
|
1359
1619
|
} = {},
|
|
1360
1620
|
) {
|
|
1361
1621
|
const pylonPath = path.resolve(options.pylonPath);
|
|
@@ -1365,108 +1625,157 @@ export async function bootstrapInstalledPylon(
|
|
|
1365
1625
|
options.diagnosticRepeats ?? DEFAULT_DIAGNOSTIC_REPEATS;
|
|
1366
1626
|
const diagnosticMaxOutputTokens =
|
|
1367
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
|
+
});
|
|
1368
1642
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
["status", "--json"],
|
|
1377
|
-
options,
|
|
1378
|
-
runProcessImpl,
|
|
1379
|
-
);
|
|
1380
|
-
emitStatus(onStatus, "Scanning for local models");
|
|
1381
|
-
const inventory = await runPylonJson(
|
|
1382
|
-
pylonPath,
|
|
1383
|
-
["inventory", "--json"],
|
|
1384
|
-
options,
|
|
1385
|
-
runProcessImpl,
|
|
1386
|
-
);
|
|
1387
|
-
|
|
1388
|
-
let download = null;
|
|
1389
|
-
if (!options.skipModelDownload) {
|
|
1390
|
-
emitStatus(onStatus, "Downloading curated model bundle", model);
|
|
1391
|
-
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(
|
|
1392
1650
|
pylonPath,
|
|
1393
|
-
["
|
|
1651
|
+
["status", "--json"],
|
|
1394
1652
|
options,
|
|
1395
1653
|
runProcessImpl,
|
|
1396
1654
|
);
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
"
|
|
1401
|
-
|
|
1655
|
+
emitStatus(onStatus, "Scanning for local models");
|
|
1656
|
+
const inventory = await runPylonJson(
|
|
1657
|
+
pylonPath,
|
|
1658
|
+
["inventory", "--json"],
|
|
1659
|
+
options,
|
|
1660
|
+
runProcessImpl,
|
|
1402
1661
|
);
|
|
1403
|
-
}
|
|
1404
1662
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
diagnostic = await runPylonJson(
|
|
1663
|
+
let download = null;
|
|
1664
|
+
if (!options.skipModelDownload) {
|
|
1665
|
+
emitStatus(onStatus, "Downloading curated model bundle", model);
|
|
1666
|
+
download = await runPylonJson(
|
|
1410
1667
|
pylonPath,
|
|
1411
|
-
[
|
|
1412
|
-
"gemma",
|
|
1413
|
-
"diagnose",
|
|
1414
|
-
model,
|
|
1415
|
-
"--max-output-tokens",
|
|
1416
|
-
String(diagnosticMaxOutputTokens),
|
|
1417
|
-
"--repeats",
|
|
1418
|
-
String(diagnosticRepeats),
|
|
1419
|
-
"--json",
|
|
1420
|
-
],
|
|
1668
|
+
["gemma", "download", model, "--json"],
|
|
1421
1669
|
options,
|
|
1422
1670
|
runProcessImpl,
|
|
1423
1671
|
);
|
|
1424
|
-
}
|
|
1425
|
-
if (!isUnsupportedGemmaDiagnoseError(error)) {
|
|
1426
|
-
throw error;
|
|
1427
|
-
}
|
|
1672
|
+
} else {
|
|
1428
1673
|
emitStatus(
|
|
1429
1674
|
onStatus,
|
|
1430
|
-
"Skipping
|
|
1431
|
-
"
|
|
1675
|
+
"Skipping optional curated GGUF cache",
|
|
1676
|
+
"use --download-curated-cache to prefetch Hugging Face weights",
|
|
1432
1677
|
);
|
|
1433
1678
|
}
|
|
1434
|
-
} else {
|
|
1435
|
-
emitStatus(onStatus, "Skipping first-run diagnostic", model);
|
|
1436
|
-
}
|
|
1437
1679
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
+
}
|
|
1442
1712
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
? `diagnostic ${diagnosticResult.status}`
|
|
1448
|
-
: "smoke path complete",
|
|
1449
|
-
);
|
|
1713
|
+
const diagnosticResult =
|
|
1714
|
+
diagnostic?.results?.find((result) => result.model_id === model) ??
|
|
1715
|
+
diagnostic?.results?.[0] ??
|
|
1716
|
+
null;
|
|
1450
1717
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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
|
+
}
|
|
1470
1779
|
}
|
|
1471
1780
|
|
|
1472
1781
|
export async function launchInstalledPylonTui(
|
|
@@ -1572,6 +1881,7 @@ export function renderBootstrapSummary(summary) {
|
|
|
1572
1881
|
`Verdict detail: ${outcome.detail}`,
|
|
1573
1882
|
`Pylon release: ${summary.version} (${summary.target.os}-${summary.target.arch})`,
|
|
1574
1883
|
`Archive source: ${summary.tagName}`,
|
|
1884
|
+
`Install source: ${installSourceForTelemetry(summary.installMethod, summary.cached).replaceAll("_", " ")}`,
|
|
1575
1885
|
`Installed from cache: ${summary.cached ? "yes" : "no"}`,
|
|
1576
1886
|
`Pylon binary: ${summary.binaries.pylon}`,
|
|
1577
1887
|
`Pylon TUI: ${summary.binaries.pylonTui}`,
|
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
|
+
}
|