@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 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 and run the first-run smoke path without Cargo.
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.0
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagentsinc/pylon",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Bootstrap the standalone OpenAgents Pylon release asset and run first-run smoke checks.",
5
5
  "type": "module",
6
6
  "bin": {
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, and run the first-run smoke path.
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 install = await ensureReleaseInstall(options, dependencies);
154
- const summary = await bootstrapInstalledPylon(
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
- dependencies,
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: ["ignore", "pipe", "pipe"],
279
+ stdio,
274
280
  });
275
281
  let stdout = "";
276
282
  let stderr = "";
277
- child.stdout.on("data", (chunk) => {
278
- stdout += chunk.toString();
279
- });
280
- child.stderr.on("data", (chunk) => {
281
- stderr += chunk.toString();
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
- diagnostic = await runPylonJson(
486
- pylonPath,
487
- [
488
- "gemma",
489
- "diagnose",
490
- model,
491
- "--max-output-tokens",
492
- String(diagnosticMaxOutputTokens),
493
- "--repeats",
494
- String(diagnosticRepeats),
495
- "--json",
496
- ],
497
- options,
498
- runProcessImpl,
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})`,