@ricsam/r5dctl 0.0.137 → 0.0.139

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/cjs/cli.cjs CHANGED
@@ -92,6 +92,7 @@ __export(cli_exports, {
92
92
  renderEnvValues: () => renderEnvValues,
93
93
  renderProcessHistory: () => renderProcessHistory,
94
94
  renderProcessInspection: () => renderProcessInspection,
95
+ renderProcessInventory: () => renderProcessInventory,
95
96
  renderProcessList: () => renderProcessList,
96
97
  renderProcessLog: () => renderProcessLog,
97
98
  renderSessionClaimList: () => renderSessionClaimList,
@@ -1989,6 +1990,9 @@ function renderWorkspaceStatus(status) {
1989
1990
  `Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
1990
1991
  ...status.workers.flatMap((worker) => [
1991
1992
  `Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`,
1993
+ ...(worker.accountWorkspaces ?? []).map(
1994
+ (workspace) => ` Account workspace ${workspace.sessionId}: ${workspace.initialized ? "initialized" : "not initialized"}; head ${workspace.head ?? "(none)"}`
1995
+ ),
1992
1996
  ...worker.hydrationRecovery ? [
1993
1997
  ` Interrupted hydration ${worker.hydrationRecovery.transactionId}: files and snapshots preserved; execution is fenced.`,
1994
1998
  ` Evidence: ${worker.hydrationRecovery.transactionPath}`,
@@ -2037,11 +2041,13 @@ function renderBranchList(branches) {
2037
2041
  }
2038
2042
  function renderWorkspaceSync(result) {
2039
2043
  const head = result.publishedHead ?? result.localHead ?? result.startingHead ?? "(none)";
2044
+ const accountWorkspaces = result.affectedSessionIds?.length ? `
2045
+ Account workspaces changed: ${result.affectedSessionIds.join(", ")}` : "";
2040
2046
  const discarded = result.discardedPaths.length > 0 ? `
2041
2047
  Discarded paths: ${result.discardedPaths.join(", ")}` : "";
2042
2048
  const error = result.error ? `
2043
2049
  Error: ${result.error}` : "";
2044
- return `Workspace sync ${result.outcome} on ${result.workerLabel}; HEAD ${head}; ${result.diffSizeBytes} bytes.${discarded}${error}
2050
+ return `Workspace sync ${result.outcome} on ${result.workerLabel}; HEAD ${head}; ${result.diffSizeBytes} bytes.${accountWorkspaces}${discarded}${error}
2045
2051
  `;
2046
2052
  }
2047
2053
  function renderSessionDescription(session) {
@@ -2099,6 +2105,15 @@ function formatProcessLane(process2) {
2099
2105
  function renderProcessHistory(processes, nowMs = Date.now()) {
2100
2106
  return processes.length === 0 ? "No process history.\n" : renderProcessList(processes, nowMs);
2101
2107
  }
2108
+ function renderProcessInventory(inventory, history = false, nowMs = Date.now()) {
2109
+ const rows = inventory.processes.length || inventory.complete ? history ? renderProcessHistory(inventory.processes, nowMs) : renderProcessList(inventory.processes, nowMs) : `No ${history ? "process history" : "active processes"} reported by available workers.
2110
+ `;
2111
+ if (inventory.complete) return rows;
2112
+ const unavailable = inventory.unavailableWorkers.map((worker) => `${worker.workerLabel ?? "default worker"} (${worker.sessionIds.length} conversations)`).join(", ");
2113
+ return `${rows}
2114
+ Process status unavailable: ${unavailable}. These workers may still have active processes; reconnect them before inspecting or stopping their commands.
2115
+ `;
2116
+ }
2102
2117
  function renderProcessLog(log) {
2103
2118
  const sections = [];
2104
2119
  for (const [name, stream] of [
@@ -3051,13 +3066,25 @@ async function executeR5dctlCommand(client, json, args) {
3051
3066
  return;
3052
3067
  }
3053
3068
  if (first === "ps" && second === "list") {
3054
- const processes = await client.processes.list(parsePsListArgs(args.slice(2)));
3055
- write(processes, renderProcessList(processes));
3069
+ const filters = parsePsListArgs(args.slice(2));
3070
+ if (json) {
3071
+ const processes = await client.processes.list(filters);
3072
+ write(processes, renderProcessList(processes));
3073
+ } else {
3074
+ const inventory = await client.processes.inventory(filters);
3075
+ write(inventory, renderProcessInventory(inventory));
3076
+ }
3056
3077
  return;
3057
3078
  }
3058
3079
  if (first === "ps" && second === "history") {
3059
- const processes = await client.processes.history(parsePsHistoryArgs(args.slice(2)));
3060
- write(processes, renderProcessHistory(processes));
3080
+ const filters = parsePsHistoryArgs(args.slice(2));
3081
+ if (json) {
3082
+ const processes = await client.processes.history(filters);
3083
+ write(processes, renderProcessHistory(processes));
3084
+ } else {
3085
+ const inventory = await client.processes.historyInventory(filters);
3086
+ write(inventory, renderProcessInventory(inventory, true));
3087
+ }
3061
3088
  return;
3062
3089
  }
3063
3090
  if (first === "ps" && second === "inspect") {
@@ -4016,6 +4043,7 @@ async function runCommand(argv) {
4016
4043
  credential: clientOptions.token ?? clientOptions.apiKey ?? "",
4017
4044
  projectId: project.id,
4018
4045
  branchName: options.branch,
4046
+ sessionId: options.session,
4019
4047
  command: execution.command
4020
4048
  });
4021
4049
  }
@@ -4120,6 +4148,7 @@ async function main(argv = process.argv.slice(2)) {
4120
4148
  renderEnvValues,
4121
4149
  renderProcessHistory,
4122
4150
  renderProcessInspection,
4151
+ renderProcessInventory,
4123
4152
  renderProcessList,
4124
4153
  renderProcessLog,
4125
4154
  renderSessionClaimList,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.137",
3
+ "version": "0.0.139",
4
4
  "type": "commonjs"
5
5
  }
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,229 +15,122 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
  var shell_exports = {};
30
20
  __export(shell_exports, {
31
- createShellWebSocketUrl: () => createShellWebSocketUrl,
32
21
  runR5dctlShell: () => runR5dctlShell
33
22
  });
34
23
  module.exports = __toCommonJS(shell_exports);
35
- var import_ws = __toESM(require("ws"), 1);
36
- function clampTerminalSize(value, fallback) {
37
- if (!Number.isFinite(value) || (value ?? 0) <= 0) {
38
- return fallback;
39
- }
40
- return Math.max(1, Math.min(Math.floor(value), 500));
41
- }
42
- function createShellWebSocketUrl(baseUrl) {
43
- const url = new URL("/ws", baseUrl);
44
- if (url.protocol === "https:") {
45
- url.protocol = "wss:";
46
- } else if (url.protocol === "http:") {
47
- url.protocol = "ws:";
48
- } else {
49
- throw new Error(`Unsupported r5d base URL protocol: ${url.protocol}`);
50
- }
51
- return url.toString();
52
- }
53
- function decodeMessage(data) {
54
- if (typeof data === "string") {
55
- return data;
56
- }
57
- if (Buffer.isBuffer(data)) {
58
- return data.toString("utf8");
59
- }
60
- if (data instanceof ArrayBuffer) {
61
- return Buffer.from(data).toString("utf8");
62
- }
63
- if (ArrayBuffer.isView(data)) {
64
- return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
65
- }
66
- if (Array.isArray(data)) {
67
- return Buffer.concat(data).toString("utf8");
68
- }
69
- return String(data);
70
- }
24
+ var import_node_crypto = require("node:crypto");
25
+ var import_node_os = require("node:os");
26
+ const terminalSize = (value, fallback) => Number.isFinite(value) && value > 0 ? Math.min(500, Math.max(1, Math.floor(value))) : fallback;
71
27
  async function runR5dctlShell(options) {
72
- const stdin = options.stdin ?? process.stdin;
73
- const stdout = options.stdout ?? process.stdout;
74
- const signalTarget = options.signalTarget ?? process;
28
+ const stdin = options.stdin ?? process.stdin, stdout = options.stdout ?? process.stdout;
29
+ const signals = options.signalTarget ?? process;
75
30
  const interactive = options.command === void 0;
76
- if (interactive && !stdin.isTTY) {
77
- throw new Error("Interactive shell requires a TTY. Use `shell -c <command>` for non-interactive use.");
78
- }
79
- if (!options.credential) {
80
- throw new Error("Authentication required. Run `r5dctl auth login` or provide a token or API key.");
81
- }
82
- const createWebSocket = options.createWebSocket ?? ((url, clientOptions) => new import_ws.default(url, clientOptions));
83
- const socket = createWebSocket(createShellWebSocketUrl(options.baseUrl), {
84
- headers: {
85
- Authorization: `Bearer ${options.credential}`
31
+ if (interactive && !stdin.isTTY) throw new Error("Interactive shell requires a TTY. Use `shell -c <command>` for non-interactive use.");
32
+ if (!options.credential) throw new Error("Authentication required. Run `r5dctl auth login` or provide a token or API key.");
33
+ const url = new URL(
34
+ `/api/product/projects/${encodeURIComponent(options.projectId)}/branches/${encodeURIComponent(options.branchName)}/terminal`,
35
+ options.baseUrl
36
+ );
37
+ if (!["http:", "https:"].includes(url.protocol)) throw new Error("Unsupported r5d base URL protocol");
38
+ const performFetch = options.fetch ?? fetch;
39
+ async function request(body, query = "") {
40
+ const serialized = body === void 0 ? void 0 : JSON.stringify(body);
41
+ for (let attempt = 0; ; attempt++) {
42
+ let response;
43
+ try {
44
+ response = await performFetch(url + query, {
45
+ method: body === void 0 ? "GET" : "POST",
46
+ redirect: "error",
47
+ headers: {
48
+ Authorization: `Bearer ${options.credential}`,
49
+ "Content-Type": "application/json",
50
+ ...options.sessionId ? { "x-r5d-session-id": options.sessionId } : {}
51
+ },
52
+ body: serialized
53
+ });
54
+ } catch (error) {
55
+ if (attempt >= 3) throw error;
56
+ await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
57
+ continue;
58
+ }
59
+ if (response.status >= 500 && attempt < 3) {
60
+ await response.body?.cancel();
61
+ await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
62
+ continue;
63
+ }
64
+ const result = await response.json();
65
+ if (!response.ok) throw new Error(result.error ?? `Terminal request failed (${response.status})`);
66
+ return result;
86
67
  }
68
+ }
69
+ const opened = await request({
70
+ method: "open",
71
+ id: (0, import_node_crypto.randomUUID)(),
72
+ cols: terminalSize(stdout.columns, 80),
73
+ rows: terminalSize(stdout.rows, 24),
74
+ ...options.command === void 0 ? {} : { command: options.command }
87
75
  });
88
- return await new Promise((resolve, reject) => {
89
- let ptyId;
90
- let openRequested = false;
91
- let settled = false;
92
- let inputAttached = false;
93
- const priorRawMode = stdin.isRaw ?? false;
94
- const send = (message) => {
95
- if (socket.readyState !== import_ws.default.OPEN) {
96
- return;
97
- }
98
- socket.send(JSON.stringify(message));
99
- };
100
- const sendResize = () => {
101
- if (!ptyId) {
102
- return;
103
- }
104
- send({
105
- type: "shell_pty_resize",
106
- ptyId,
107
- cols: clampTerminalSize(stdout.columns, 80),
108
- rows: clampTerminalSize(stdout.rows, 24)
109
- });
110
- };
111
- const onInput = (chunk) => {
112
- if (ptyId) {
113
- send({ type: "shell_pty_input", ptyId, data: Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk });
114
- }
115
- };
116
- const onInputEnd = () => {
117
- if (!interactive && ptyId) {
118
- send({ type: "shell_pty_input", ptyId, data: "" });
119
- }
120
- };
121
- const onSigint = () => {
122
- if (ptyId) {
123
- send({ type: "shell_pty_input", ptyId, data: "" });
124
- }
125
- };
126
- const onSigterm = () => {
127
- if (ptyId) {
128
- send({ type: "shell_pty_close", ptyId });
129
- ptyId = void 0;
130
- }
131
- finish({ exitCode: 143 });
132
- };
133
- const cleanup = () => {
134
- socket.off("message", onMessage);
135
- socket.off("error", onSocketError);
136
- socket.off("close", onSocketClose);
137
- if (inputAttached) {
138
- stdin.off("data", onInput);
139
- stdin.off("end", onInputEnd);
140
- }
141
- signalTarget.off("SIGWINCH", sendResize);
142
- signalTarget.off("SIGINT", onSigint);
143
- signalTarget.off("SIGTERM", onSigterm);
144
- if (stdin.isTTY && stdin.setRawMode) {
145
- stdin.setRawMode(priorRawMode);
146
- }
147
- };
148
- const finish = (result) => {
149
- if (settled) {
150
- return;
151
- }
152
- settled = true;
153
- cleanup();
154
- if (socket.readyState === import_ws.default.OPEN || socket.readyState === import_ws.default.CONNECTING) {
155
- socket.close(1e3, "r5dctl shell finished");
156
- }
157
- if ("error" in result) {
158
- reject(result.error);
159
- } else {
160
- resolve(result.exitCode);
161
- }
162
- };
163
- function attachInput() {
164
- if (inputAttached) {
165
- return;
166
- }
167
- inputAttached = true;
168
- if (stdin.isTTY && stdin.setRawMode) {
169
- stdin.setRawMode(true);
170
- }
171
- stdin.on("data", onInput);
172
- stdin.on("end", onInputEnd);
173
- signalTarget.on("SIGWINCH", sendResize);
174
- signalTarget.on("SIGTERM", onSigterm);
175
- if (!stdin.isTTY) {
176
- signalTarget.on("SIGINT", onSigint);
177
- }
178
- }
179
- function onMessage(data) {
180
- let message;
181
- try {
182
- const parsed = JSON.parse(decodeMessage(data));
183
- if (!parsed || typeof parsed !== "object" || !("type" in parsed)) {
184
- throw new Error("Invalid message");
76
+ if (!opened.ptyId) throw new Error("Terminal did not acknowledge its durable run identity");
77
+ const ptyId = opened.ptyId, priorRaw = stdin.isRaw ?? false;
78
+ let pending = Promise.resolve(), failure, stopped = false;
79
+ const send = (command) => {
80
+ const body = { ...command, id: (0, import_node_crypto.randomUUID)(), ptyId };
81
+ pending = pending.then(() => request(body)).catch((error) => {
82
+ failure = error;
83
+ });
84
+ };
85
+ const input = (chunk) => send({ method: "input", data: Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk });
86
+ const inputEnd = () => {
87
+ if (!interactive) send({ method: "input", data: "" });
88
+ };
89
+ const resize = () => send({ method: "resize", cols: terminalSize(stdout.columns, 80), rows: terminalSize(stdout.rows, 24) });
90
+ const interrupt = () => send({ method: "input", data: "" });
91
+ const terminate = () => {
92
+ send({ method: "close" });
93
+ stopped = true;
94
+ };
95
+ try {
96
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(true);
97
+ stdin.on("data", input);
98
+ stdin.on("end", inputEnd);
99
+ signals.on("SIGWINCH", resize);
100
+ signals.on("SIGTERM", terminate);
101
+ if (!stdin.isTTY) signals.on("SIGINT", interrupt);
102
+ let cursor = 0;
103
+ while (!stopped) {
104
+ if (failure) throw failure;
105
+ const page = await request(
106
+ void 0,
107
+ `?ptyId=${encodeURIComponent(ptyId)}&after=${cursor}`
108
+ );
109
+ if (!Number.isSafeInteger(page.nextCursor) || page.nextCursor < cursor) throw new Error("Invalid terminal output cursor");
110
+ for (const event of page.events) {
111
+ if (event.type === "output") stdout.write(event.data);
112
+ else if (event.type === "exit") {
113
+ await pending;
114
+ if (failure) throw failure;
115
+ return event.exitCode ?? (event.signal ? 128 + (typeof event.signal === "number" ? event.signal : import_node_os.constants.signals[event.signal] ?? 0) : 1);
185
116
  }
186
- message = parsed;
187
- } catch {
188
- finish({ error: new Error("Received an invalid response from r5d.dev") });
189
- return;
190
- }
191
- if (message.type === "connected") {
192
- if (!openRequested) {
193
- openRequested = true;
194
- send({
195
- type: "shell_pty_open",
196
- projectId: options.projectId,
197
- branchName: options.branchName,
198
- cols: clampTerminalSize(stdout.columns, 80),
199
- rows: clampTerminalSize(stdout.rows, 24),
200
- ...options.command === void 0 ? {} : { command: options.command }
201
- });
202
- }
203
- return;
204
- }
205
- if (message.type === "shell_pty_opened") {
206
- ptyId = message.ptyId;
207
- attachInput();
208
- return;
209
- }
210
- if (message.type === "shell_pty_output") {
211
- if (message.ptyId === ptyId) {
212
- stdout.write(message.data);
213
- }
214
- return;
215
- }
216
- if (message.type === "shell_pty_exit") {
217
- if (message.ptyId === ptyId) {
218
- ptyId = void 0;
219
- finish({ exitCode: message.exitCode });
220
- }
221
- return;
222
- }
223
- if (message.type === "shell_pty_error" && (!message.ptyId || message.ptyId === ptyId)) {
224
- finish({ error: new Error(message.message) });
225
117
  }
118
+ cursor = page.nextCursor;
119
+ await new Promise((resolve) => setTimeout(resolve, options.pollIntervalMs ?? 150));
226
120
  }
227
- function onSocketError(error) {
228
- finish({ error: new Error(`r5d.dev shell connection failed: ${error.message}`) });
229
- }
230
- function onSocketClose(code, reason) {
231
- if (!settled) {
232
- const suffix = reason.length > 0 ? `: ${reason.toString("utf8")}` : "";
233
- finish({ error: new Error(`r5d.dev shell connection closed (${code})${suffix}`) });
234
- }
235
- }
236
- socket.on("message", onMessage);
237
- socket.on("error", onSocketError);
238
- socket.on("close", onSocketClose);
239
- });
121
+ await pending;
122
+ if (failure) throw failure;
123
+ return 143;
124
+ } finally {
125
+ stdin.off("data", input);
126
+ stdin.off("end", inputEnd);
127
+ signals.off("SIGWINCH", resize);
128
+ signals.off("SIGTERM", terminate);
129
+ signals.off("SIGINT", interrupt);
130
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(priorRaw);
131
+ }
240
132
  }
241
133
  // Annotate the CommonJS export names for ESM import in node:
242
134
  0 && (module.exports = {
243
- createShellWebSocketUrl,
244
135
  runR5dctlShell
245
136
  });
package/dist/mjs/cli.mjs CHANGED
@@ -1879,6 +1879,9 @@ function renderWorkspaceStatus(status) {
1879
1879
  `Remediation: ${status.incident ? `${status.incident.kind} ${status.incident.status} (${status.incident.id}) on ${status.incident.originWorkerLabel}` : "none"}`,
1880
1880
  ...status.workers.flatMap((worker) => [
1881
1881
  `Worker ${worker.workerLabel}: ${worker.configured ? "configured" : "configuring"}; head ${worker.localHead ?? "(none)"}; pending checkouts ${worker.pendingCheckouts.length}`,
1882
+ ...(worker.accountWorkspaces ?? []).map(
1883
+ (workspace) => ` Account workspace ${workspace.sessionId}: ${workspace.initialized ? "initialized" : "not initialized"}; head ${workspace.head ?? "(none)"}`
1884
+ ),
1882
1885
  ...worker.hydrationRecovery ? [
1883
1886
  ` Interrupted hydration ${worker.hydrationRecovery.transactionId}: files and snapshots preserved; execution is fenced.`,
1884
1887
  ` Evidence: ${worker.hydrationRecovery.transactionPath}`,
@@ -1927,11 +1930,13 @@ function renderBranchList(branches) {
1927
1930
  }
1928
1931
  function renderWorkspaceSync(result) {
1929
1932
  const head = result.publishedHead ?? result.localHead ?? result.startingHead ?? "(none)";
1933
+ const accountWorkspaces = result.affectedSessionIds?.length ? `
1934
+ Account workspaces changed: ${result.affectedSessionIds.join(", ")}` : "";
1930
1935
  const discarded = result.discardedPaths.length > 0 ? `
1931
1936
  Discarded paths: ${result.discardedPaths.join(", ")}` : "";
1932
1937
  const error = result.error ? `
1933
1938
  Error: ${result.error}` : "";
1934
- return `Workspace sync ${result.outcome} on ${result.workerLabel}; HEAD ${head}; ${result.diffSizeBytes} bytes.${discarded}${error}
1939
+ return `Workspace sync ${result.outcome} on ${result.workerLabel}; HEAD ${head}; ${result.diffSizeBytes} bytes.${accountWorkspaces}${discarded}${error}
1935
1940
  `;
1936
1941
  }
1937
1942
  function renderSessionDescription(session) {
@@ -1989,6 +1994,15 @@ function formatProcessLane(process2) {
1989
1994
  function renderProcessHistory(processes, nowMs = Date.now()) {
1990
1995
  return processes.length === 0 ? "No process history.\n" : renderProcessList(processes, nowMs);
1991
1996
  }
1997
+ function renderProcessInventory(inventory, history = false, nowMs = Date.now()) {
1998
+ const rows = inventory.processes.length || inventory.complete ? history ? renderProcessHistory(inventory.processes, nowMs) : renderProcessList(inventory.processes, nowMs) : `No ${history ? "process history" : "active processes"} reported by available workers.
1999
+ `;
2000
+ if (inventory.complete) return rows;
2001
+ const unavailable = inventory.unavailableWorkers.map((worker) => `${worker.workerLabel ?? "default worker"} (${worker.sessionIds.length} conversations)`).join(", ");
2002
+ return `${rows}
2003
+ Process status unavailable: ${unavailable}. These workers may still have active processes; reconnect them before inspecting or stopping their commands.
2004
+ `;
2005
+ }
1992
2006
  function renderProcessLog(log) {
1993
2007
  const sections = [];
1994
2008
  for (const [name, stream] of [
@@ -2941,13 +2955,25 @@ async function executeR5dctlCommand(client, json, args) {
2941
2955
  return;
2942
2956
  }
2943
2957
  if (first === "ps" && second === "list") {
2944
- const processes = await client.processes.list(parsePsListArgs(args.slice(2)));
2945
- write(processes, renderProcessList(processes));
2958
+ const filters = parsePsListArgs(args.slice(2));
2959
+ if (json) {
2960
+ const processes = await client.processes.list(filters);
2961
+ write(processes, renderProcessList(processes));
2962
+ } else {
2963
+ const inventory = await client.processes.inventory(filters);
2964
+ write(inventory, renderProcessInventory(inventory));
2965
+ }
2946
2966
  return;
2947
2967
  }
2948
2968
  if (first === "ps" && second === "history") {
2949
- const processes = await client.processes.history(parsePsHistoryArgs(args.slice(2)));
2950
- write(processes, renderProcessHistory(processes));
2969
+ const filters = parsePsHistoryArgs(args.slice(2));
2970
+ if (json) {
2971
+ const processes = await client.processes.history(filters);
2972
+ write(processes, renderProcessHistory(processes));
2973
+ } else {
2974
+ const inventory = await client.processes.historyInventory(filters);
2975
+ write(inventory, renderProcessInventory(inventory, true));
2976
+ }
2951
2977
  return;
2952
2978
  }
2953
2979
  if (first === "ps" && second === "inspect") {
@@ -3906,6 +3932,7 @@ async function runCommand(argv) {
3906
3932
  credential: clientOptions.token ?? clientOptions.apiKey ?? "",
3907
3933
  projectId: project.id,
3908
3934
  branchName: options.branch,
3935
+ sessionId: options.session,
3909
3936
  command: execution.command
3910
3937
  });
3911
3938
  }
@@ -4009,6 +4036,7 @@ export {
4009
4036
  renderEnvValues,
4010
4037
  renderProcessHistory,
4011
4038
  renderProcessInspection,
4039
+ renderProcessInventory,
4012
4040
  renderProcessList,
4013
4041
  renderProcessLog,
4014
4042
  renderSessionClaimList,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.137",
3
+ "version": "0.0.139",
4
4
  "type": "module"
5
5
  }
@@ -1,210 +1,112 @@
1
- import WebSocket, {} from "ws";
2
- function clampTerminalSize(value, fallback) {
3
- if (!Number.isFinite(value) || (value ?? 0) <= 0) {
4
- return fallback;
5
- }
6
- return Math.max(1, Math.min(Math.floor(value), 500));
7
- }
8
- function createShellWebSocketUrl(baseUrl) {
9
- const url = new URL("/ws", baseUrl);
10
- if (url.protocol === "https:") {
11
- url.protocol = "wss:";
12
- } else if (url.protocol === "http:") {
13
- url.protocol = "ws:";
14
- } else {
15
- throw new Error(`Unsupported r5d base URL protocol: ${url.protocol}`);
16
- }
17
- return url.toString();
18
- }
19
- function decodeMessage(data) {
20
- if (typeof data === "string") {
21
- return data;
22
- }
23
- if (Buffer.isBuffer(data)) {
24
- return data.toString("utf8");
25
- }
26
- if (data instanceof ArrayBuffer) {
27
- return Buffer.from(data).toString("utf8");
28
- }
29
- if (ArrayBuffer.isView(data)) {
30
- return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
31
- }
32
- if (Array.isArray(data)) {
33
- return Buffer.concat(data).toString("utf8");
34
- }
35
- return String(data);
36
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { constants } from "node:os";
3
+ const terminalSize = (value, fallback) => Number.isFinite(value) && value > 0 ? Math.min(500, Math.max(1, Math.floor(value))) : fallback;
37
4
  async function runR5dctlShell(options) {
38
- const stdin = options.stdin ?? process.stdin;
39
- const stdout = options.stdout ?? process.stdout;
40
- const signalTarget = options.signalTarget ?? process;
5
+ const stdin = options.stdin ?? process.stdin, stdout = options.stdout ?? process.stdout;
6
+ const signals = options.signalTarget ?? process;
41
7
  const interactive = options.command === void 0;
42
- if (interactive && !stdin.isTTY) {
43
- throw new Error("Interactive shell requires a TTY. Use `shell -c <command>` for non-interactive use.");
44
- }
45
- if (!options.credential) {
46
- throw new Error("Authentication required. Run `r5dctl auth login` or provide a token or API key.");
47
- }
48
- const createWebSocket = options.createWebSocket ?? ((url, clientOptions) => new WebSocket(url, clientOptions));
49
- const socket = createWebSocket(createShellWebSocketUrl(options.baseUrl), {
50
- headers: {
51
- Authorization: `Bearer ${options.credential}`
8
+ if (interactive && !stdin.isTTY) throw new Error("Interactive shell requires a TTY. Use `shell -c <command>` for non-interactive use.");
9
+ if (!options.credential) throw new Error("Authentication required. Run `r5dctl auth login` or provide a token or API key.");
10
+ const url = new URL(
11
+ `/api/product/projects/${encodeURIComponent(options.projectId)}/branches/${encodeURIComponent(options.branchName)}/terminal`,
12
+ options.baseUrl
13
+ );
14
+ if (!["http:", "https:"].includes(url.protocol)) throw new Error("Unsupported r5d base URL protocol");
15
+ const performFetch = options.fetch ?? fetch;
16
+ async function request(body, query = "") {
17
+ const serialized = body === void 0 ? void 0 : JSON.stringify(body);
18
+ for (let attempt = 0; ; attempt++) {
19
+ let response;
20
+ try {
21
+ response = await performFetch(url + query, {
22
+ method: body === void 0 ? "GET" : "POST",
23
+ redirect: "error",
24
+ headers: {
25
+ Authorization: `Bearer ${options.credential}`,
26
+ "Content-Type": "application/json",
27
+ ...options.sessionId ? { "x-r5d-session-id": options.sessionId } : {}
28
+ },
29
+ body: serialized
30
+ });
31
+ } catch (error) {
32
+ if (attempt >= 3) throw error;
33
+ await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
34
+ continue;
35
+ }
36
+ if (response.status >= 500 && attempt < 3) {
37
+ await response.body?.cancel();
38
+ await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
39
+ continue;
40
+ }
41
+ const result = await response.json();
42
+ if (!response.ok) throw new Error(result.error ?? `Terminal request failed (${response.status})`);
43
+ return result;
52
44
  }
45
+ }
46
+ const opened = await request({
47
+ method: "open",
48
+ id: randomUUID(),
49
+ cols: terminalSize(stdout.columns, 80),
50
+ rows: terminalSize(stdout.rows, 24),
51
+ ...options.command === void 0 ? {} : { command: options.command }
53
52
  });
54
- return await new Promise((resolve, reject) => {
55
- let ptyId;
56
- let openRequested = false;
57
- let settled = false;
58
- let inputAttached = false;
59
- const priorRawMode = stdin.isRaw ?? false;
60
- const send = (message) => {
61
- if (socket.readyState !== WebSocket.OPEN) {
62
- return;
63
- }
64
- socket.send(JSON.stringify(message));
65
- };
66
- const sendResize = () => {
67
- if (!ptyId) {
68
- return;
69
- }
70
- send({
71
- type: "shell_pty_resize",
72
- ptyId,
73
- cols: clampTerminalSize(stdout.columns, 80),
74
- rows: clampTerminalSize(stdout.rows, 24)
75
- });
76
- };
77
- const onInput = (chunk) => {
78
- if (ptyId) {
79
- send({ type: "shell_pty_input", ptyId, data: Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk });
80
- }
81
- };
82
- const onInputEnd = () => {
83
- if (!interactive && ptyId) {
84
- send({ type: "shell_pty_input", ptyId, data: "" });
85
- }
86
- };
87
- const onSigint = () => {
88
- if (ptyId) {
89
- send({ type: "shell_pty_input", ptyId, data: "" });
90
- }
91
- };
92
- const onSigterm = () => {
93
- if (ptyId) {
94
- send({ type: "shell_pty_close", ptyId });
95
- ptyId = void 0;
96
- }
97
- finish({ exitCode: 143 });
98
- };
99
- const cleanup = () => {
100
- socket.off("message", onMessage);
101
- socket.off("error", onSocketError);
102
- socket.off("close", onSocketClose);
103
- if (inputAttached) {
104
- stdin.off("data", onInput);
105
- stdin.off("end", onInputEnd);
106
- }
107
- signalTarget.off("SIGWINCH", sendResize);
108
- signalTarget.off("SIGINT", onSigint);
109
- signalTarget.off("SIGTERM", onSigterm);
110
- if (stdin.isTTY && stdin.setRawMode) {
111
- stdin.setRawMode(priorRawMode);
112
- }
113
- };
114
- const finish = (result) => {
115
- if (settled) {
116
- return;
117
- }
118
- settled = true;
119
- cleanup();
120
- if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
121
- socket.close(1e3, "r5dctl shell finished");
122
- }
123
- if ("error" in result) {
124
- reject(result.error);
125
- } else {
126
- resolve(result.exitCode);
127
- }
128
- };
129
- function attachInput() {
130
- if (inputAttached) {
131
- return;
132
- }
133
- inputAttached = true;
134
- if (stdin.isTTY && stdin.setRawMode) {
135
- stdin.setRawMode(true);
136
- }
137
- stdin.on("data", onInput);
138
- stdin.on("end", onInputEnd);
139
- signalTarget.on("SIGWINCH", sendResize);
140
- signalTarget.on("SIGTERM", onSigterm);
141
- if (!stdin.isTTY) {
142
- signalTarget.on("SIGINT", onSigint);
143
- }
144
- }
145
- function onMessage(data) {
146
- let message;
147
- try {
148
- const parsed = JSON.parse(decodeMessage(data));
149
- if (!parsed || typeof parsed !== "object" || !("type" in parsed)) {
150
- throw new Error("Invalid message");
53
+ if (!opened.ptyId) throw new Error("Terminal did not acknowledge its durable run identity");
54
+ const ptyId = opened.ptyId, priorRaw = stdin.isRaw ?? false;
55
+ let pending = Promise.resolve(), failure, stopped = false;
56
+ const send = (command) => {
57
+ const body = { ...command, id: randomUUID(), ptyId };
58
+ pending = pending.then(() => request(body)).catch((error) => {
59
+ failure = error;
60
+ });
61
+ };
62
+ const input = (chunk) => send({ method: "input", data: Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk });
63
+ const inputEnd = () => {
64
+ if (!interactive) send({ method: "input", data: "" });
65
+ };
66
+ const resize = () => send({ method: "resize", cols: terminalSize(stdout.columns, 80), rows: terminalSize(stdout.rows, 24) });
67
+ const interrupt = () => send({ method: "input", data: "" });
68
+ const terminate = () => {
69
+ send({ method: "close" });
70
+ stopped = true;
71
+ };
72
+ try {
73
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(true);
74
+ stdin.on("data", input);
75
+ stdin.on("end", inputEnd);
76
+ signals.on("SIGWINCH", resize);
77
+ signals.on("SIGTERM", terminate);
78
+ if (!stdin.isTTY) signals.on("SIGINT", interrupt);
79
+ let cursor = 0;
80
+ while (!stopped) {
81
+ if (failure) throw failure;
82
+ const page = await request(
83
+ void 0,
84
+ `?ptyId=${encodeURIComponent(ptyId)}&after=${cursor}`
85
+ );
86
+ if (!Number.isSafeInteger(page.nextCursor) || page.nextCursor < cursor) throw new Error("Invalid terminal output cursor");
87
+ for (const event of page.events) {
88
+ if (event.type === "output") stdout.write(event.data);
89
+ else if (event.type === "exit") {
90
+ await pending;
91
+ if (failure) throw failure;
92
+ return event.exitCode ?? (event.signal ? 128 + (typeof event.signal === "number" ? event.signal : constants.signals[event.signal] ?? 0) : 1);
151
93
  }
152
- message = parsed;
153
- } catch {
154
- finish({ error: new Error("Received an invalid response from r5d.dev") });
155
- return;
156
- }
157
- if (message.type === "connected") {
158
- if (!openRequested) {
159
- openRequested = true;
160
- send({
161
- type: "shell_pty_open",
162
- projectId: options.projectId,
163
- branchName: options.branchName,
164
- cols: clampTerminalSize(stdout.columns, 80),
165
- rows: clampTerminalSize(stdout.rows, 24),
166
- ...options.command === void 0 ? {} : { command: options.command }
167
- });
168
- }
169
- return;
170
- }
171
- if (message.type === "shell_pty_opened") {
172
- ptyId = message.ptyId;
173
- attachInput();
174
- return;
175
- }
176
- if (message.type === "shell_pty_output") {
177
- if (message.ptyId === ptyId) {
178
- stdout.write(message.data);
179
- }
180
- return;
181
- }
182
- if (message.type === "shell_pty_exit") {
183
- if (message.ptyId === ptyId) {
184
- ptyId = void 0;
185
- finish({ exitCode: message.exitCode });
186
- }
187
- return;
188
- }
189
- if (message.type === "shell_pty_error" && (!message.ptyId || message.ptyId === ptyId)) {
190
- finish({ error: new Error(message.message) });
191
94
  }
95
+ cursor = page.nextCursor;
96
+ await new Promise((resolve) => setTimeout(resolve, options.pollIntervalMs ?? 150));
192
97
  }
193
- function onSocketError(error) {
194
- finish({ error: new Error(`r5d.dev shell connection failed: ${error.message}`) });
195
- }
196
- function onSocketClose(code, reason) {
197
- if (!settled) {
198
- const suffix = reason.length > 0 ? `: ${reason.toString("utf8")}` : "";
199
- finish({ error: new Error(`r5d.dev shell connection closed (${code})${suffix}`) });
200
- }
201
- }
202
- socket.on("message", onMessage);
203
- socket.on("error", onSocketError);
204
- socket.on("close", onSocketClose);
205
- });
98
+ await pending;
99
+ if (failure) throw failure;
100
+ return 143;
101
+ } finally {
102
+ stdin.off("data", input);
103
+ stdin.off("end", inputEnd);
104
+ signals.off("SIGWINCH", resize);
105
+ signals.off("SIGTERM", terminate);
106
+ signals.off("SIGINT", interrupt);
107
+ if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(priorRaw);
108
+ }
206
109
  }
207
110
  export {
208
- createShellWebSocketUrl,
209
111
  runR5dctlShell
210
112
  };
@@ -1,4 +1,4 @@
1
- import { R5dctlApiError, R5dctlClient, type R5dctlConversationForkResponse, type R5dctlConversationHeadResponse, type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlProcessInspection, type R5dctlProcessListInput, type R5dctlProcessLog, type R5dctlProcessRun, type R5dctlWorkerCapacityReport, type R5dctlBranch, type R5dctlBranchDeletionStatus, type R5dctlBranchDescription, type R5dctlBranchOperation, type R5dctlSessionSummary, type R5dctlCheckoutPathResponse, type R5dctlSessionClaim, type R5dctlSessionClaimListResponse, type R5dctlSessionClaimScope, type R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorkingTreeMode, type R5dctlWorktreeSource, type R5dctlPromptDelivery, type R5dctlMessageDelivery, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
1
+ import { R5dctlApiError, R5dctlClient, type R5dctlConversationForkResponse, type R5dctlConversationHeadResponse, type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlConversationNodeResponse, type R5dctlConversationOverviewResponse, type R5dctlConversationWorkDetail, type R5dctlConversationWorkResponse, type R5dctlEnvData, type R5dctlProcessInspection, type R5dctlProcessInventory, type R5dctlProcessListInput, type R5dctlProcessLog, type R5dctlProcessRun, type R5dctlWorkerCapacityReport, type R5dctlBranch, type R5dctlBranchDeletionStatus, type R5dctlBranchDescription, type R5dctlBranchOperation, type R5dctlSessionSummary, type R5dctlCheckoutPathResponse, type R5dctlSessionClaim, type R5dctlSessionClaimListResponse, type R5dctlSessionClaimScope, type R5dctlSessionEvent, type R5dctlSessionStartInput, type R5dctlWorkspaceStatus, type R5dctlWorkspaceSyncResult, type R5dctlWorkingTreeMode, type R5dctlWorktreeSource, type R5dctlPromptDelivery, type R5dctlMessageDelivery, type R5dctlSessionMode, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
2
2
  import { type AccountCommand } from "./account";
3
3
  export type GlobalOptions = {
4
4
  baseUrl?: string;
@@ -320,6 +320,8 @@ export declare function renderProcessList(processes: R5dctlProcessRun[], nowMs?:
320
320
  /** `general`, `control`, `queued:general`, or `-` when the worker reported nothing for this run. */
321
321
  export declare function formatProcessLane(process: Pick<R5dctlProcessRun, "admission" | "commandClass">): string;
322
322
  export declare function renderProcessHistory(processes: R5dctlProcessRun[], nowMs?: number): string;
323
+ /** Partial human output never labels unreachable workers as having no effects. */
324
+ export declare function renderProcessInventory(inventory: R5dctlProcessInventory, history?: boolean, nowMs?: number): string;
323
325
  /** Streams go to stdout under `==> name <==` headers; the selection summary is returned separately for stderr. */
324
326
  export declare function renderProcessLog(log: R5dctlProcessLog): {
325
327
  text: string;
@@ -1,5 +1,4 @@
1
1
  import type { Readable, Writable } from "node:stream";
2
- import WebSocket, { type ClientOptions } from "ws";
3
2
  type ShellInput = Readable & {
4
3
  isTTY?: boolean;
5
4
  isRaw?: boolean;
@@ -9,18 +8,19 @@ type ShellOutput = Writable & {
9
8
  columns?: number;
10
9
  rows?: number;
11
10
  };
12
- export type ShellWebSocket = Pick<WebSocket, "readyState" | "send" | "close" | "on" | "off">;
13
11
  export type RunR5dctlShellOptions = {
14
12
  baseUrl: string;
15
13
  credential: string;
16
14
  projectId: string;
17
15
  branchName: string;
16
+ sessionId?: string;
18
17
  command?: string;
19
18
  stdin?: ShellInput;
20
19
  stdout?: ShellOutput;
21
20
  signalTarget?: Pick<NodeJS.EventEmitter, "on" | "off">;
22
- createWebSocket?: (url: string, options: ClientOptions) => ShellWebSocket;
21
+ fetch?: typeof fetch;
22
+ pollIntervalMs?: number;
23
23
  };
24
- export declare function createShellWebSocketUrl(baseUrl: string): string;
24
+ /** Original CLI behavior, using durable HTTP PTY handles instead of process-owned WebSockets. */
25
25
  export declare function runR5dctlShell(options: RunR5dctlShellOptions): Promise<number>;
26
26
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.137",
3
+ "version": "0.0.139",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/cli.cjs",
6
6
  "module": "./dist/mjs/cli.mjs",
@@ -26,7 +26,7 @@
26
26
  "r5dctl": "dist/cjs/main.cjs"
27
27
  },
28
28
  "dependencies": {
29
- "@ricsam/r5d-api": "^0.0.137",
29
+ "@ricsam/r5d-api": "^0.0.139",
30
30
  "dotenv": "^17",
31
31
  "qrcode": "^1.5.4",
32
32
  "ws": "^8.18.3"