@openagentsinc/pylon 0.1.0 → 0.1.1
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 +7 -2
- package/package.json +1 -1
- package/src/cli.js +89 -4
- package/src/index.js +118 -23
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# `@openagentsinc/pylon`
|
|
2
2
|
|
|
3
3
|
Bootstrap the latest tagged standalone `Pylon` release asset from GitHub
|
|
4
|
-
Releases
|
|
4
|
+
Releases, stream first-run status updates in the terminal, and open the Pylon
|
|
5
|
+
terminal UI without Cargo.
|
|
5
6
|
|
|
6
7
|
## Usage
|
|
7
8
|
|
|
8
9
|
```bash
|
|
9
10
|
npx @openagentsinc/pylon
|
|
10
|
-
npx @openagentsinc/pylon --version 0.1
|
|
11
|
+
npx @openagentsinc/pylon --version 0.0.1-rc3
|
|
12
|
+
npx @openagentsinc/pylon --no-launch
|
|
11
13
|
npx @openagentsinc/pylon --model gemma-4-e2b --diagnostic-repeats 2
|
|
12
14
|
```
|
|
13
15
|
|
|
@@ -20,9 +22,12 @@ The launcher:
|
|
|
20
22
|
- downloads the archive and published SHA-256 checksum
|
|
21
23
|
- verifies the checksum before extracting
|
|
22
24
|
- caches the unpacked binaries under `~/.openagents/pylon/bootstrap/`
|
|
25
|
+
- prints status lines such as release resolution, runtime checks, and local
|
|
26
|
+
model scanning while it runs
|
|
23
27
|
- runs `pylon --help`, `init`, `status --json`, and `inventory --json`
|
|
24
28
|
- runs `pylon gemma download <model>`
|
|
25
29
|
- runs `pylon gemma diagnose <model> --json`
|
|
30
|
+
- opens `pylon-tui` by default after the smoke path unless `--no-launch` is set
|
|
26
31
|
|
|
27
32
|
## Publish
|
|
28
33
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
DEFAULT_RELEASE_REPO,
|
|
7
7
|
bootstrapInstalledPylon,
|
|
8
8
|
ensureReleaseInstall,
|
|
9
|
+
launchInstalledPylonTui,
|
|
9
10
|
renderBootstrapSummary,
|
|
10
11
|
} from "./index.js";
|
|
11
12
|
|
|
@@ -17,6 +18,52 @@ function parseIntegerFlag(value, label) {
|
|
|
17
18
|
return parsed;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function createTerminalStyles(enableColor) {
|
|
22
|
+
if (!enableColor) {
|
|
23
|
+
return {
|
|
24
|
+
bold: (value) => value,
|
|
25
|
+
cyan: (value) => value,
|
|
26
|
+
dim: (value) => value,
|
|
27
|
+
green: (value) => value,
|
|
28
|
+
red: (value) => value,
|
|
29
|
+
yellow: (value) => value,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const wrap = (open, close) => (value) => `${open}${value}${close}`;
|
|
34
|
+
return {
|
|
35
|
+
bold: wrap("\u001B[1m", "\u001B[22m"),
|
|
36
|
+
cyan: wrap("\u001B[36m", "\u001B[39m"),
|
|
37
|
+
dim: wrap("\u001B[2m", "\u001B[22m"),
|
|
38
|
+
green: wrap("\u001B[32m", "\u001B[39m"),
|
|
39
|
+
red: wrap("\u001B[31m", "\u001B[39m"),
|
|
40
|
+
yellow: wrap("\u001B[33m", "\u001B[39m"),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createReporter({ enableColor = process.stdout.isTTY && !process.env.NO_COLOR } = {}) {
|
|
45
|
+
const styles = createTerminalStyles(enableColor);
|
|
46
|
+
return {
|
|
47
|
+
status({ message, detail = null }) {
|
|
48
|
+
const prefix = styles.cyan("›");
|
|
49
|
+
const suffix = detail ? ` ${styles.dim(detail)}` : "";
|
|
50
|
+
console.log(`${prefix} ${styles.bold(message)}${suffix}`);
|
|
51
|
+
},
|
|
52
|
+
success(message, detail = null) {
|
|
53
|
+
const suffix = detail ? ` ${styles.dim(detail)}` : "";
|
|
54
|
+
console.log(`${styles.green("✓")} ${styles.bold(message)}${suffix}`);
|
|
55
|
+
},
|
|
56
|
+
warning(message, detail = null) {
|
|
57
|
+
const suffix = detail ? ` ${styles.dim(detail)}` : "";
|
|
58
|
+
console.log(`${styles.yellow("!")} ${styles.bold(message)}${suffix}`);
|
|
59
|
+
},
|
|
60
|
+
failure(message, detail = null) {
|
|
61
|
+
const suffix = detail ? ` ${styles.dim(detail)}` : "";
|
|
62
|
+
console.error(`${styles.red("x")} ${styles.bold(message)}${suffix}`);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
20
67
|
export function usage() {
|
|
21
68
|
return `Usage:
|
|
22
69
|
npx @openagentsinc/pylon [options]
|
|
@@ -24,7 +71,8 @@ export function usage() {
|
|
|
24
71
|
Description:
|
|
25
72
|
Download the latest tagged standalone Pylon release asset for this machine,
|
|
26
73
|
or a specific tagged Pylon version when --version is set. Verify its
|
|
27
|
-
checksum, cache the binaries locally,
|
|
74
|
+
checksum, cache the binaries locally, run the first-run smoke path, and then
|
|
75
|
+
open the Pylon terminal UI by default with live status updates.
|
|
28
76
|
|
|
29
77
|
Options:
|
|
30
78
|
--version <x.y.z> Resolve a specific Pylon release.
|
|
@@ -39,6 +87,7 @@ Options:
|
|
|
39
87
|
Default: ${DEFAULT_DIAGNOSTIC_MAX_OUTPUT_TOKENS}
|
|
40
88
|
--skip-model-download Skip pylon gemma download.
|
|
41
89
|
--skip-diagnostics Skip pylon gemma diagnose.
|
|
90
|
+
--no-launch Do not open pylon-tui after bootstrap.
|
|
42
91
|
--json Emit a machine-readable JSON summary.
|
|
43
92
|
|
|
44
93
|
Test and maintainer options:
|
|
@@ -61,6 +110,7 @@ export function parseArgs(argv) {
|
|
|
61
110
|
diagnosticMaxOutputTokens: DEFAULT_DIAGNOSTIC_MAX_OUTPUT_TOKENS,
|
|
62
111
|
skipModelDownload: false,
|
|
63
112
|
skipDiagnostics: false,
|
|
113
|
+
noLaunch: false,
|
|
64
114
|
json: false,
|
|
65
115
|
help: false,
|
|
66
116
|
};
|
|
@@ -116,6 +166,9 @@ export function parseArgs(argv) {
|
|
|
116
166
|
case "--skip-diagnostics":
|
|
117
167
|
options.skipDiagnostics = true;
|
|
118
168
|
break;
|
|
169
|
+
case "--no-launch":
|
|
170
|
+
options.noLaunch = true;
|
|
171
|
+
break;
|
|
119
172
|
case "--json":
|
|
120
173
|
options.json = true;
|
|
121
174
|
break;
|
|
@@ -144,26 +197,58 @@ export function parseArgs(argv) {
|
|
|
144
197
|
}
|
|
145
198
|
|
|
146
199
|
export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
200
|
+
const {
|
|
201
|
+
ensureReleaseInstallImpl = ensureReleaseInstall,
|
|
202
|
+
bootstrapInstalledPylonImpl = bootstrapInstalledPylon,
|
|
203
|
+
launchInstalledPylonTuiImpl = launchInstalledPylonTui,
|
|
204
|
+
} = dependencies;
|
|
147
205
|
const options = parseArgs(argv);
|
|
148
206
|
if (options.help) {
|
|
149
207
|
console.log(usage());
|
|
150
208
|
return null;
|
|
151
209
|
}
|
|
152
210
|
|
|
153
|
-
const
|
|
154
|
-
|
|
211
|
+
const reporter = options.json ? null : createReporter();
|
|
212
|
+
|
|
213
|
+
const install = await ensureReleaseInstallImpl(options, {
|
|
214
|
+
...dependencies,
|
|
215
|
+
onStatus: reporter?.status,
|
|
216
|
+
});
|
|
217
|
+
const summary = await bootstrapInstalledPylonImpl(
|
|
155
218
|
{
|
|
156
219
|
...options,
|
|
157
220
|
...install,
|
|
158
221
|
version: install.version,
|
|
159
222
|
},
|
|
160
|
-
|
|
223
|
+
{
|
|
224
|
+
...dependencies,
|
|
225
|
+
onStatus: reporter?.status,
|
|
226
|
+
},
|
|
161
227
|
);
|
|
162
228
|
|
|
163
229
|
if (options.json) {
|
|
164
230
|
console.log(JSON.stringify(summary, null, 2));
|
|
165
231
|
} else {
|
|
232
|
+
reporter?.success("Pylon bootstrap complete");
|
|
166
233
|
console.log(renderBootstrapSummary(summary));
|
|
234
|
+
if (!options.noLaunch) {
|
|
235
|
+
await launchInstalledPylonTuiImpl(
|
|
236
|
+
{
|
|
237
|
+
...options,
|
|
238
|
+
...install,
|
|
239
|
+
version: install.version,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
...dependencies,
|
|
243
|
+
onStatus: reporter?.status,
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
} else {
|
|
247
|
+
reporter?.warning(
|
|
248
|
+
"Skipped Pylon terminal UI launch",
|
|
249
|
+
"pass no flag to open pylon-tui by default",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
167
252
|
}
|
|
168
253
|
return summary;
|
|
169
254
|
}
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,12 @@ export const DEFAULT_DIAGNOSTIC_REPEATS = 3;
|
|
|
12
12
|
export const DEFAULT_DIAGNOSTIC_MAX_OUTPUT_TOKENS = 96;
|
|
13
13
|
const PYLON_RELEASE_TAG_PREFIX = "pylon-v";
|
|
14
14
|
|
|
15
|
+
function emitStatus(onStatus, message, detail = null) {
|
|
16
|
+
if (typeof onStatus === "function") {
|
|
17
|
+
onStatus({ message, detail });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
function normalizeVersion(value) {
|
|
16
22
|
return value.replace(/^pylon-v/, "").replace(/^v/, "");
|
|
17
23
|
}
|
|
@@ -264,22 +270,26 @@ export function buildInstallPaths(installRoot, version, target) {
|
|
|
264
270
|
export async function runProcess(
|
|
265
271
|
command,
|
|
266
272
|
args,
|
|
267
|
-
{ cwd, env } = {},
|
|
273
|
+
{ cwd, env, stdio = ["ignore", "pipe", "pipe"] } = {},
|
|
268
274
|
) {
|
|
269
275
|
return new Promise((resolve, reject) => {
|
|
270
276
|
const child = spawn(command, args, {
|
|
271
277
|
cwd,
|
|
272
278
|
env,
|
|
273
|
-
stdio
|
|
279
|
+
stdio,
|
|
274
280
|
});
|
|
275
281
|
let stdout = "";
|
|
276
282
|
let stderr = "";
|
|
277
|
-
child.stdout
|
|
278
|
-
stdout
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
if (child.stdout) {
|
|
284
|
+
child.stdout.on("data", (chunk) => {
|
|
285
|
+
stdout += chunk.toString();
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (child.stderr) {
|
|
289
|
+
child.stderr.on("data", (chunk) => {
|
|
290
|
+
stderr += chunk.toString();
|
|
291
|
+
});
|
|
292
|
+
}
|
|
283
293
|
child.on("error", (error) => {
|
|
284
294
|
reject(
|
|
285
295
|
new Error(
|
|
@@ -343,17 +353,31 @@ async function runPylonJson(pylonPath, args, options, runProcessImpl) {
|
|
|
343
353
|
}
|
|
344
354
|
}
|
|
345
355
|
|
|
356
|
+
function isUnsupportedGemmaDiagnoseError(error) {
|
|
357
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
358
|
+
return (
|
|
359
|
+
message.includes("unknown gemma command: diagnose") ||
|
|
360
|
+
message.includes("unknown command: diagnose")
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
346
364
|
export async function ensureReleaseInstall(
|
|
347
365
|
options = {},
|
|
348
366
|
{
|
|
349
367
|
fetchImpl = globalThis.fetch,
|
|
350
368
|
runProcessImpl = runProcess,
|
|
369
|
+
onStatus = null,
|
|
351
370
|
} = {},
|
|
352
371
|
) {
|
|
353
372
|
if (typeof fetchImpl !== "function") {
|
|
354
373
|
throw new Error("A global fetch implementation is required to bootstrap Pylon.");
|
|
355
374
|
}
|
|
356
375
|
|
|
376
|
+
emitStatus(
|
|
377
|
+
onStatus,
|
|
378
|
+
"Resolving latest tagged Pylon release",
|
|
379
|
+
options.version ? `requested ${options.version}` : "default release track",
|
|
380
|
+
);
|
|
357
381
|
const target = resolvePlatformTarget(options.platform, options.arch);
|
|
358
382
|
const installRoot = options.installRoot ?? defaultInstallRoot();
|
|
359
383
|
const release = await fetchReleaseMetadata({
|
|
@@ -368,6 +392,11 @@ export async function ensureReleaseInstall(
|
|
|
368
392
|
const binariesPresent =
|
|
369
393
|
(await pathExists(paths.pylonPath)) && (await pathExists(paths.pylonTuiPath));
|
|
370
394
|
if (binariesPresent) {
|
|
395
|
+
emitStatus(
|
|
396
|
+
onStatus,
|
|
397
|
+
"Using cached standalone binaries",
|
|
398
|
+
`${selected.tagName} for ${target.os}-${target.arch}`,
|
|
399
|
+
);
|
|
371
400
|
return {
|
|
372
401
|
...selected,
|
|
373
402
|
...paths,
|
|
@@ -380,6 +409,11 @@ export async function ensureReleaseInstall(
|
|
|
380
409
|
};
|
|
381
410
|
}
|
|
382
411
|
|
|
412
|
+
emitStatus(
|
|
413
|
+
onStatus,
|
|
414
|
+
"Fetching release checksum",
|
|
415
|
+
selected.checksumAsset.name,
|
|
416
|
+
);
|
|
383
417
|
const checksumPayload = await fetchText(fetchImpl, selected.checksumAsset.url);
|
|
384
418
|
const expectedSha256 = parseSha256File(
|
|
385
419
|
checksumPayload,
|
|
@@ -393,6 +427,11 @@ export async function ensureReleaseInstall(
|
|
|
393
427
|
archiveReady = (await sha256File(paths.archivePath)) === expectedSha256;
|
|
394
428
|
}
|
|
395
429
|
if (!archiveReady) {
|
|
430
|
+
emitStatus(
|
|
431
|
+
onStatus,
|
|
432
|
+
"Downloading standalone binaries",
|
|
433
|
+
selected.archiveAsset.name,
|
|
434
|
+
);
|
|
396
435
|
await downloadFile(fetchImpl, selected.archiveAsset.url, paths.archivePath);
|
|
397
436
|
}
|
|
398
437
|
|
|
@@ -403,6 +442,11 @@ export async function ensureReleaseInstall(
|
|
|
403
442
|
);
|
|
404
443
|
}
|
|
405
444
|
|
|
445
|
+
emitStatus(
|
|
446
|
+
onStatus,
|
|
447
|
+
"Extracting standalone binaries",
|
|
448
|
+
paths.installDir,
|
|
449
|
+
);
|
|
406
450
|
await fs.rm(paths.installDir, { recursive: true, force: true });
|
|
407
451
|
await extractArchive(paths.archivePath, paths.versionsDir, runProcessImpl);
|
|
408
452
|
|
|
@@ -432,6 +476,12 @@ export async function ensureReleaseInstall(
|
|
|
432
476
|
)}\n`,
|
|
433
477
|
);
|
|
434
478
|
|
|
479
|
+
emitStatus(
|
|
480
|
+
onStatus,
|
|
481
|
+
"Installed standalone binaries",
|
|
482
|
+
`${selected.tagName} for ${target.os}-${target.arch}`,
|
|
483
|
+
);
|
|
484
|
+
|
|
435
485
|
return {
|
|
436
486
|
...selected,
|
|
437
487
|
...paths,
|
|
@@ -445,6 +495,7 @@ export async function bootstrapInstalledPylon(
|
|
|
445
495
|
options,
|
|
446
496
|
{
|
|
447
497
|
runProcessImpl = runProcess,
|
|
498
|
+
onStatus = null,
|
|
448
499
|
} = {},
|
|
449
500
|
) {
|
|
450
501
|
const pylonPath = path.resolve(options.pylonPath);
|
|
@@ -455,14 +506,18 @@ export async function bootstrapInstalledPylon(
|
|
|
455
506
|
const diagnosticMaxOutputTokens =
|
|
456
507
|
options.diagnosticMaxOutputTokens ?? DEFAULT_DIAGNOSTIC_MAX_OUTPUT_TOKENS;
|
|
457
508
|
|
|
509
|
+
emitStatus(onStatus, "Verifying Pylon binary", path.basename(pylonPath));
|
|
458
510
|
await runPylonCommand(pylonPath, ["--help"], options, runProcessImpl);
|
|
511
|
+
emitStatus(onStatus, "Bootstrapping local Pylon identity");
|
|
459
512
|
const init = await runPylonJson(pylonPath, ["init"], options, runProcessImpl);
|
|
513
|
+
emitStatus(onStatus, "Checking runtime health");
|
|
460
514
|
const status = await runPylonJson(
|
|
461
515
|
pylonPath,
|
|
462
516
|
["status", "--json"],
|
|
463
517
|
options,
|
|
464
518
|
runProcessImpl,
|
|
465
519
|
);
|
|
520
|
+
emitStatus(onStatus, "Scanning for local models");
|
|
466
521
|
const inventory = await runPylonJson(
|
|
467
522
|
pylonPath,
|
|
468
523
|
["inventory", "--json"],
|
|
@@ -472,31 +527,48 @@ export async function bootstrapInstalledPylon(
|
|
|
472
527
|
|
|
473
528
|
let download = null;
|
|
474
529
|
if (!options.skipModelDownload) {
|
|
530
|
+
emitStatus(onStatus, "Downloading curated model bundle", model);
|
|
475
531
|
download = await runPylonJson(
|
|
476
532
|
pylonPath,
|
|
477
533
|
["gemma", "download", model, "--json"],
|
|
478
534
|
options,
|
|
479
535
|
runProcessImpl,
|
|
480
536
|
);
|
|
537
|
+
} else {
|
|
538
|
+
emitStatus(onStatus, "Skipping curated model download", model);
|
|
481
539
|
}
|
|
482
540
|
|
|
483
541
|
let diagnostic = null;
|
|
484
542
|
if (!options.skipDiagnostics) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
543
|
+
emitStatus(onStatus, "Running first-run diagnostic", model);
|
|
544
|
+
try {
|
|
545
|
+
diagnostic = await runPylonJson(
|
|
546
|
+
pylonPath,
|
|
547
|
+
[
|
|
548
|
+
"gemma",
|
|
549
|
+
"diagnose",
|
|
550
|
+
model,
|
|
551
|
+
"--max-output-tokens",
|
|
552
|
+
String(diagnosticMaxOutputTokens),
|
|
553
|
+
"--repeats",
|
|
554
|
+
String(diagnosticRepeats),
|
|
555
|
+
"--json",
|
|
556
|
+
],
|
|
557
|
+
options,
|
|
558
|
+
runProcessImpl,
|
|
559
|
+
);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (!isUnsupportedGemmaDiagnoseError(error)) {
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
emitStatus(
|
|
565
|
+
onStatus,
|
|
566
|
+
"Skipping first-run diagnostic",
|
|
567
|
+
"installed Pylon release does not expose gemma diagnose",
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
emitStatus(onStatus, "Skipping first-run diagnostic", model);
|
|
500
572
|
}
|
|
501
573
|
|
|
502
574
|
const diagnosticResult =
|
|
@@ -504,6 +576,14 @@ export async function bootstrapInstalledPylon(
|
|
|
504
576
|
diagnostic?.results?.[0] ??
|
|
505
577
|
null;
|
|
506
578
|
|
|
579
|
+
emitStatus(
|
|
580
|
+
onStatus,
|
|
581
|
+
"Bootstrap complete",
|
|
582
|
+
diagnosticResult?.status
|
|
583
|
+
? `diagnostic ${diagnosticResult.status}`
|
|
584
|
+
: "smoke path complete",
|
|
585
|
+
);
|
|
586
|
+
|
|
507
587
|
return {
|
|
508
588
|
version: options.version,
|
|
509
589
|
tagName: options.tagName ?? `pylon-v${options.version}`,
|
|
@@ -525,6 +605,21 @@ export async function bootstrapInstalledPylon(
|
|
|
525
605
|
};
|
|
526
606
|
}
|
|
527
607
|
|
|
608
|
+
export async function launchInstalledPylonTui(
|
|
609
|
+
options,
|
|
610
|
+
{
|
|
611
|
+
runProcessImpl = runProcess,
|
|
612
|
+
onStatus = null,
|
|
613
|
+
} = {},
|
|
614
|
+
) {
|
|
615
|
+
const pylonTuiPath = path.resolve(options.pylonTuiPath);
|
|
616
|
+
emitStatus(onStatus, "Opening Pylon terminal UI", path.basename(pylonTuiPath));
|
|
617
|
+
return runProcessImpl(pylonTuiPath, [], {
|
|
618
|
+
env: buildPylonEnv(options),
|
|
619
|
+
stdio: "inherit",
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
528
623
|
export function renderBootstrapSummary(summary) {
|
|
529
624
|
const lines = [
|
|
530
625
|
`Pylon release: ${summary.version} (${summary.target.os}-${summary.target.arch})`,
|