replicas-cli 0.2.349 → 0.2.350

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.
Files changed (2) hide show
  1. package/dist/index.mjs +75 -37
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -9601,7 +9601,7 @@ var HOOK_EXEC_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
9601
9601
  var REPLICAS_CONFIG_FILENAMES = ["replicas.json", "replicas.yaml", "replicas.yml"];
9602
9602
 
9603
9603
  // ../shared/src/cli-version.ts
9604
- var CLI_VERSION = "0.2.349";
9604
+ var CLI_VERSION = "0.2.350";
9605
9605
 
9606
9606
  // ../shared/src/version.ts
9607
9607
  function compareVersions(v1, v2) {
@@ -14796,21 +14796,23 @@ async function learningsDeleteCommand(id) {
14796
14796
  // src/commands/computer/index.ts
14797
14797
  import { spawn as spawn4, spawnSync as spawnSync3 } from "child_process";
14798
14798
  import { createHash as createHash2 } from "crypto";
14799
- import { closeSync, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, readSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
14800
- import { dirname as dirname2 } from "path";
14799
+ import { closeSync, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync3, readSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
14800
+ import { dirname as dirname3 } from "path";
14801
14801
  import chalk21 from "chalk";
14802
14802
 
14803
14803
  // src/commands/computer/desktop.ts
14804
14804
  import { spawnSync } from "child_process";
14805
- import { existsSync } from "fs";
14806
- import { isAbsolute, resolve } from "path";
14805
+ import { existsSync, mkdirSync, readFileSync } from "fs";
14806
+ import { dirname, isAbsolute, resolve } from "path";
14807
14807
  var STATE_DIR = process.env.REPLICAS_DESKTOP_STATE_DIR || "/tmp/replicas-computer";
14808
14808
  var DEFAULT_DISPLAY = process.env.REPLICAS_DESKTOP_DISPLAY || ":99";
14809
14809
  var NOVNC_PORT = process.env.REPLICAS_DESKTOP_NOVNC_PORT ? parseInt(process.env.REPLICAS_DESKTOP_NOVNC_PORT, 10) : DESKTOP_NOVNC_PORT;
14810
14810
  var SERVICES_SCRIPT = "/usr/local/bin/replicas-start-desktop-services";
14811
+ var INPUT_LOCK_FILE = process.env.REPLICAS_DESKTOP_INPUT_LOCK_FILE || `${STATE_DIR}/input.lock`;
14811
14812
  var CHROME_WRAPPER = "/usr/local/bin/replicas-chrome";
14812
14813
  var INFO_WAIT_TIMEOUT_MS = 1e4;
14813
14814
  var INFO_WAIT_INTERVAL_MS = 500;
14815
+ var lastDesktopHealthAt = 0;
14814
14816
  var CHROME_DEBUG_PORT = (() => {
14815
14817
  const value = process.env.REPLICAS_DESKTOP_CHROME_DEBUG_PORT;
14816
14818
  if (!value) return 9222;
@@ -14823,16 +14825,49 @@ var CHROME_DEBUG_PORT = (() => {
14823
14825
  function fail(msg) {
14824
14826
  throw new Error(msg);
14825
14827
  }
14828
+ function getDesktopBridgeStatus() {
14829
+ const r = spawnSync("bash", [SERVICES_SCRIPT, "--status-json"], { stdio: "pipe" });
14830
+ if (r.status !== 0) return null;
14831
+ try {
14832
+ return JSON.parse(r.stdout.toString());
14833
+ } catch {
14834
+ return null;
14835
+ }
14836
+ }
14837
+ function desktopStackHealthy() {
14838
+ const pids = {};
14839
+ for (const name of ["openbox", "tint2", "x11vnc", "novnc"]) {
14840
+ try {
14841
+ const pid = Number.parseInt(readFileSync(`${STATE_DIR}/${name}.pid`, "utf8").trim(), 10);
14842
+ if (!Number.isFinite(pid)) return false;
14843
+ process.kill(pid, 0);
14844
+ pids[name] = pid;
14845
+ } catch {
14846
+ return false;
14847
+ }
14848
+ }
14849
+ if (spawnSync("xdpyinfo", [], { env: withDisplay(), stdio: "ignore" }).status !== 0) return false;
14850
+ const wm = spawnSync("xprop", ["-root", "_NET_SUPPORTING_WM_CHECK"], { env: withDisplay(), stdio: "pipe" });
14851
+ if (wm.status !== 0 || !wm.stdout?.toString().includes("window id")) return false;
14852
+ const bridge = getDesktopBridgeStatus();
14853
+ return !!bridge?.x11vnc.listenerPids?.includes(pids.x11vnc) && !!bridge.websockify.listenerPids?.includes(pids.novnc);
14854
+ }
14826
14855
  function ensureServicesRunning() {
14827
14856
  if (!existsSync(SERVICES_SCRIPT)) {
14828
14857
  fail(
14829
14858
  `Desktop services script missing at ${SERVICES_SCRIPT}. The workspace image is out of date \u2014 Xvfb / openbox / x11vnc / websockify must be installed and \`replicas-start-desktop-services\` baked in.`
14830
14859
  );
14831
14860
  }
14861
+ if (Date.now() - lastDesktopHealthAt < 1e3) return;
14862
+ if (desktopStackHealthy()) {
14863
+ lastDesktopHealthAt = Date.now();
14864
+ return;
14865
+ }
14832
14866
  const r = spawnSync("bash", [SERVICES_SCRIPT], { stdio: "pipe" });
14833
14867
  if (r.status !== 0) {
14834
14868
  fail(`Failed to start desktop services: ${r.stderr?.toString() || "unknown error"}`);
14835
14869
  }
14870
+ lastDesktopHealthAt = Date.now();
14836
14871
  }
14837
14872
  function withDisplay(env = process.env) {
14838
14873
  return { ...env, DISPLAY: DEFAULT_DISPLAY };
@@ -14845,6 +14880,17 @@ function runDisplayCmd(bin, args) {
14845
14880
  }
14846
14881
  return r.stdout?.toString() ?? "";
14847
14882
  }
14883
+ function runDesktopInputCmd(args) {
14884
+ mkdirSync(dirname(INPUT_LOCK_FILE), { recursive: true });
14885
+ return runDisplayCmd("flock", [
14886
+ "--exclusive",
14887
+ "--wait",
14888
+ process.env.REPLICAS_DESKTOP_INPUT_LOCK_WAIT_SECONDS || "60",
14889
+ INPUT_LOCK_FILE,
14890
+ "xdotool",
14891
+ ...args
14892
+ ]);
14893
+ }
14848
14894
  function tryDisplayCmd(bin, args) {
14849
14895
  ensureServicesRunning();
14850
14896
  const r = spawnSync(bin, args, { env: withDisplay(), stdio: "pipe" });
@@ -14910,8 +14956,8 @@ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
14910
14956
 
14911
14957
  // src/commands/computer/recording.ts
14912
14958
  import { spawn as spawn3 } from "child_process";
14913
- import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
14914
- import { dirname } from "path";
14959
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
14960
+ import { dirname as dirname2 } from "path";
14915
14961
 
14916
14962
  // src/commands/computer/recording/render.ts
14917
14963
  import { spawnSync as spawnSync2 } from "child_process";
@@ -15393,7 +15439,7 @@ var RECORD_DIMENSIONS_FILE = `${STATE_DIR}/recording-dimensions.json`;
15393
15439
  var RECORD_ACTIONS_FILE = `${STATE_DIR}/recording-actions.jsonl`;
15394
15440
  function recordingStartedAt() {
15395
15441
  if (!existsSync2(RECORD_STARTED_AT_FILE)) return null;
15396
- const startedAt = Number.parseInt(readFileSync(RECORD_STARTED_AT_FILE, "utf8").trim(), 10);
15442
+ const startedAt = Number.parseInt(readFileSync2(RECORD_STARTED_AT_FILE, "utf8").trim(), 10);
15397
15443
  return Number.isFinite(startedAt) ? startedAt : null;
15398
15444
  }
15399
15445
  function logRecordingAction(action) {
@@ -15406,7 +15452,7 @@ function logRecordingAction(action) {
15406
15452
  function readRecordingDimensions() {
15407
15453
  if (!existsSync2(RECORD_DIMENSIONS_FILE)) return configuredDesktopDimensions();
15408
15454
  try {
15409
- const dimensions = JSON.parse(readFileSync(RECORD_DIMENSIONS_FILE, "utf8"));
15455
+ const dimensions = JSON.parse(readFileSync2(RECORD_DIMENSIONS_FILE, "utf8"));
15410
15456
  const width = dimensions?.width;
15411
15457
  const height = dimensions?.height;
15412
15458
  if (typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0) {
@@ -15424,7 +15470,7 @@ function isOptionalNumber(value) {
15424
15470
  }
15425
15471
  function readRecordingActions() {
15426
15472
  if (!existsSync2(RECORD_ACTIONS_FILE)) return [];
15427
- return readFileSync(RECORD_ACTIONS_FILE, "utf8").split("\n").filter(Boolean).flatMap((line) => {
15473
+ return readFileSync2(RECORD_ACTIONS_FILE, "utf8").split("\n").filter(Boolean).flatMap((line) => {
15428
15474
  try {
15429
15475
  const value = JSON.parse(line);
15430
15476
  if (typeof value !== "object" || value === null) return [];
@@ -15446,7 +15492,7 @@ function readRecordingActions() {
15446
15492
  async function computerRecordStartCommand(path6, options) {
15447
15493
  ensureServicesRunning();
15448
15494
  if (existsSync2(RECORD_PID_FILE)) {
15449
- const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
15495
+ const pid = parseInt(readFileSync2(RECORD_PID_FILE, "utf8").trim(), 10);
15450
15496
  if (Number.isFinite(pid)) {
15451
15497
  let alive = false;
15452
15498
  try {
@@ -15458,10 +15504,10 @@ async function computerRecordStartCommand(path6, options) {
15458
15504
  }
15459
15505
  }
15460
15506
  const target = resolvePath(path6);
15461
- mkdirSync(dirname(target), { recursive: true });
15507
+ mkdirSync2(dirname2(target), { recursive: true });
15462
15508
  const fps = options.fps ? parseCoord(options.fps, "--fps") : 60;
15463
15509
  const { width, height } = configuredDesktopDimensions();
15464
- mkdirSync(STATE_DIR, { recursive: true });
15510
+ mkdirSync2(STATE_DIR, { recursive: true });
15465
15511
  const rawTarget = `${target}.raw-${Date.now()}.mp4`;
15466
15512
  rmSync2(RECORD_ACTIONS_FILE, { force: true });
15467
15513
  const child = spawn3("ffmpeg", [
@@ -15506,7 +15552,7 @@ async function computerRecordStartCommand(path6, options) {
15506
15552
  async function computerRecordStopCommand() {
15507
15553
  if (!existsSync2(RECORD_PID_FILE) && !existsSync2(RECORD_PATH_FILE)) fail("no recording in progress");
15508
15554
  if (existsSync2(RECORD_PID_FILE)) {
15509
- const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
15555
+ const pid = parseInt(readFileSync2(RECORD_PID_FILE, "utf8").trim(), 10);
15510
15556
  if (!Number.isFinite(pid)) fail("invalid recording pidfile");
15511
15557
  try {
15512
15558
  process.kill(pid, "SIGINT");
@@ -15523,9 +15569,9 @@ async function computerRecordStopCommand() {
15523
15569
  rmSync2(RECORD_PID_FILE, { force: true });
15524
15570
  }
15525
15571
  if (existsSync2(RECORD_PATH_FILE)) {
15526
- const target = readFileSync(RECORD_PATH_FILE, "utf8").trim();
15527
- const rawPath = existsSync2(RECORD_RAW_PATH_FILE) ? readFileSync(RECORD_RAW_PATH_FILE, "utf8").trim() : target;
15528
- const fps = existsSync2(RECORD_FPS_FILE) ? parseInt(readFileSync(RECORD_FPS_FILE, "utf8").trim(), 10) : 60;
15572
+ const target = readFileSync2(RECORD_PATH_FILE, "utf8").trim();
15573
+ const rawPath = existsSync2(RECORD_RAW_PATH_FILE) ? readFileSync2(RECORD_RAW_PATH_FILE, "utf8").trim() : target;
15574
+ const fps = existsSync2(RECORD_FPS_FILE) ? parseInt(readFileSync2(RECORD_FPS_FILE, "utf8").trim(), 10) : 60;
15529
15575
  const size = readRecordingDimensions();
15530
15576
  const actions = readRecordingActions();
15531
15577
  if (rawPath !== target) {
@@ -15543,15 +15589,6 @@ async function computerRecordStopCommand() {
15543
15589
  }
15544
15590
 
15545
15591
  // src/commands/computer/index.ts
15546
- function desktopBridgeStatus() {
15547
- const r = spawnSync3("bash", [SERVICES_SCRIPT, "--status-json"], { stdio: "pipe" });
15548
- if (r.status !== 0) return null;
15549
- try {
15550
- return JSON.parse(r.stdout.toString());
15551
- } catch {
15552
- return null;
15553
- }
15554
- }
15555
15592
  function bridgeStatus(details, includeBacklog = false, rootsOnly = false) {
15556
15593
  const pids = rootsOnly ? details.rootPids ?? [] : details.listenerPids ?? [];
15557
15594
  const listenerCount = rootsOnly ? details.rootListeners ?? pids.length : details.listeners ?? pids.length;
@@ -15591,7 +15628,7 @@ async function computerInfoCommand() {
15591
15628
  }
15592
15629
  async function computerStatusCommand() {
15593
15630
  ensureServicesRunning();
15594
- const bridge = desktopBridgeStatus();
15631
+ const bridge = getDesktopBridgeStatus();
15595
15632
  const procs = ["Xvfb", "openbox", "tint2", "x11vnc", "websockify"];
15596
15633
  for (const p of procs) {
15597
15634
  const r = spawnSync3("pgrep", ["-af", p], { stdio: "pipe" });
@@ -15631,7 +15668,7 @@ function loadBrandSvg(canvasW, canvasH) {
15631
15668
  `Brand wallpaper SVG missing at ${path6}. The workspace image is out of date \u2014 \`desktop/brand-wallpaper.svg\` must be installed at $REPLICAS_DESKTOP_TEMPLATES.`
15632
15669
  );
15633
15670
  }
15634
- return readFileSync2(path6, "utf8").replace(/(<svg[^>]*\s)width="\d+"/, `$1width="${canvasW}"`).replace(/(<svg[^>]*\s)height="\d+"/, `$1height="${canvasH}"`);
15671
+ return readFileSync3(path6, "utf8").replace(/(<svg[^>]*\s)width="\d+"/, `$1width="${canvasW}"`).replace(/(<svg[^>]*\s)height="\d+"/, `$1height="${canvasH}"`);
15635
15672
  }
15636
15673
  var BRAND_PAD_FRACTION = 0.06;
15637
15674
  var SCREENSHOT_CORNER_FRACTION = 0.022;
@@ -15694,7 +15731,7 @@ function overlayGrid(rawPath, target, width, height, gridSize, gridPath) {
15694
15731
  }
15695
15732
  async function computerScreenshotCommand(path6, options = {}) {
15696
15733
  const target = resolvePath(path6);
15697
- mkdirSync2(dirname2(target), { recursive: true });
15734
+ mkdirSync3(dirname3(target), { recursive: true });
15698
15735
  const stamp = `${process.pid}-${Date.now()}`;
15699
15736
  const rawPath = `/tmp/replicas-screenshot-${stamp}.raw.png`;
15700
15737
  const svgPath = `/tmp/replicas-screenshot-${stamp}.brand.svg`;
@@ -15773,7 +15810,7 @@ async function computerScreenshotCommand(path6, options = {}) {
15773
15810
  console.log(target);
15774
15811
  }
15775
15812
  function hashFile(path6) {
15776
- return createHash2("sha256").update(readFileSync2(path6)).digest("hex");
15813
+ return createHash2("sha256").update(readFileSync3(path6)).digest("hex");
15777
15814
  }
15778
15815
  async function captureStableRawScreenshot(target, options) {
15779
15816
  const start = Date.now();
@@ -15824,7 +15861,7 @@ function recordingMousePosition() {
15824
15861
  }
15825
15862
  async function computerObserveCommand(path6, options = {}) {
15826
15863
  const target = resolvePath(path6);
15827
- mkdirSync2(dirname2(target), { recursive: true });
15864
+ mkdirSync3(dirname3(target), { recursive: true });
15828
15865
  const timeoutMs = options.timeout ? parseCoord(options.timeout, "--timeout") : 3e3;
15829
15866
  const stableMs = options.stableMs ? parseCoord(options.stableMs, "--stable-ms") : 600;
15830
15867
  const pollMs = options.pollMs ? parseCoord(options.pollMs, "--poll-ms") : 200;
@@ -15835,6 +15872,7 @@ async function computerObserveCommand(path6, options = {}) {
15835
15872
  const rawPath = `/tmp/replicas-observe-${stamp}.raw.png`;
15836
15873
  const gridPath = `/tmp/replicas-observe-${stamp}.grid.svg`;
15837
15874
  try {
15875
+ ensureServicesRunning();
15838
15876
  const capture = await captureStableRawScreenshot(rawPath, { timeoutMs, stableMs, pollMs });
15839
15877
  const gridSize = options.raw ? null : parseGridSize(options.grid ?? true);
15840
15878
  if (gridSize === null) {
@@ -16340,7 +16378,7 @@ async function computerClickCommand(xStr, yStr, options) {
16340
16378
  args.push("keyup", mod);
16341
16379
  }
16342
16380
  }
16343
- runDisplayCmd("xdotool", args);
16381
+ runDesktopInputCmd(args);
16344
16382
  logRecordingAction({ type: "click", x, y });
16345
16383
  console.log(`clicked ${button === "1" ? "left" : button === "2" ? "middle" : button === "3" ? "right" : `button ${button}`} at (${x},${y})${options.double ? " x2" : ""}`);
16346
16384
  }
@@ -16348,19 +16386,19 @@ async function computerMoveCommand(xStr, yStr) {
16348
16386
  const dimensions = getDisplayDimensions();
16349
16387
  const x = parseScreenCoord(xStr, "x", dimensions.width);
16350
16388
  const y = parseScreenCoord(yStr, "y", dimensions.height);
16351
- runDisplayCmd("xdotool", ["mousemove", "--sync", String(x), String(y)]);
16389
+ runDesktopInputCmd(["mousemove", "--sync", String(x), String(y)]);
16352
16390
  logRecordingAction({ type: "move", x, y });
16353
16391
  console.log(`moved to (${x},${y})`);
16354
16392
  }
16355
16393
  async function computerTypeCommand(text, options) {
16356
16394
  const delay = options.delay ? parseCoord(options.delay, "--delay") : 12;
16357
- runDisplayCmd("xdotool", ["type", "--delay", String(delay), "--", text]);
16395
+ runDesktopInputCmd(["type", "--delay", String(delay), "--", text]);
16358
16396
  const position = recordingMousePosition();
16359
16397
  logRecordingAction({ type: "type", ...position ?? {} });
16360
16398
  console.log(`typed ${text.length} char${text.length === 1 ? "" : "s"}`);
16361
16399
  }
16362
16400
  async function computerKeyCommand(combo) {
16363
- runDisplayCmd("xdotool", ["key", "--", combo]);
16401
+ runDesktopInputCmd(["key", "--", combo]);
16364
16402
  const position = recordingMousePosition();
16365
16403
  logRecordingAction({ type: "key", ...position ?? {} });
16366
16404
  console.log(`pressed ${combo}`);
@@ -16387,7 +16425,7 @@ async function computerScrollCommand(direction, options) {
16387
16425
  );
16388
16426
  }
16389
16427
  args.push("click", "--repeat", String(amount), "--delay", "30", button);
16390
- runDisplayCmd("xdotool", args);
16428
+ runDesktopInputCmd(args);
16391
16429
  const position = hoverPosition ?? recordingMousePosition();
16392
16430
  logRecordingAction({ type: "scroll", ...position ?? {} });
16393
16431
  console.log(`scrolled ${dir} x${amount}`);
@@ -16414,7 +16452,7 @@ async function computerDragCommand(fx, fy, tx, ty) {
16414
16452
  args.push("mousemove", "--sync", String(x), String(y), "sleep", "0.018");
16415
16453
  }
16416
16454
  args.push("mouseup", "1");
16417
- runDisplayCmd("xdotool", args);
16455
+ runDesktopInputCmd(args);
16418
16456
  logRecordingAction({ type: "drag", x: fromX, y: fromY, toX, toY });
16419
16457
  console.log(`dragged (${fromX},${fromY}) -> (${toX},${toY})`);
16420
16458
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-cli",
3
- "version": "0.2.349",
3
+ "version": "0.2.350",
4
4
  "description": "CLI for managing Replicas workspaces - SSH into cloud dev environments with automatic port forwarding",
5
5
  "main": "dist/index.mjs",
6
6
  "bin": {