replicas-cli 0.2.349 → 0.2.351

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