@yolo-labs/yolobridge 0.15.0 → 0.17.0

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/dist/cli.js CHANGED
@@ -17,7 +17,8 @@
17
17
  * YOLOBRIDGE_AUTH_URL for local dev against a different environment.
18
18
  */
19
19
  import { fileURLToPath } from 'node:url';
20
- import { realpathSync } from 'node:fs';
20
+ import path from 'node:path';
21
+ import { realpathSync, existsSync, readFileSync } from 'node:fs';
21
22
  import { hostname } from 'node:os';
22
23
  import { runLogin } from './login-cmd.js';
23
24
  import { runAttachFromDisk, pickWorkspaceFromDisk } from './attach-cmd.js';
@@ -99,6 +100,7 @@ function printHelp() {
99
100
  ' that check entirely.',
100
101
  ' detach Detach the current workspace attachment.',
101
102
  ' status Print local login/attach state.',
103
+ ' version Print the installed yolo-bridge version (also --version, -v).',
102
104
  ' --help Print this help.',
103
105
  '',
104
106
  `API base: ${apiUrl()} (override: YOLOBRIDGE_API_URL)`,
@@ -559,6 +561,35 @@ async function cmdWorkspaces() {
559
561
  process.stdout.write(`${formatWorkspacesTable(result.workspaces)}\n`);
560
562
  return 0;
561
563
  }
564
+ /**
565
+ * This package's own version, read from the package.json that ships beside
566
+ * `dist/`.
567
+ *
568
+ * Resolved from `import.meta.url`, NOT `process.argv[1]`: the global install
569
+ * puts a symlink on PATH, and argv[1] is that symlink's unresolved path, so
570
+ * walking up from it lands outside the package. Same reasoning as
571
+ * `isMainModule` below.
572
+ *
573
+ * Returns `'unknown'` rather than throwing — a version probe must never be the
574
+ * thing that stops the CLI from starting.
575
+ */
576
+ export function readOwnVersion() {
577
+ try {
578
+ const here = path.dirname(fileURLToPath(import.meta.url));
579
+ // dist/cli.js -> package root. Walk up a couple of levels so this holds
580
+ // whether it is run from `dist/` or from a ts-node-style layout.
581
+ for (const rel of ['..', '../..']) {
582
+ const candidate = path.join(here, rel, 'package.json');
583
+ if (!existsSync(candidate))
584
+ continue;
585
+ const parsed = JSON.parse(readFileSync(candidate, 'utf-8'));
586
+ if (parsed.name === '@yolo-labs/yolobridge' && typeof parsed.version === 'string')
587
+ return parsed.version;
588
+ }
589
+ }
590
+ catch { /* fall through */ }
591
+ return 'unknown';
592
+ }
562
593
  async function main() {
563
594
  const [, , cmd, ...rest] = process.argv;
564
595
  switch (cmd) {
@@ -572,6 +603,11 @@ async function main() {
572
603
  return cmdDetach();
573
604
  case 'status':
574
605
  return cmdStatus();
606
+ case 'version':
607
+ case '--version':
608
+ case '-v':
609
+ process.stdout.write(`${readOwnVersion()}\n`);
610
+ return 0;
575
611
  case '--help':
576
612
  case '-h':
577
613
  case undefined:
@@ -412,10 +412,16 @@ export function computeUsedRows(term) {
412
412
  }
413
413
  return used;
414
414
  }
415
- /** The PTY's grid, which the tile must render at EXACTLY (it cannot be
416
- * resized — the human at the keyboard is watching the same PTY), plus how
415
+ /** The PTY's CURRENT grid, which the tile must render at EXACTLY, plus how
417
416
  * much of that grid is in USE.
418
417
  *
418
+ * The viewer must not resize this grid — the human at the keyboard is
419
+ * watching the same PTY, so a cloud-side resize would reach over and reflow
420
+ * their real screen. But the human themselves CAN resize it, and when they
421
+ * do the ring restarts under a fresh epoch (see `handleLocalResize`), so a
422
+ * viewer holding the old geometry re-seeds rather than replaying old bytes
423
+ * against a grid that no longer exists.
424
+ *
419
425
  * ⚠️ `usedRows` IS NOT A SECOND GEOMETRY. It never changes `cols`/`rows` and
420
426
  * the viewer never resizes its terminal to it: a viewer's terminal that is
421
427
  * not exactly `cols`×`rows` cannot replay the daemon's bytes (relative cursor
@@ -679,6 +685,12 @@ export function startLocalAgent(opts = {}) {
679
685
  // explicitly to disable stdin piping entirely, distinct from omitting the
680
686
  // field (which defaults to wiring up the real `process.stdin`).
681
687
  const inStream = 'stdin' in opts ? opts.stdin : process.stdin;
688
+ // Same defaulting rule as `resolveCols`/`resolveRows`: only reach for the real
689
+ // `process.stdout` when nothing was injected, so a test with a fake sink does
690
+ // not silently pick up the CI runner's terminal.
691
+ const resizeSource = 'resizeSource' in opts
692
+ ? opts.resizeSource
693
+ : (opts.stdout === undefined ? process.stdout : undefined);
682
694
  const ptyProcess = spawnImpl(agentBin, agentArgs, {
683
695
  name: 'xterm-256color',
684
696
  cols,
@@ -706,8 +718,14 @@ export function startLocalAgent(opts = {}) {
706
718
  writeChain: Promise.resolve(),
707
719
  stdin: inStream,
708
720
  rawModeEnabled: false,
721
+ resizeSource,
709
722
  };
710
723
  current = state;
724
+ if (resizeSource && typeof resizeSource.on === 'function') {
725
+ const resizeListener = () => handleLocalResize(state);
726
+ state.resizeListener = resizeListener;
727
+ resizeSource.on('resize', resizeListener);
728
+ }
711
729
  ptyProcess.onData((data) => {
712
730
  state.lastOutputAt = Date.now();
713
731
  outStream.write(data);
@@ -756,7 +774,53 @@ export function startLocalAgent(opts = {}) {
756
774
  * exit code 124. With `stdin.pause()` added below, the same repro exits
757
775
  * cleanly on its own well under a second — no timeout/kill needed.
758
776
  */
777
+ /**
778
+ * The human at the keyboard resized their terminal window.
779
+ *
780
+ * WHY THIS EXISTS: the PTY size used to be treated as fixed at spawn, on the
781
+ * reasoning that "nothing resizes this PTY, least of all the cloud". The cloud
782
+ * half of that is right and still enforced — a viewer must never reach over and
783
+ * reflow the operator's real screen. The other half was simply wrong: the human
784
+ * watching this PTY through `process.stdout` can drag their window, Node raises
785
+ * `'resize'` on SIGWINCH, and we ignored it. The child kept rendering to the old
786
+ * grid, so the operator's own terminal garbled and the tile kept reporting a
787
+ * geometry that no longer matched anything.
788
+ *
789
+ * Everything retained in the ring was captured against a grid that no longer
790
+ * exists — relative cursor moves and the scroll region are defined against the
791
+ * REAL grid, so replaying those bytes at the new size garbles rather than
792
+ * degrades. A fresh epoch is the honest answer: the viewer re-seeds from the
793
+ * current buffer instead of splicing two geometries together.
794
+ */
795
+ function handleLocalResize(state) {
796
+ const src = state.resizeSource;
797
+ if (!src)
798
+ return;
799
+ const cols = src.columns ?? 0;
800
+ const rows = src.rows ?? 0;
801
+ // A stdout that has stopped being a TTY (piped, or the window is gone)
802
+ // reports 0/undefined. Resizing a PTY to zero wedges the child.
803
+ if (cols < 1 || rows < 1)
804
+ return;
805
+ // SIGWINCH also fires for changes that are not a size change. Resizing to the
806
+ // size we already have would burn an epoch and force a pointless re-seed.
807
+ if (cols === state.cols && rows === state.rows)
808
+ return;
809
+ state.cols = cols;
810
+ state.rows = rows;
811
+ state.term.resize(cols, rows);
812
+ rawRing = newRawRing(rows);
813
+ try {
814
+ state.ptyProcess.resize(cols, rows);
815
+ }
816
+ catch {
817
+ // The child is already gone; onExit will clear `current`.
818
+ }
819
+ }
759
820
  function teardownStdio(state) {
821
+ if (state.resizeSource && state.resizeListener && typeof state.resizeSource.removeListener === 'function') {
822
+ state.resizeSource.removeListener('resize', state.resizeListener);
823
+ }
760
824
  const { stdin, stdinListener } = state;
761
825
  if (stdin && stdinListener && typeof stdin.removeListener === 'function') {
762
826
  stdin.removeListener('data', stdinListener);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yolo-labs/yolobridge",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "YoloBridge — local coding-agent daemon that attaches a user's own Claude Code/Codex session to a YOLO Studio workspace as a first-class tile (docs/YOLOBRIDGE_PLAN.md, build-order Phase 5).",
5
5
  "license": "MIT",
6
6
  "type": "module",