@openagentsinc/pylon 0.1.3 → 0.1.5
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 +11 -3
- package/package.json +1 -1
- package/src/cli.js +111 -45
- package/src/index.js +566 -122
- package/src/telemetry.js +160 -0
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";
|
|
@@ -18,6 +24,9 @@ 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";
|
|
20
26
|
const PREFERRED_RUNTIME_MODEL_NAME = "gemma4:e4b";
|
|
27
|
+
const LEGACY_SOURCE_BUILD_SIBLING_REPOSITORIES = {
|
|
28
|
+
"spark-sdk": "https://github.com/AtlantisPleb/spark-sdk.git",
|
|
29
|
+
};
|
|
21
30
|
|
|
22
31
|
function emitStatus(onStatus, message, detail = null) {
|
|
23
32
|
if (typeof onStatus === "function") {
|
|
@@ -31,10 +40,82 @@ function emitVerboseStatus(onStatus, verbose, message, detail = null) {
|
|
|
31
40
|
}
|
|
32
41
|
}
|
|
33
42
|
|
|
43
|
+
function emitTelemetry(telemetryClient, eventName, properties = {}) {
|
|
44
|
+
if (typeof telemetryClient?.emit === "function") {
|
|
45
|
+
void telemetryClient.emit(eventName, properties);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
function normalizeVersion(value) {
|
|
35
50
|
return value.replace(/^pylon-v/, "").replace(/^v/, "");
|
|
36
51
|
}
|
|
37
52
|
|
|
53
|
+
function parseComparableVersion(value) {
|
|
54
|
+
const normalized = normalizeVersion(value).trim();
|
|
55
|
+
const match = normalized.match(
|
|
56
|
+
/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+?)(\d+)?)?$/,
|
|
57
|
+
);
|
|
58
|
+
if (!match) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
normalized,
|
|
64
|
+
major: Number.parseInt(match[1], 10),
|
|
65
|
+
minor: Number.parseInt(match[2], 10),
|
|
66
|
+
patch: Number.parseInt(match[3], 10),
|
|
67
|
+
prereleaseLabel: match[4] ?? null,
|
|
68
|
+
prereleaseNumber:
|
|
69
|
+
match[5] != null ? Number.parseInt(match[5], 10) : null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function comparePylonReleaseTags(leftTagName, rightTagName) {
|
|
74
|
+
const left = parseComparableVersion(leftTagName);
|
|
75
|
+
const right = parseComparableVersion(rightTagName);
|
|
76
|
+
|
|
77
|
+
if (!left && !right) {
|
|
78
|
+
return String(leftTagName).localeCompare(String(rightTagName));
|
|
79
|
+
}
|
|
80
|
+
if (!left) {
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
83
|
+
if (!right) {
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
88
|
+
if (left[key] !== right[key]) {
|
|
89
|
+
return left[key] > right[key] ? 1 : -1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (left.prereleaseLabel == null && right.prereleaseLabel == null) {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
if (left.prereleaseLabel == null) {
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
if (right.prereleaseLabel == null) {
|
|
100
|
+
return -1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const labelComparison = left.prereleaseLabel.localeCompare(
|
|
104
|
+
right.prereleaseLabel,
|
|
105
|
+
);
|
|
106
|
+
if (labelComparison !== 0) {
|
|
107
|
+
return labelComparison;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const leftNumber = left.prereleaseNumber ?? 0;
|
|
111
|
+
const rightNumber = right.prereleaseNumber ?? 0;
|
|
112
|
+
if (leftNumber !== rightNumber) {
|
|
113
|
+
return leftNumber > rightNumber ? 1 : -1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return left.normalized.localeCompare(right.normalized);
|
|
117
|
+
}
|
|
118
|
+
|
|
38
119
|
function createBootstrapError(message, context = {}) {
|
|
39
120
|
const error = new Error(message);
|
|
40
121
|
Object.assign(error, context);
|
|
@@ -50,6 +131,27 @@ async function pathExists(value) {
|
|
|
50
131
|
}
|
|
51
132
|
}
|
|
52
133
|
|
|
134
|
+
async function findFilesNamed(rootDir, targetName, results = []) {
|
|
135
|
+
const entries = await fs.readdir(rootDir, { withFileTypes: true });
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
if (entry.name === ".git" || entry.name === "node_modules" || entry.name === "target") {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
await findFilesNamed(fullPath, targetName, results);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (entry.isFile() && entry.name === targetName) {
|
|
148
|
+
results.push(fullPath);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
154
|
+
|
|
53
155
|
function defaultInstallRoot() {
|
|
54
156
|
return path.join(os.homedir(), ".openagents", "pylon", "bootstrap");
|
|
55
157
|
}
|
|
@@ -497,14 +599,31 @@ export function isPylonReleaseTag(tagName) {
|
|
|
497
599
|
);
|
|
498
600
|
}
|
|
499
601
|
|
|
500
|
-
|
|
602
|
+
function releaseHasTargetAssets(release, target) {
|
|
603
|
+
if (!target || !release?.tag_name) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const { archiveName, checksumName } = buildAssetNames(release.tag_name, target);
|
|
608
|
+
const assetNames = new Set(
|
|
609
|
+
(Array.isArray(release.assets) ? release.assets : [])
|
|
610
|
+
.map((asset) => asset?.name)
|
|
611
|
+
.filter(Boolean),
|
|
612
|
+
);
|
|
613
|
+
return assetNames.has(archiveName) && assetNames.has(checksumName);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function selectLatestPylonRelease(releases, target = null) {
|
|
501
617
|
if (!Array.isArray(releases)) {
|
|
502
618
|
throw new Error("GitHub release lookup did not return a release list.");
|
|
503
619
|
}
|
|
504
620
|
|
|
505
|
-
const
|
|
506
|
-
(candidate) => !candidate?.draft && isPylonReleaseTag(candidate?.tag_name)
|
|
507
|
-
|
|
621
|
+
const candidates = releases
|
|
622
|
+
.filter((candidate) => !candidate?.draft && isPylonReleaseTag(candidate?.tag_name))
|
|
623
|
+
.sort((left, right) => comparePylonReleaseTags(right.tag_name, left.tag_name));
|
|
624
|
+
const release =
|
|
625
|
+
candidates.find((candidate) => releaseHasTargetAssets(candidate, target)) ??
|
|
626
|
+
candidates[0];
|
|
508
627
|
if (!release) {
|
|
509
628
|
throw new Error(
|
|
510
629
|
`GitHub release lookup did not find any published ${PYLON_RELEASE_TAG_PREFIX} releases.`,
|
|
@@ -522,6 +641,7 @@ export async function fetchReleaseMetadata({
|
|
|
522
641
|
apiBase = DEFAULT_RELEASE_API_BASE,
|
|
523
642
|
repo = DEFAULT_RELEASE_REPO,
|
|
524
643
|
version = null,
|
|
644
|
+
target = null,
|
|
525
645
|
} = {}) {
|
|
526
646
|
const normalizedVersion = normalizeRequestedVersion(version);
|
|
527
647
|
const endpoint = normalizedVersion
|
|
@@ -538,7 +658,7 @@ export async function fetchReleaseMetadata({
|
|
|
538
658
|
? "GitHub tagged release lookup"
|
|
539
659
|
: "GitHub release list lookup",
|
|
540
660
|
});
|
|
541
|
-
return normalizedVersion ? payload : selectLatestPylonRelease(payload);
|
|
661
|
+
return normalizedVersion ? payload : selectLatestPylonRelease(payload, target);
|
|
542
662
|
}
|
|
543
663
|
|
|
544
664
|
export function selectReleaseAssets(release, target) {
|
|
@@ -704,6 +824,105 @@ function manualSourceBuildCommands(tagName, cloneUrl) {
|
|
|
704
824
|
].join("\n");
|
|
705
825
|
}
|
|
706
826
|
|
|
827
|
+
function inferLegacySiblingRepositoryName(relativeDependencyPath) {
|
|
828
|
+
if (typeof relativeDependencyPath !== "string" || !relativeDependencyPath.startsWith("..")) {
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const segments = relativeDependencyPath
|
|
833
|
+
.split(/[\\/]+/)
|
|
834
|
+
.filter(Boolean);
|
|
835
|
+
const firstRepoSegment = segments.find((segment) => segment !== "..");
|
|
836
|
+
if (!firstRepoSegment) {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return Object.hasOwn(LEGACY_SOURCE_BUILD_SIBLING_REPOSITORIES, firstRepoSegment)
|
|
841
|
+
? firstRepoSegment
|
|
842
|
+
: null;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function discoverLegacySiblingRepositories(repoDir) {
|
|
846
|
+
const cargoTomlFiles = await findFilesNamed(repoDir, "Cargo.toml");
|
|
847
|
+
const discovered = new Set();
|
|
848
|
+
|
|
849
|
+
for (const cargoTomlPath of cargoTomlFiles) {
|
|
850
|
+
const payload = await fs.readFile(cargoTomlPath, "utf8");
|
|
851
|
+
const matches = payload.matchAll(/path\s*=\s*"([^"]+)"/g);
|
|
852
|
+
for (const match of matches) {
|
|
853
|
+
const dependencyPath = match[1]?.trim();
|
|
854
|
+
const repoName = inferLegacySiblingRepositoryName(dependencyPath);
|
|
855
|
+
if (!repoName) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const resolvedPath = path.resolve(path.dirname(cargoTomlPath), dependencyPath);
|
|
860
|
+
if (resolvedPath.startsWith(`${repoDir}${path.sep}`) || resolvedPath === repoDir) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
discovered.add(repoName);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return [...discovered];
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function hydrateLegacySiblingRepositories({
|
|
872
|
+
repoDir,
|
|
873
|
+
buildEnv,
|
|
874
|
+
runProcessImpl,
|
|
875
|
+
onStatus,
|
|
876
|
+
telemetryClient,
|
|
877
|
+
releaseTag,
|
|
878
|
+
target,
|
|
879
|
+
}) {
|
|
880
|
+
const repoNames = await discoverLegacySiblingRepositories(repoDir);
|
|
881
|
+
const workspaceRoot = path.dirname(repoDir);
|
|
882
|
+
|
|
883
|
+
for (const repoName of repoNames) {
|
|
884
|
+
const cloneUrl = LEGACY_SOURCE_BUILD_SIBLING_REPOSITORIES[repoName];
|
|
885
|
+
const checkoutDir = path.join(workspaceRoot, repoName);
|
|
886
|
+
if (!cloneUrl || (await pathExists(checkoutDir))) {
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
emitStatus(onStatus, "Hydrating legacy sibling checkout", repoName);
|
|
891
|
+
emitTelemetry(telemetryClient, "installer_legacy_sibling_checkout_started", {
|
|
892
|
+
release_tag: releaseTag,
|
|
893
|
+
os: target.os,
|
|
894
|
+
arch: target.arch,
|
|
895
|
+
repository: repoName,
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
try {
|
|
899
|
+
await runProcessImpl(
|
|
900
|
+
"git",
|
|
901
|
+
["clone", "--depth", "1", cloneUrl, checkoutDir],
|
|
902
|
+
{
|
|
903
|
+
cwd: workspaceRoot,
|
|
904
|
+
env: buildEnv,
|
|
905
|
+
},
|
|
906
|
+
);
|
|
907
|
+
emitTelemetry(telemetryClient, "installer_legacy_sibling_checkout_completed", {
|
|
908
|
+
release_tag: releaseTag,
|
|
909
|
+
os: target.os,
|
|
910
|
+
arch: target.arch,
|
|
911
|
+
repository: repoName,
|
|
912
|
+
});
|
|
913
|
+
} catch (error) {
|
|
914
|
+
emitTelemetry(telemetryClient, "installer_legacy_sibling_checkout_failed", {
|
|
915
|
+
release_tag: releaseTag,
|
|
916
|
+
os: target.os,
|
|
917
|
+
arch: target.arch,
|
|
918
|
+
repository: repoName,
|
|
919
|
+
...telemetryFailureContext(error, "legacy_sibling_checkout"),
|
|
920
|
+
});
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
707
926
|
function rustInstallCommand() {
|
|
708
927
|
return "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
|
|
709
928
|
}
|
|
@@ -713,6 +932,7 @@ async function ensureRustToolchain({
|
|
|
713
932
|
fetchImpl,
|
|
714
933
|
runProcessImpl,
|
|
715
934
|
onStatus,
|
|
935
|
+
telemetryClient,
|
|
716
936
|
promptImpl = promptForApproval,
|
|
717
937
|
commandExistsImpl = commandExists,
|
|
718
938
|
env = process.env,
|
|
@@ -725,44 +945,70 @@ async function ensureRustToolchain({
|
|
|
725
945
|
return toolchainEnv;
|
|
726
946
|
}
|
|
727
947
|
|
|
948
|
+
emitTelemetry(telemetryClient, "installer_rust_missing", {
|
|
949
|
+
os: target.os,
|
|
950
|
+
arch: target.arch,
|
|
951
|
+
});
|
|
952
|
+
|
|
728
953
|
emitStatus(
|
|
729
954
|
onStatus,
|
|
730
955
|
"Rust toolchain required for source build",
|
|
731
956
|
`${target.os}-${target.arch}`,
|
|
732
957
|
);
|
|
733
958
|
|
|
959
|
+
emitTelemetry(telemetryClient, "installer_rust_install_prompt_shown", {
|
|
960
|
+
os: target.os,
|
|
961
|
+
arch: target.arch,
|
|
962
|
+
});
|
|
734
963
|
const approved = await promptImpl(
|
|
735
964
|
`Rust is required to build Pylon from source for ${target.os}-${target.arch}. Install the official Rust toolchain now via rustup?`,
|
|
736
965
|
);
|
|
737
966
|
if (!approved) {
|
|
967
|
+
emitTelemetry(telemetryClient, "installer_rust_install_declined", {
|
|
968
|
+
os: target.os,
|
|
969
|
+
arch: target.arch,
|
|
970
|
+
});
|
|
738
971
|
throw new Error(
|
|
739
972
|
`Rust is required to build Pylon from source.\nInstall it manually and rerun:\n${rustInstallCommand()}`,
|
|
740
973
|
);
|
|
741
974
|
}
|
|
742
975
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
accept: "text/plain",
|
|
747
|
-
"user-agent": "@openagentsinc/pylon bootstrap",
|
|
748
|
-
},
|
|
749
|
-
runProcessImpl,
|
|
750
|
-
onStatus,
|
|
751
|
-
stage: "Rust toolchain installer download",
|
|
976
|
+
emitTelemetry(telemetryClient, "installer_rust_install_approved", {
|
|
977
|
+
os: target.os,
|
|
978
|
+
arch: target.arch,
|
|
752
979
|
});
|
|
753
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pylon-rustup-"));
|
|
754
|
-
const scriptPath = path.join(tempDir, "rustup-init.sh");
|
|
755
980
|
|
|
981
|
+
emitStatus(onStatus, "Installing Rust toolchain", "official rustup installer");
|
|
756
982
|
try {
|
|
983
|
+
const scriptPayload = await fetchText(fetchImpl, rustupInitUrl, {
|
|
984
|
+
headers: {
|
|
985
|
+
accept: "text/plain",
|
|
986
|
+
"user-agent": "@openagentsinc/pylon bootstrap",
|
|
987
|
+
},
|
|
988
|
+
runProcessImpl,
|
|
989
|
+
onStatus,
|
|
990
|
+
stage: "Rust toolchain installer download",
|
|
991
|
+
});
|
|
992
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pylon-rustup-"));
|
|
993
|
+
const scriptPath = path.join(tempDir, "rustup-init.sh");
|
|
757
994
|
await fs.writeFile(scriptPath, scriptPayload);
|
|
758
995
|
await fs.chmod(scriptPath, 0o755);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
996
|
+
try {
|
|
997
|
+
await runProcessImpl("sh", [scriptPath, "-y"], {
|
|
998
|
+
cwd: tempDir,
|
|
999
|
+
env: toolchainEnv,
|
|
1000
|
+
stdio: "inherit",
|
|
1001
|
+
});
|
|
1002
|
+
} finally {
|
|
1003
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
1004
|
+
}
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
emitTelemetry(telemetryClient, "installer_rust_install_failed", {
|
|
1007
|
+
os: target.os,
|
|
1008
|
+
arch: target.arch,
|
|
1009
|
+
...telemetryFailureContext(error, "rust_install"),
|
|
763
1010
|
});
|
|
764
|
-
|
|
765
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
1011
|
+
throw error;
|
|
766
1012
|
}
|
|
767
1013
|
|
|
768
1014
|
toolchainEnv = withPrependedPath(env, path.join(os.homedir(), ".cargo", "bin"));
|
|
@@ -779,6 +1025,10 @@ async function ensureRustToolchain({
|
|
|
779
1025
|
"Rust toolchain installed",
|
|
780
1026
|
path.join(os.homedir(), ".cargo", "bin"),
|
|
781
1027
|
);
|
|
1028
|
+
emitTelemetry(telemetryClient, "installer_rust_install_completed", {
|
|
1029
|
+
os: target.os,
|
|
1030
|
+
arch: target.arch,
|
|
1031
|
+
});
|
|
782
1032
|
return toolchainEnv;
|
|
783
1033
|
}
|
|
784
1034
|
|
|
@@ -793,6 +1043,7 @@ async function installSourceBuild(
|
|
|
793
1043
|
fetchImpl,
|
|
794
1044
|
runProcessImpl,
|
|
795
1045
|
onStatus,
|
|
1046
|
+
telemetryClient,
|
|
796
1047
|
promptImpl = promptForApproval,
|
|
797
1048
|
commandExistsImpl = commandExists,
|
|
798
1049
|
},
|
|
@@ -812,6 +1063,13 @@ async function installSourceBuild(
|
|
|
812
1063
|
"Prebuilt asset missing; falling back to source build",
|
|
813
1064
|
`${selected.tagName} for ${target.os}-${target.arch}`,
|
|
814
1065
|
);
|
|
1066
|
+
emitTelemetry(telemetryClient, "installer_prebuilt_asset_missing", {
|
|
1067
|
+
release_tag: selected.tagName,
|
|
1068
|
+
release_commit: selected.targetCommitish ?? null,
|
|
1069
|
+
os: target.os,
|
|
1070
|
+
arch: target.arch,
|
|
1071
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1072
|
+
});
|
|
815
1073
|
|
|
816
1074
|
if (!(await commandExistsImpl("git", process.env))) {
|
|
817
1075
|
throw new Error(
|
|
@@ -824,6 +1082,7 @@ async function installSourceBuild(
|
|
|
824
1082
|
fetchImpl,
|
|
825
1083
|
runProcessImpl,
|
|
826
1084
|
onStatus,
|
|
1085
|
+
telemetryClient,
|
|
827
1086
|
promptImpl,
|
|
828
1087
|
commandExistsImpl,
|
|
829
1088
|
});
|
|
@@ -872,6 +1131,15 @@ async function installSourceBuild(
|
|
|
872
1131
|
env: buildEnv,
|
|
873
1132
|
},
|
|
874
1133
|
);
|
|
1134
|
+
await hydrateLegacySiblingRepositories({
|
|
1135
|
+
repoDir,
|
|
1136
|
+
buildEnv,
|
|
1137
|
+
runProcessImpl,
|
|
1138
|
+
onStatus,
|
|
1139
|
+
telemetryClient,
|
|
1140
|
+
releaseTag: selected.tagName,
|
|
1141
|
+
target,
|
|
1142
|
+
});
|
|
875
1143
|
|
|
876
1144
|
const { stdout: commitStdout } = await runProcessImpl(
|
|
877
1145
|
"git",
|
|
@@ -897,6 +1165,13 @@ async function installSourceBuild(
|
|
|
897
1165
|
"Building Pylon from source",
|
|
898
1166
|
`${selected.tagName} (${sourceCommit.slice(0, 12)})`,
|
|
899
1167
|
);
|
|
1168
|
+
emitTelemetry(telemetryClient, "installer_source_build_started", {
|
|
1169
|
+
release_tag: selected.tagName,
|
|
1170
|
+
release_commit: sourceCommit,
|
|
1171
|
+
os: target.os,
|
|
1172
|
+
arch: target.arch,
|
|
1173
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1174
|
+
});
|
|
900
1175
|
await runProcessImpl(buildCommand[0], buildCommand.slice(1), {
|
|
901
1176
|
cwd: repoDir,
|
|
902
1177
|
env: buildEnv,
|
|
@@ -938,6 +1213,17 @@ async function installSourceBuild(
|
|
|
938
1213
|
"Installed source-built binaries",
|
|
939
1214
|
`${selected.tagName} for ${target.os}-${target.arch}`,
|
|
940
1215
|
);
|
|
1216
|
+
emitTelemetry(telemetryClient, "installer_source_build_completed", {
|
|
1217
|
+
release_tag: selected.tagName,
|
|
1218
|
+
release_commit: sourceCommit,
|
|
1219
|
+
os: target.os,
|
|
1220
|
+
arch: target.arch,
|
|
1221
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1222
|
+
install_source: installSourceForTelemetry(
|
|
1223
|
+
SOURCE_BUILD_INSTALL_METHOD,
|
|
1224
|
+
false,
|
|
1225
|
+
),
|
|
1226
|
+
});
|
|
941
1227
|
|
|
942
1228
|
return {
|
|
943
1229
|
...selected,
|
|
@@ -950,6 +1236,14 @@ async function installSourceBuild(
|
|
|
950
1236
|
sourceCommit,
|
|
951
1237
|
};
|
|
952
1238
|
} catch (error) {
|
|
1239
|
+
emitTelemetry(telemetryClient, "installer_source_build_failed", {
|
|
1240
|
+
release_tag: selected.tagName,
|
|
1241
|
+
release_commit: selected.targetCommitish ?? null,
|
|
1242
|
+
os: target.os,
|
|
1243
|
+
arch: target.arch,
|
|
1244
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1245
|
+
...telemetryFailureContext(error, "source_build"),
|
|
1246
|
+
});
|
|
953
1247
|
const message = error instanceof Error ? error.message : String(error);
|
|
954
1248
|
throw new Error(
|
|
955
1249
|
`${message}\nManual source-build fallback:\n${manualBuildInstructions}`,
|
|
@@ -1013,6 +1307,9 @@ async function findLatestCachedInstall(installRoot, target) {
|
|
|
1013
1307
|
pylonTuiPath,
|
|
1014
1308
|
expectedSha256: manifest.sha256 ?? null,
|
|
1015
1309
|
cached: true,
|
|
1310
|
+
installMethod: manifest.installMethod ?? RELEASE_ASSET_INSTALL_METHOD,
|
|
1311
|
+
sourceCloneUrl: manifest.sourceCloneUrl ?? null,
|
|
1312
|
+
sourceCommit: manifest.sourceCommit ?? null,
|
|
1016
1313
|
mtimeMs: manifestStat.mtimeMs,
|
|
1017
1314
|
});
|
|
1018
1315
|
} catch {
|
|
@@ -1124,6 +1421,7 @@ export async function ensureReleaseInstall(
|
|
|
1124
1421
|
fetchImpl = globalThis.fetch,
|
|
1125
1422
|
runProcessImpl = runProcess,
|
|
1126
1423
|
onStatus = null,
|
|
1424
|
+
telemetryClient = null,
|
|
1127
1425
|
promptImpl = promptForApproval,
|
|
1128
1426
|
commandExistsImpl = commandExists,
|
|
1129
1427
|
} = {},
|
|
@@ -1141,25 +1439,38 @@ export async function ensureReleaseInstall(
|
|
|
1141
1439
|
const installRoot = options.installRoot ?? defaultInstallRoot();
|
|
1142
1440
|
if (options.version) {
|
|
1143
1441
|
const requestedPaths = buildInstallPaths(installRoot, options.version, target);
|
|
1442
|
+
const requestedManifest = await readInstallManifest(requestedPaths.manifestPath);
|
|
1144
1443
|
const requestedCached =
|
|
1145
1444
|
(await pathExists(requestedPaths.pylonPath)) &&
|
|
1146
1445
|
(await pathExists(requestedPaths.pylonTuiPath));
|
|
1147
1446
|
if (requestedCached) {
|
|
1447
|
+
const installMethod =
|
|
1448
|
+
requestedManifest?.installMethod ?? RELEASE_ASSET_INSTALL_METHOD;
|
|
1148
1449
|
emitStatus(
|
|
1149
1450
|
onStatus,
|
|
1150
|
-
|
|
1451
|
+
installMethod === SOURCE_BUILD_INSTALL_METHOD
|
|
1452
|
+
? "Using cached source-built binaries"
|
|
1453
|
+
: "Using cached standalone binaries",
|
|
1151
1454
|
`pylon-v${normalizeVersion(options.version)} for ${target.os}-${target.arch}`,
|
|
1152
1455
|
);
|
|
1456
|
+
emitTelemetry(telemetryClient, "installer_cached_install_reused", {
|
|
1457
|
+
release_tag: `pylon-v${normalizeVersion(options.version)}`,
|
|
1458
|
+
release_commit: requestedManifest?.sourceCommit ?? null,
|
|
1459
|
+
os: target.os,
|
|
1460
|
+
arch: target.arch,
|
|
1461
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1462
|
+
install_source: installSourceForTelemetry(installMethod, true),
|
|
1463
|
+
});
|
|
1153
1464
|
return {
|
|
1154
1465
|
version: normalizeVersion(options.version),
|
|
1155
1466
|
tagName: `pylon-v${normalizeVersion(options.version)}`,
|
|
1156
1467
|
target,
|
|
1157
1468
|
...requestedPaths,
|
|
1158
|
-
expectedSha256:
|
|
1159
|
-
.readFile(requestedPaths.manifestPath, "utf8")
|
|
1160
|
-
.then((payload) => JSON.parse(payload).sha256)
|
|
1161
|
-
.catch(() => null),
|
|
1469
|
+
expectedSha256: requestedManifest?.sha256 ?? null,
|
|
1162
1470
|
cached: true,
|
|
1471
|
+
installMethod,
|
|
1472
|
+
sourceCloneUrl: requestedManifest?.sourceCloneUrl ?? null,
|
|
1473
|
+
sourceCommit: requestedManifest?.sourceCommit ?? null,
|
|
1163
1474
|
};
|
|
1164
1475
|
}
|
|
1165
1476
|
}
|
|
@@ -1174,6 +1485,14 @@ export async function ensureReleaseInstall(
|
|
|
1174
1485
|
apiBase: options.apiBase ?? DEFAULT_RELEASE_API_BASE,
|
|
1175
1486
|
repo: options.repo ?? DEFAULT_RELEASE_REPO,
|
|
1176
1487
|
version: options.version ?? null,
|
|
1488
|
+
target,
|
|
1489
|
+
});
|
|
1490
|
+
emitTelemetry(telemetryClient, "installer_release_resolved", {
|
|
1491
|
+
release_tag: release?.tag_name ?? null,
|
|
1492
|
+
release_commit: release?.target_commitish ?? null,
|
|
1493
|
+
os: target.os,
|
|
1494
|
+
arch: target.arch,
|
|
1495
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1177
1496
|
});
|
|
1178
1497
|
} catch (error) {
|
|
1179
1498
|
const cached = !options.version
|
|
@@ -1182,9 +1501,19 @@ export async function ensureReleaseInstall(
|
|
|
1182
1501
|
if (cached) {
|
|
1183
1502
|
emitStatus(
|
|
1184
1503
|
onStatus,
|
|
1185
|
-
|
|
1504
|
+
cached.installMethod === SOURCE_BUILD_INSTALL_METHOD
|
|
1505
|
+
? "Using cached source-built binaries"
|
|
1506
|
+
: "Using cached standalone binaries",
|
|
1186
1507
|
`release lookup failed; falling back to ${cached.tagName}`,
|
|
1187
1508
|
);
|
|
1509
|
+
emitTelemetry(telemetryClient, "installer_cached_install_reused", {
|
|
1510
|
+
release_tag: cached.tagName,
|
|
1511
|
+
release_commit: cached.sourceCommit ?? null,
|
|
1512
|
+
os: target.os,
|
|
1513
|
+
arch: target.arch,
|
|
1514
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1515
|
+
install_source: installSourceForTelemetry(cached.installMethod, true),
|
|
1516
|
+
});
|
|
1188
1517
|
return cached;
|
|
1189
1518
|
}
|
|
1190
1519
|
|
|
@@ -1205,6 +1534,14 @@ export async function ensureReleaseInstall(
|
|
|
1205
1534
|
let missingAssetsError = null;
|
|
1206
1535
|
try {
|
|
1207
1536
|
selected = selectReleaseAssets(release, target);
|
|
1537
|
+
emitTelemetry(telemetryClient, "installer_prebuilt_asset_found", {
|
|
1538
|
+
release_tag: selected.tagName,
|
|
1539
|
+
release_commit: release?.target_commitish ?? null,
|
|
1540
|
+
asset_name: selected.archiveAsset.name,
|
|
1541
|
+
os: target.os,
|
|
1542
|
+
arch: target.arch,
|
|
1543
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1544
|
+
});
|
|
1208
1545
|
} catch (error) {
|
|
1209
1546
|
if (!(error instanceof MissingReleaseAssetsError)) {
|
|
1210
1547
|
throw error;
|
|
@@ -1235,6 +1572,14 @@ export async function ensureReleaseInstall(
|
|
|
1235
1572
|
: "Using cached standalone binaries",
|
|
1236
1573
|
`${selected.tagName} for ${target.os}-${target.arch}`,
|
|
1237
1574
|
);
|
|
1575
|
+
emitTelemetry(telemetryClient, "installer_cached_install_reused", {
|
|
1576
|
+
release_tag: selected.tagName,
|
|
1577
|
+
release_commit: manifest?.sourceCommit ?? release?.target_commitish ?? null,
|
|
1578
|
+
os: target.os,
|
|
1579
|
+
arch: target.arch,
|
|
1580
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1581
|
+
install_source: installSourceForTelemetry(installMethod, true),
|
|
1582
|
+
});
|
|
1238
1583
|
return {
|
|
1239
1584
|
...selected,
|
|
1240
1585
|
...paths,
|
|
@@ -1259,6 +1604,7 @@ export async function ensureReleaseInstall(
|
|
|
1259
1604
|
fetchImpl,
|
|
1260
1605
|
runProcessImpl,
|
|
1261
1606
|
onStatus,
|
|
1607
|
+
telemetryClient,
|
|
1262
1608
|
promptImpl,
|
|
1263
1609
|
commandExistsImpl,
|
|
1264
1610
|
},
|
|
@@ -1293,20 +1639,62 @@ export async function ensureReleaseInstall(
|
|
|
1293
1639
|
"Downloading standalone binaries",
|
|
1294
1640
|
selected.archiveAsset.name,
|
|
1295
1641
|
);
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1642
|
+
emitTelemetry(telemetryClient, "installer_prebuilt_download_started", {
|
|
1643
|
+
release_tag: selected.tagName,
|
|
1644
|
+
asset_name: selected.archiveAsset.name,
|
|
1645
|
+
os: target.os,
|
|
1646
|
+
arch: target.arch,
|
|
1647
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1301
1648
|
});
|
|
1649
|
+
try {
|
|
1650
|
+
await downloadFile(fetchImpl, selected.archiveAsset.url, paths.archivePath, {
|
|
1651
|
+
runProcessImpl,
|
|
1652
|
+
onStatus,
|
|
1653
|
+
verbose: Boolean(options.verbose),
|
|
1654
|
+
stage: "Release archive download",
|
|
1655
|
+
});
|
|
1656
|
+
emitTelemetry(telemetryClient, "installer_prebuilt_download_completed", {
|
|
1657
|
+
release_tag: selected.tagName,
|
|
1658
|
+
asset_name: selected.archiveAsset.name,
|
|
1659
|
+
os: target.os,
|
|
1660
|
+
arch: target.arch,
|
|
1661
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1662
|
+
});
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
emitTelemetry(telemetryClient, "installer_prebuilt_download_failed", {
|
|
1665
|
+
release_tag: selected.tagName,
|
|
1666
|
+
asset_name: selected.archiveAsset.name,
|
|
1667
|
+
os: target.os,
|
|
1668
|
+
arch: target.arch,
|
|
1669
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1670
|
+
...telemetryFailureContext(error, "prebuilt_download"),
|
|
1671
|
+
});
|
|
1672
|
+
throw error;
|
|
1673
|
+
}
|
|
1302
1674
|
}
|
|
1303
1675
|
|
|
1304
1676
|
const actualSha256 = await sha256File(paths.archivePath);
|
|
1305
1677
|
if (actualSha256 !== expectedSha256) {
|
|
1678
|
+
emitTelemetry(telemetryClient, "installer_checksum_failed", {
|
|
1679
|
+
release_tag: selected.tagName,
|
|
1680
|
+
asset_name: selected.archiveAsset.name,
|
|
1681
|
+
os: target.os,
|
|
1682
|
+
arch: target.arch,
|
|
1683
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1684
|
+
error_stage: "checksum_verify",
|
|
1685
|
+
error_code: "sha256_mismatch",
|
|
1686
|
+
});
|
|
1306
1687
|
throw new Error(
|
|
1307
1688
|
`SHA-256 verification failed for ${selected.archiveAsset.name}: expected ${expectedSha256}, got ${actualSha256}.`,
|
|
1308
1689
|
);
|
|
1309
1690
|
}
|
|
1691
|
+
emitTelemetry(telemetryClient, "installer_checksum_verified", {
|
|
1692
|
+
release_tag: selected.tagName,
|
|
1693
|
+
asset_name: selected.archiveAsset.name,
|
|
1694
|
+
os: target.os,
|
|
1695
|
+
arch: target.arch,
|
|
1696
|
+
platform_key: `${target.os}-${target.arch}`,
|
|
1697
|
+
});
|
|
1310
1698
|
|
|
1311
1699
|
emitStatus(
|
|
1312
1700
|
onStatus,
|
|
@@ -1348,6 +1736,9 @@ export async function ensureReleaseInstall(
|
|
|
1348
1736
|
target,
|
|
1349
1737
|
expectedSha256,
|
|
1350
1738
|
cached: false,
|
|
1739
|
+
installMethod: RELEASE_ASSET_INSTALL_METHOD,
|
|
1740
|
+
sourceCloneUrl: null,
|
|
1741
|
+
sourceCommit: null,
|
|
1351
1742
|
};
|
|
1352
1743
|
}
|
|
1353
1744
|
|
|
@@ -1356,6 +1747,7 @@ export async function bootstrapInstalledPylon(
|
|
|
1356
1747
|
{
|
|
1357
1748
|
runProcessImpl = runProcess,
|
|
1358
1749
|
onStatus = null,
|
|
1750
|
+
telemetryClient = null,
|
|
1359
1751
|
} = {},
|
|
1360
1752
|
) {
|
|
1361
1753
|
const pylonPath = path.resolve(options.pylonPath);
|
|
@@ -1365,125 +1757,176 @@ export async function bootstrapInstalledPylon(
|
|
|
1365
1757
|
options.diagnosticRepeats ?? DEFAULT_DIAGNOSTIC_REPEATS;
|
|
1366
1758
|
const diagnosticMaxOutputTokens =
|
|
1367
1759
|
options.diagnosticMaxOutputTokens ?? DEFAULT_DIAGNOSTIC_MAX_OUTPUT_TOKENS;
|
|
1760
|
+
emitTelemetry(telemetryClient, "installer_smoke_test_started", {
|
|
1761
|
+
release_tag: options.tagName ?? `pylon-v${options.version}`,
|
|
1762
|
+
release_commit: options.sourceCommit ?? null,
|
|
1763
|
+
os: options.target?.os ?? null,
|
|
1764
|
+
arch: options.target?.arch ?? null,
|
|
1765
|
+
platform_key:
|
|
1766
|
+
options.target?.os && options.target?.arch
|
|
1767
|
+
? `${options.target.os}-${options.target.arch}`
|
|
1768
|
+
: null,
|
|
1769
|
+
install_source: installSourceForTelemetry(
|
|
1770
|
+
options.installMethod,
|
|
1771
|
+
Boolean(options.cached),
|
|
1772
|
+
),
|
|
1773
|
+
});
|
|
1368
1774
|
|
|
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(
|
|
1775
|
+
try {
|
|
1776
|
+
emitStatus(onStatus, "Verifying Pylon binary", path.basename(pylonPath));
|
|
1777
|
+
await runPylonCommand(pylonPath, ["--help"], options, runProcessImpl);
|
|
1778
|
+
emitStatus(onStatus, "Bootstrapping local Pylon identity");
|
|
1779
|
+
const init = await runPylonJson(pylonPath, ["init"], options, runProcessImpl);
|
|
1780
|
+
emitStatus(onStatus, "Checking runtime health");
|
|
1781
|
+
const status = await runPylonJson(
|
|
1392
1782
|
pylonPath,
|
|
1393
|
-
["
|
|
1783
|
+
["status", "--json"],
|
|
1394
1784
|
options,
|
|
1395
1785
|
runProcessImpl,
|
|
1396
1786
|
);
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
"
|
|
1401
|
-
|
|
1787
|
+
emitStatus(onStatus, "Scanning for local models");
|
|
1788
|
+
const inventory = await runPylonJson(
|
|
1789
|
+
pylonPath,
|
|
1790
|
+
["inventory", "--json"],
|
|
1791
|
+
options,
|
|
1792
|
+
runProcessImpl,
|
|
1402
1793
|
);
|
|
1403
|
-
}
|
|
1404
1794
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
diagnostic = await runPylonJson(
|
|
1795
|
+
let download = null;
|
|
1796
|
+
if (!options.skipModelDownload) {
|
|
1797
|
+
emitStatus(onStatus, "Downloading curated model bundle", model);
|
|
1798
|
+
download = await runPylonJson(
|
|
1410
1799
|
pylonPath,
|
|
1411
|
-
[
|
|
1412
|
-
"gemma",
|
|
1413
|
-
"diagnose",
|
|
1414
|
-
model,
|
|
1415
|
-
"--max-output-tokens",
|
|
1416
|
-
String(diagnosticMaxOutputTokens),
|
|
1417
|
-
"--repeats",
|
|
1418
|
-
String(diagnosticRepeats),
|
|
1419
|
-
"--json",
|
|
1420
|
-
],
|
|
1800
|
+
["gemma", "download", model, "--json"],
|
|
1421
1801
|
options,
|
|
1422
1802
|
runProcessImpl,
|
|
1423
1803
|
);
|
|
1424
|
-
}
|
|
1425
|
-
if (!isUnsupportedGemmaDiagnoseError(error)) {
|
|
1426
|
-
throw error;
|
|
1427
|
-
}
|
|
1804
|
+
} else {
|
|
1428
1805
|
emitStatus(
|
|
1429
1806
|
onStatus,
|
|
1430
|
-
"Skipping
|
|
1431
|
-
"
|
|
1807
|
+
"Skipping optional curated GGUF cache",
|
|
1808
|
+
"use --download-curated-cache to prefetch Hugging Face weights",
|
|
1432
1809
|
);
|
|
1433
1810
|
}
|
|
1434
|
-
} else {
|
|
1435
|
-
emitStatus(onStatus, "Skipping first-run diagnostic", model);
|
|
1436
|
-
}
|
|
1437
1811
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1812
|
+
let diagnostic = null;
|
|
1813
|
+
if (!options.skipDiagnostics) {
|
|
1814
|
+
emitStatus(onStatus, "Running first-run diagnostic", model);
|
|
1815
|
+
try {
|
|
1816
|
+
diagnostic = await runPylonJson(
|
|
1817
|
+
pylonPath,
|
|
1818
|
+
[
|
|
1819
|
+
"gemma",
|
|
1820
|
+
"diagnose",
|
|
1821
|
+
model,
|
|
1822
|
+
"--max-output-tokens",
|
|
1823
|
+
String(diagnosticMaxOutputTokens),
|
|
1824
|
+
"--repeats",
|
|
1825
|
+
String(diagnosticRepeats),
|
|
1826
|
+
"--json",
|
|
1827
|
+
],
|
|
1828
|
+
options,
|
|
1829
|
+
runProcessImpl,
|
|
1830
|
+
);
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
if (!isUnsupportedGemmaDiagnoseError(error)) {
|
|
1833
|
+
throw error;
|
|
1834
|
+
}
|
|
1835
|
+
emitStatus(
|
|
1836
|
+
onStatus,
|
|
1837
|
+
"Skipping first-run diagnostic",
|
|
1838
|
+
"installed Pylon release does not expose gemma diagnose",
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
} else {
|
|
1842
|
+
emitStatus(onStatus, "Skipping first-run diagnostic", model);
|
|
1843
|
+
}
|
|
1442
1844
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
? `diagnostic ${diagnosticResult.status}`
|
|
1448
|
-
: "smoke path complete",
|
|
1449
|
-
);
|
|
1845
|
+
const diagnosticResult =
|
|
1846
|
+
diagnostic?.results?.find((result) => result.model_id === model) ??
|
|
1847
|
+
diagnostic?.results?.[0] ??
|
|
1848
|
+
null;
|
|
1450
1849
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1850
|
+
emitStatus(
|
|
1851
|
+
onStatus,
|
|
1852
|
+
"Bootstrap complete",
|
|
1853
|
+
diagnosticResult?.status
|
|
1854
|
+
? `diagnostic ${diagnosticResult.status}`
|
|
1855
|
+
: "smoke path complete",
|
|
1856
|
+
);
|
|
1857
|
+
emitTelemetry(telemetryClient, "installer_smoke_test_completed", {
|
|
1858
|
+
release_tag: options.tagName ?? `pylon-v${options.version}`,
|
|
1859
|
+
release_commit: options.sourceCommit ?? null,
|
|
1860
|
+
os: options.target?.os ?? null,
|
|
1861
|
+
arch: options.target?.arch ?? null,
|
|
1862
|
+
platform_key:
|
|
1863
|
+
options.target?.os && options.target?.arch
|
|
1864
|
+
? `${options.target.os}-${options.target.arch}`
|
|
1865
|
+
: null,
|
|
1866
|
+
install_source: installSourceForTelemetry(
|
|
1867
|
+
options.installMethod,
|
|
1868
|
+
Boolean(options.cached),
|
|
1869
|
+
),
|
|
1870
|
+
diagnostic_status: diagnosticResult?.status ?? null,
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
return {
|
|
1874
|
+
version: options.version,
|
|
1875
|
+
tagName: options.tagName ?? `pylon-v${options.version}`,
|
|
1876
|
+
target: options.target,
|
|
1877
|
+
cached: Boolean(options.cached),
|
|
1878
|
+
installMethod: options.installMethod ?? RELEASE_ASSET_INSTALL_METHOD,
|
|
1879
|
+
binaries: {
|
|
1880
|
+
pylon: pylonPath,
|
|
1881
|
+
pylonTui: pylonTuiPath,
|
|
1882
|
+
},
|
|
1883
|
+
configPath: init?.config_path ?? options.configPath ?? null,
|
|
1884
|
+
pylonHome: options.pylonHome ? path.resolve(options.pylonHome) : null,
|
|
1885
|
+
init,
|
|
1886
|
+
status,
|
|
1887
|
+
inventory,
|
|
1888
|
+
model,
|
|
1889
|
+
download,
|
|
1890
|
+
diagnostic,
|
|
1891
|
+
diagnosticResult,
|
|
1892
|
+
};
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
emitTelemetry(telemetryClient, "installer_smoke_test_failed", {
|
|
1895
|
+
release_tag: options.tagName ?? `pylon-v${options.version}`,
|
|
1896
|
+
release_commit: options.sourceCommit ?? null,
|
|
1897
|
+
os: options.target?.os ?? null,
|
|
1898
|
+
arch: options.target?.arch ?? null,
|
|
1899
|
+
platform_key:
|
|
1900
|
+
options.target?.os && options.target?.arch
|
|
1901
|
+
? `${options.target.os}-${options.target.arch}`
|
|
1902
|
+
: null,
|
|
1903
|
+
install_source: installSourceForTelemetry(
|
|
1904
|
+
options.installMethod,
|
|
1905
|
+
Boolean(options.cached),
|
|
1906
|
+
),
|
|
1907
|
+
...telemetryFailureContext(error, "smoke_test"),
|
|
1908
|
+
});
|
|
1909
|
+
throw error;
|
|
1910
|
+
}
|
|
1470
1911
|
}
|
|
1471
1912
|
|
|
1472
|
-
export async function
|
|
1913
|
+
export async function launchInstalledPylon(
|
|
1473
1914
|
options,
|
|
1474
1915
|
{
|
|
1475
1916
|
runProcessImpl = runProcess,
|
|
1476
1917
|
onStatus = null,
|
|
1477
1918
|
} = {},
|
|
1478
1919
|
) {
|
|
1479
|
-
const
|
|
1480
|
-
emitStatus(onStatus, "
|
|
1481
|
-
return runProcessImpl(
|
|
1920
|
+
const pylonPath = path.resolve(options.pylonPath);
|
|
1921
|
+
emitStatus(onStatus, "Starting Pylon default earning loop", path.basename(pylonPath));
|
|
1922
|
+
return runProcessImpl(pylonPath, [], {
|
|
1482
1923
|
env: buildPylonEnv(options),
|
|
1483
1924
|
stdio: "inherit",
|
|
1484
1925
|
});
|
|
1485
1926
|
}
|
|
1486
1927
|
|
|
1928
|
+
export const launchInstalledPylonTui = launchInstalledPylon;
|
|
1929
|
+
|
|
1487
1930
|
export function resolveBootstrapOutcome(summary) {
|
|
1488
1931
|
const runtimeState =
|
|
1489
1932
|
summary.status?.snapshot?.runtime?.authoritative_status ?? "unknown";
|
|
@@ -1546,7 +1989,7 @@ function renderBootstrapNextSteps(summary, outcome) {
|
|
|
1546
1989
|
];
|
|
1547
1990
|
|
|
1548
1991
|
if (outcome.verdict === "fully online" || outcome.verdict === "runtime ready") {
|
|
1549
|
-
lines.push("Next step:
|
|
1992
|
+
lines.push("Next step: run `pylon`; it starts the default online earning loop.");
|
|
1550
1993
|
return lines;
|
|
1551
1994
|
}
|
|
1552
1995
|
|
|
@@ -1572,6 +2015,7 @@ export function renderBootstrapSummary(summary) {
|
|
|
1572
2015
|
`Verdict detail: ${outcome.detail}`,
|
|
1573
2016
|
`Pylon release: ${summary.version} (${summary.target.os}-${summary.target.arch})`,
|
|
1574
2017
|
`Archive source: ${summary.tagName}`,
|
|
2018
|
+
`Install source: ${installSourceForTelemetry(summary.installMethod, summary.cached).replaceAll("_", " ")}`,
|
|
1575
2019
|
`Installed from cache: ${summary.cached ? "yes" : "no"}`,
|
|
1576
2020
|
`Pylon binary: ${summary.binaries.pylon}`,
|
|
1577
2021
|
`Pylon TUI: ${summary.binaries.pylonTui}`,
|