replicas-cli 0.3.284 → 0.3.286

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.cjs +184 -13
  2. package/package.json +4 -2
package/dist/index.cjs CHANGED
@@ -15880,7 +15880,7 @@ var init_sandbox = __esm({
15880
15880
  });
15881
15881
 
15882
15882
  // ../shared/src/pricing.ts
15883
- var TRIAL_CREDIT_CENTS, SCOUT_SOURCE, WORKSPACE_SOURCES, CLIENT_MESSAGE_SOURCES, MESSAGE_SOURCES, AUTOMATED_SOURCES, AUTOMATED_SOURCES_SQL, PLANS, TEAM_PLAN, ENTERPRISE_PLAN;
15883
+ var TRIAL_CREDIT_CENTS, SCOUT_SOURCE, WORKSPACE_SOURCES, CLIENT_MESSAGE_SOURCES, MESSAGE_SOURCES, AUTOMATED_SOURCES, AUTOMATED_SOURCES_SQL, EGRESS_MICRODOLLARS_PER_GB, EGRESS_RATE_LABEL, PLANS, TEAM_PLAN, ENTERPRISE_PLAN;
15884
15884
  var init_pricing = __esm({
15885
15885
  "../shared/src/pricing.ts"() {
15886
15886
  "use strict";
@@ -15892,6 +15892,8 @@ var init_pricing = __esm({
15892
15892
  MESSAGE_SOURCES = [...CLIENT_MESSAGE_SOURCES, ...WORKSPACE_SOURCES];
15893
15893
  AUTOMATED_SOURCES = ["api", "automation"];
15894
15894
  AUTOMATED_SOURCES_SQL = AUTOMATED_SOURCES.map((s) => `'${s}'`).join(", ");
15895
+ EGRESS_MICRODOLLARS_PER_GB = 15e4;
15896
+ EGRESS_RATE_LABEL = `$${(EGRESS_MICRODOLLARS_PER_GB / 1e6).toFixed(2)}/GB`;
15895
15897
  PLANS = {
15896
15898
  trial: {
15897
15899
  id: "trial",
@@ -17208,7 +17210,7 @@ var init_workspace_identity = __esm({
17208
17210
  });
17209
17211
 
17210
17212
  // ../shared/src/routes/environment.ts
17211
- var ENVIRONMENT_MCP_TRANSPORTS, stringMap, createEnvironmentMcpRequestSchema, updateEnvironmentMcpRequestSchema, screenPasswordSchema, createEnvironmentDeviceRequestSchema, updateEnvironmentDeviceRequestSchema, provisionedDeviceSchema;
17213
+ var ENVIRONMENT_MCP_TRANSPORTS, stringMap, createEnvironmentMcpRequestSchema, updateEnvironmentMcpRequestSchema, screenPasswordSchema, hostSchema, createEnvironmentDeviceRequestSchema, updateEnvironmentDeviceRequestSchema, deviceConnectionResponseSchema, deviceHostKeysRequestSchema, DEVICE_CONNECT_COMMAND, provisionedDeviceSchema;
17212
17214
  var init_environment = __esm({
17213
17215
  "../shared/src/routes/environment.ts"() {
17214
17216
  "use strict";
@@ -17227,19 +17229,30 @@ var init_environment = __esm({
17227
17229
  });
17228
17230
  updateEnvironmentMcpRequestSchema = createEnvironmentMcpRequestSchema.partial();
17229
17231
  screenPasswordSchema = external_exports.string().min(1, "Enter the screen password").max(256).describe('Screen Sharing password, for `replicas computer screen`. On a Mac, the "VNC viewers may control screen with password" password. Never returned.');
17232
+ hostSchema = external_exports.string().max(253).regex(/^(?:[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?|[0-9A-Fa-f:]+)$/, "Enter a hostname or IP address");
17230
17233
  createEnvironmentDeviceRequestSchema = external_exports.object({
17231
17234
  name: external_exports.string().regex(/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/, "Use lowercase letters, numbers and dashes").describe("SSH alias in workspaces, e.g. `mac-mini` for `ssh mac-mini`."),
17232
- host: external_exports.string().max(253).regex(/^(?:[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?|[0-9A-Fa-f:]+)$/, "Enter a hostname or IP address").describe("Hostname or IP, such as a Tailscale MagicDNS name (`mac-mini`) or a public address."),
17233
- port: external_exports.number().int().min(1).max(65535).optional().describe("SSH port. Defaults to 22."),
17235
+ host: hostSchema.optional().describe("Hostname or IP, such as a Tailscale MagicDNS name (`mac-mini`) or a public address. Leave it out when the device connects to Replicas itself with `replicas device connect`."),
17236
+ port: external_exports.number().int().min(1).max(65535).optional().describe("SSH port on the device. Defaults to 22."),
17234
17237
  username: external_exports.string().regex(/^[A-Za-z0-9_][A-Za-z0-9._-]{0,63}$/, "Enter the account name on the device").describe("Account the agent signs in as."),
17235
17238
  screen_password: screenPasswordSchema.optional()
17236
17239
  });
17237
17240
  updateEnvironmentDeviceRequestSchema = createEnvironmentDeviceRequestSchema.partial().extend({
17241
+ host: external_exports.union([hostSchema, external_exports.literal("")]).optional().describe("New hostname or IP, or an empty string for a device that connects to Replicas itself."),
17238
17242
  screen_password: external_exports.union([screenPasswordSchema, external_exports.literal("")]).optional().describe("New Screen Sharing password, or an empty string to remove it.")
17239
17243
  });
17244
+ deviceConnectionResponseSchema = external_exports.object({
17245
+ name: external_exports.string(),
17246
+ port: external_exports.number(),
17247
+ workspace_ids: external_exports.array(external_exports.string())
17248
+ });
17249
+ deviceHostKeysRequestSchema = external_exports.object({
17250
+ host_keys: external_exports.array(external_exports.string().max(2048).regex(/^[a-z0-9@.-]+ [A-Za-z0-9+/]+={0,3}$/, "Each host key must be `<type> <base64>`")).min(1).max(8).transform((keys) => [...new Set(keys)].sort())
17251
+ });
17252
+ DEVICE_CONNECT_COMMAND = "npx -y replicas-cli device connect";
17240
17253
  provisionedDeviceSchema = external_exports.object({
17241
17254
  name: external_exports.string(),
17242
- host: external_exports.string(),
17255
+ host: external_exports.string().nullable(),
17243
17256
  port: external_exports.number(),
17244
17257
  username: external_exports.string(),
17245
17258
  private_key: external_exports.string(),
@@ -31704,7 +31717,7 @@ var CLI_VERSION;
31704
31717
  var init_cli_version = __esm({
31705
31718
  "../shared/src/cli-version.ts"() {
31706
31719
  "use strict";
31707
- CLI_VERSION = "0.3.284";
31720
+ CLI_VERSION = "0.3.286";
31708
31721
  }
31709
31722
  });
31710
31723
 
@@ -42288,6 +42301,29 @@ var init_variable_requests = __esm({
42288
42301
  }
42289
42302
  });
42290
42303
 
42304
+ // ../shared/src/websocket-liveness.ts
42305
+ function trackWebSocketLiveness(socket) {
42306
+ let answered = true;
42307
+ for (const event of ["pong", "message"]) socket.on(event, () => {
42308
+ answered = true;
42309
+ });
42310
+ return () => {
42311
+ if (socket.isPaused) {
42312
+ answered = true;
42313
+ return true;
42314
+ }
42315
+ if (!answered) return false;
42316
+ answered = false;
42317
+ socket.ping();
42318
+ return true;
42319
+ };
42320
+ }
42321
+ var init_websocket_liveness = __esm({
42322
+ "../shared/src/websocket-liveness.ts"() {
42323
+ "use strict";
42324
+ }
42325
+ });
42326
+
42291
42327
  // ../shared/src/index.ts
42292
42328
  var init_src = __esm({
42293
42329
  "../shared/src/index.ts"() {
@@ -42465,6 +42501,7 @@ var init_src = __esm({
42465
42501
  init_environment_setups();
42466
42502
  init_variable_requests();
42467
42503
  init_api_casing();
42504
+ init_websocket_liveness();
42468
42505
  }
42469
42506
  });
42470
42507
 
@@ -45428,16 +45465,24 @@ async function envDevicesListCommand(env) {
45428
45465
  `/v1/environments/${await resolveEnvironmentId(env)}/devices`
45429
45466
  );
45430
45467
  output(response, ({ environment_devices }) => {
45431
- for (const device of environment_devices) console.log(`${device.id} ${device.name} ${device.username}@${device.host}:${device.port}`);
45468
+ for (const device of environment_devices) {
45469
+ console.log(`${device.id} ${device.name} ${device.host === null ? `${device.username}, connects from the device` : `${device.username}@${device.host}:${device.port}`}`);
45470
+ }
45432
45471
  });
45433
45472
  }
45434
45473
  async function envDevicesAddCommand(env, name, options) {
45435
45474
  const id = await resolveEnvironmentId(env);
45436
45475
  const body = { name, host: options.host, username: options.user, port: options.port, screen_password: options.screenPassword };
45437
45476
  const response = await orgAuthenticatedFetch(`/v1/environments/${id}/devices`, { method: "POST", body });
45438
- output(response, ({ environment_device }) => {
45477
+ const connect2 = options.host === void 0 ? await orgAuthenticatedFetch(`/v1/environments/${id}/devices/${response.environment_device.id}/connect-token`) : void 0;
45478
+ output({ ...response, connect_token: connect2?.token }, ({ environment_device, connect_token }) => {
45439
45479
  console.log(import_chalk20.default.green(`Added device ${name}. Allow its key by running this on the device as ${options.user}:`));
45440
45480
  console.log(`mkdir -p ~/.ssh && echo '${environment_device.public_key}' >> ~/.ssh/authorized_keys`);
45481
+ if (connect_token) {
45482
+ console.log(import_chalk20.default.green("Then keep this running on it, so workspaces can reach it, and paste this token when it asks:"));
45483
+ console.log(DEVICE_CONNECT_COMMAND);
45484
+ console.log(connect_token);
45485
+ }
45441
45486
  });
45442
45487
  }
45443
45488
  async function envDevicesRemoveCommand(env, nameOrId) {
@@ -45448,6 +45493,18 @@ async function envDevicesRemoveCommand(env, nameOrId) {
45448
45493
  await orgAuthenticatedFetch(`/v1/environments/${id}/devices/${device.id}`, { method: "DELETE" });
45449
45494
  outputSuccess(`Removed device ${device.name}`);
45450
45495
  }
45496
+ async function envDevicesResetCommand(env, nameOrId) {
45497
+ const id = await resolveEnvironmentId(env);
45498
+ const { environment_devices } = await orgAuthenticatedFetch(`/v1/environments/${id}/devices`);
45499
+ const device = resolveByNameOrId(nameOrId, environment_devices);
45500
+ if (!device) throw new Error(`Device not found: ${nameOrId}`);
45501
+ const response = await orgAuthenticatedFetch(`/v1/environments/${id}/devices/${device.id}/connect-token`, { method: "POST" });
45502
+ output(response, ({ token }) => {
45503
+ console.log(import_chalk20.default.green(`Reset ${device.name}'s connection. Its old token no longer works; run this on it again and paste the new token when it asks:`));
45504
+ console.log(DEVICE_CONNECT_COMMAND);
45505
+ console.log(token);
45506
+ });
45507
+ }
45451
45508
  function envWarmPoolCommand(enabled) {
45452
45509
  return async (env) => {
45453
45510
  const id = await resolveEnvironmentId(env);
@@ -48592,6 +48649,116 @@ trim "$1"`;
48592
48649
  }
48593
48650
  });
48594
48651
 
48652
+ // src/commands/device.ts
48653
+ async function refuse(response) {
48654
+ const body = await response.json().catch(() => null);
48655
+ throw new Error(body && typeof body === "object" && "error" in body ? String(body.error) : `Replicas refused this device (${response.status})`);
48656
+ }
48657
+ async function scanHostKeys(port) {
48658
+ const { stdout } = await (0, import_node_util.promisify)(import_node_child_process4.execFile)("ssh-keyscan", ["-T", "5", "-p", String(port), "127.0.0.1"]).catch(() => ({ stdout: "" }));
48659
+ return stdout.split("\n").filter((line) => line && !line.startsWith("#")).map((line) => line.split(" ").slice(1, 3).join(" "));
48660
+ }
48661
+ async function deviceConnectCommand() {
48662
+ const token = process.env.REPLICAS_DEVICE_TOKEN ?? (isInteractive() ? (await ask({ type: "password", name: "token", message: "Device token, from the device in the environment's Devices settings" })).token : void 0);
48663
+ if (!token) throw new Error("Paste the device token when asked, or set REPLICAS_DEVICE_TOKEN to it. It's on the device in the environment's Devices settings.");
48664
+ const headers = { Authorization: `Bearer ${token}` };
48665
+ let pinned = false;
48666
+ const waiting = /* @__PURE__ */ new Map();
48667
+ let port = 22;
48668
+ let summary = "";
48669
+ const wait = (workspaceId, failures = 0) => {
48670
+ if (!waiting.has(workspaceId)) return;
48671
+ const startedAt = Date.now();
48672
+ let opened = false;
48673
+ const socket = new import_ws.default(`${MONOLITH_URL.replace(/^http/, "ws")}/v1/devices/workspaces/${workspaceId}/ws`, { headers, handshakeTimeout: HEARTBEAT_MS });
48674
+ waiting.set(workspaceId, socket);
48675
+ const alive = trackWebSocketLiveness(socket);
48676
+ let heartbeat;
48677
+ socket.once("open", () => {
48678
+ heartbeat = setInterval(() => {
48679
+ if (!alive()) socket.terminate();
48680
+ }, HEARTBEAT_MS);
48681
+ });
48682
+ socket.once("message", (data, isBinary) => {
48683
+ if (isBinary || data.toString() !== "open") return socket.close();
48684
+ opened = true;
48685
+ wait(workspaceId);
48686
+ const sshd = (0, import_node_net.connect)(port, "127.0.0.1");
48687
+ const stream = (0, import_ws.createWebSocketStream)(socket);
48688
+ const close = () => {
48689
+ sshd.destroy();
48690
+ stream.destroy();
48691
+ };
48692
+ sshd.once("error", (error51) => {
48693
+ console.log(import_chalk32.default.yellow(`A workspace couldn't reach SSH on port ${port} here (${error51.message}). ${REMOTE_LOGIN_HINT}`));
48694
+ });
48695
+ sshd.on("error", close).on("close", close);
48696
+ stream.on("error", close).on("close", close);
48697
+ sshd.pipe(stream).pipe(sshd);
48698
+ });
48699
+ socket.on("error", () => void 0);
48700
+ socket.once("close", () => {
48701
+ clearInterval(heartbeat);
48702
+ if (opened) return;
48703
+ const failed = Date.now() - startedAt < POLL_INTERVAL_MS;
48704
+ const delay = failed ? Math.min(5e3 * 2 ** failures, MAX_RETRY_DELAY_MS) : 0;
48705
+ setTimeout(() => {
48706
+ if (waiting.get(workspaceId) === socket) wait(workspaceId, failed ? failures + 1 : 0);
48707
+ }, delay).unref();
48708
+ });
48709
+ };
48710
+ for (; ; ) {
48711
+ const response = await fetch(`${MONOLITH_URL}/v1/devices/workspaces`, { headers }).catch(() => null);
48712
+ if (response && [401, 403, 409].includes(response.status)) await refuse(response);
48713
+ const device = response?.ok ? deviceConnectionResponseSchema.parse(await response.json()) : void 0;
48714
+ let problem = device ? void 0 : `Can't reach Replicas (${response?.status ?? "network error"}), retrying...`;
48715
+ if (device && !pinned) {
48716
+ const body = { host_keys: await scanHostKeys(device.port) };
48717
+ const pin = body.host_keys.length === 0 ? void 0 : await fetch(`${MONOLITH_URL}/v1/devices/host-keys`, { method: "PUT", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify(body) }).catch(() => null);
48718
+ if (pin && [400, 401, 403, 409].includes(pin.status)) await refuse(pin);
48719
+ pinned = pin?.ok === true;
48720
+ if (pin === void 0) problem = `Can't reach SSH on port ${device.port} here, retrying... ${REMOTE_LOGIN_HINT}`;
48721
+ else if (!pinned) problem = `Can't reach Replicas (${pin?.status ?? "network error"}), retrying...`;
48722
+ }
48723
+ if (device && pinned) {
48724
+ port = device.port;
48725
+ for (const [workspaceId, socket] of waiting) {
48726
+ if (device.workspace_ids.includes(workspaceId)) continue;
48727
+ waiting.delete(workspaceId);
48728
+ socket?.close();
48729
+ }
48730
+ for (const workspaceId of device.workspace_ids) {
48731
+ if (waiting.has(workspaceId)) continue;
48732
+ waiting.set(workspaceId, void 0);
48733
+ wait(workspaceId);
48734
+ }
48735
+ }
48736
+ const next = problem ?? `\u2713 ${device?.name}: ready for ${waiting.size} ${waiting.size === 1 ? "workspace" : "workspaces"}, forwarding to SSH on port ${port}`;
48737
+ if (next !== summary) console.log(problem ? import_chalk32.default.yellow(next) : import_chalk32.default.green(next));
48738
+ summary = next;
48739
+ await (0, import_promises2.setTimeout)(POLL_INTERVAL_MS);
48740
+ }
48741
+ }
48742
+ var import_chalk32, import_node_child_process4, import_node_net, import_promises2, import_node_util, import_ws, POLL_INTERVAL_MS, HEARTBEAT_MS, MAX_RETRY_DELAY_MS, REMOTE_LOGIN_HINT;
48743
+ var init_device = __esm({
48744
+ "src/commands/device.ts"() {
48745
+ "use strict";
48746
+ import_chalk32 = __toESM(require("chalk"));
48747
+ import_node_child_process4 = require("child_process");
48748
+ import_node_net = require("net");
48749
+ import_promises2 = require("timers/promises");
48750
+ import_node_util = require("util");
48751
+ import_ws = __toESM(require("ws"));
48752
+ init_src();
48753
+ init_monolith_url();
48754
+ init_output();
48755
+ POLL_INTERVAL_MS = 1e4;
48756
+ HEARTBEAT_MS = 3e4;
48757
+ MAX_RETRY_DELAY_MS = 5 * 6e4;
48758
+ REMOTE_LOGIN_HINT = "On a Mac, turn on Remote Login in System Settings \u2192 General \u2192 Sharing.";
48759
+ }
48760
+ });
48761
+
48595
48762
  // src/index.ts
48596
48763
  var src_exports = {};
48597
48764
  function parseBooleanOption(value) {
@@ -48663,6 +48830,7 @@ var init_src2 = __esm({
48663
48830
  init_slack4();
48664
48831
  init_service();
48665
48832
  init_environment3();
48833
+ init_device();
48666
48834
  init_config();
48667
48835
  init_organization2();
48668
48836
  init_output();
@@ -48680,6 +48848,7 @@ var init_src2 = __esm({
48680
48848
  "codex-auth",
48681
48849
  "muse-auth",
48682
48850
  "config",
48851
+ "device",
48683
48852
  "init",
48684
48853
  "login",
48685
48854
  "logout",
@@ -48858,12 +49027,14 @@ var init_src2 = __esm({
48858
49027
  envMcps.command("remove <env> <name-or-id>").description("Remove an MCP server").action(run(envMcpsRemoveCommand));
48859
49028
  envDevices = environment.command("devices", { hidden: true }).description("Computers the agent reaches over SSH, like a Mac mini or your own laptop");
48860
49029
  envDevices.command("list <env>").description("List devices").action(run(envDevicesListCommand));
48861
- envDevices.command("add <env> <name>").description("Add a device and print the command that allows its key on the device").requiredOption("--host <host>", "Hostname or IP, such as its Tailscale name").requiredOption("--user <user>", "Account the agent signs in as").option("--port <port>", "SSH port (default 22)", (value) => {
49030
+ envDevices.command("add <env> <name>").description("Add a device and print the command that allows its key on the device").option("--host <host>", "Hostname or IP, such as its Tailscale name. Leave it out to connect from the device instead").requiredOption("--user <user>", "Account the agent signs in as").option("--port <port>", "SSH port (default 22)", (value) => {
48862
49031
  const port = Number(value);
48863
49032
  if (!Number.isInteger(port)) throw new import_commander.InvalidArgumentError("must be a whole number");
48864
49033
  return port;
48865
49034
  }).option("--screen-password <password>", "Screen Sharing password, so the agent can see and control the screen").action(run(envDevicesAddCommand));
48866
49035
  envDevices.command("remove <env> <name-or-id>").description("Remove a device and delete its key").action(run(envDevicesRemoveCommand));
49036
+ envDevices.command("reset <env> <name-or-id>").description("Replace the connect token of a device that connects from itself and forget its SSH identity, for a leaked token or a reinstalled device").action(run(envDevicesResetCommand));
49037
+ program.command("device", { hidden: true }).description("Run on one of your computers so Replicas workspaces can reach it").command("connect").description("Keep this computer reachable from the workspaces that have it, without opening ports or a tailnet. Asks for the token from the device's settings, or reads REPLICAS_DEVICE_TOKEN").action(run(deviceConnectCommand));
48867
49038
  envWarmPool = environment.command("warm-pool").description("Keep pre-warmed workspaces ready for an environment");
48868
49039
  envWarmPool.command("enable <env>").description("Enable the warm pool").action(run(envWarmPoolCommand(true)));
48869
49040
  envWarmPool.command("disable <env>").description("Disable the warm pool").action(run(envWarmPoolCommand(false)));
@@ -48975,12 +49146,12 @@ ${VISUAL_PAGE_RULES}`).action(run(visualizeCommand));
48975
49146
  });
48976
49147
 
48977
49148
  // src/bin.ts
48978
- var import_node_child_process4 = require("child_process");
48979
- var import_chalk32 = __toESM(require("chalk"));
49149
+ var import_node_child_process5 = require("child_process");
49150
+ var import_chalk33 = __toESM(require("chalk"));
48980
49151
  init_config();
48981
49152
  if (process.argv[2] === "computer" && isAgentMode()) {
48982
- const result = (0, import_node_child_process4.spawnSync)("replicas-computer", process.argv.slice(3), { stdio: "inherit" });
48983
- if (result.error) console.error(import_chalk32.default.red(`
49153
+ const result = (0, import_node_child_process5.spawnSync)("replicas-computer", process.argv.slice(3), { stdio: "inherit" });
49154
+ if (result.error) console.error(import_chalk33.default.red(`
48984
49155
  \u2717 replicas-computer is unavailable in this workspace image
48985
49156
  `));
48986
49157
  process.exitCode = result.error ? 1 : result.status ?? 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-cli",
3
- "version": "0.3.284",
3
+ "version": "0.3.286",
4
4
  "description": "CLI for managing Replicas workspaces - SSH into cloud dev environments with automatic port forwarding",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {
@@ -47,11 +47,13 @@
47
47
  "dotenv": "^17.2.3",
48
48
  "open": "^8.4.2",
49
49
  "prompts": "^2.4.2",
50
- "tslib": "^2.6.2"
50
+ "tslib": "^2.6.2",
51
+ "ws": "8.21.0"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@replicas/shared": "*",
54
55
  "@types/node": "^20.11.0",
56
+ "@types/ws": "8.18.1",
55
57
  "tsup": "^8.5.0",
56
58
  "typescript": "^5.3.3"
57
59
  }