@ricsam/r5dctl 0.0.21 → 0.0.23

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.
@@ -0,0 +1,210 @@
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
+ }
37
+ async function runR5dctlShell(options) {
38
+ const stdin = options.stdin ?? process.stdin;
39
+ const stdout = options.stdout ?? process.stdout;
40
+ const signalTarget = options.signalTarget ?? process;
41
+ 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}`
52
+ }
53
+ });
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");
151
+ }
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
+ }
192
+ }
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
+ });
206
+ }
207
+ export {
208
+ createShellWebSocketUrl,
209
+ runR5dctlShell
210
+ };
@@ -1,4 +1,4 @@
1
- import { type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
1
+ import { type R5dctlConversationRenderOptions, type R5dctlConversationResponse, type R5dctlEnvData, type R5dctlEnvTarget, type ChatMode, type ModelTier } from "@ricsam/r5d-api";
2
2
  export type GlobalOptions = {
3
3
  baseUrl?: string;
4
4
  json: boolean;
@@ -26,6 +26,9 @@ type CommandExecutionPlan = {
26
26
  } | {
27
27
  kind: "auth-api-key-create";
28
28
  commandArgs: string[];
29
+ } | {
30
+ kind: "shell";
31
+ command?: string;
29
32
  } | {
30
33
  kind: "cli-help";
31
34
  text: string;
@@ -38,12 +41,32 @@ export declare function parseGlobalArgs(argv: string[]): {
38
41
  rest: string[];
39
42
  };
40
43
  export declare function parseAnswerFlags(args: string[]): string[];
44
+ export type SetEnvOptions = {
45
+ assignments: string[];
46
+ fromEnvFile?: string;
47
+ target: R5dctlEnvTarget;
48
+ description?: string;
49
+ optional?: boolean;
50
+ onlyMissing: boolean;
51
+ includeEmpty: boolean;
52
+ };
53
+ export declare function parseSetEnvArgs(args: string[]): SetEnvOptions;
54
+ export declare function readDotenvFile(filePath: string): Record<string, string>;
41
55
  export declare function parseEnvFlags(args: string[]): string[];
42
56
  export declare function parsePromptArgs(args: string[]): {
43
57
  mode: ChatMode;
44
58
  model: ModelTier;
45
59
  message: string;
46
60
  };
61
+ export declare function parseShellArgs(args: string[]): {
62
+ command?: string;
63
+ };
64
+ export type EnvSummaryData = Partial<Record<R5dctlEnvTarget, Record<string, {
65
+ optional: boolean;
66
+ description: string;
67
+ hasValue: boolean;
68
+ }>>>;
69
+ export declare function summarizeEnvData(data: R5dctlEnvData): EnvSummaryData;
47
70
  export declare function parseConversationRenderArgs(args: string[]): ConversationRenderOptions;
48
71
  export declare function renderConversationResponse(read: R5dctlConversationResponse): string;
49
72
  export declare function resolveCommandExecution(options: GlobalOptions, rest: string[]): CommandExecutionPlan;
@@ -0,0 +1,26 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import WebSocket, { type ClientOptions } from "ws";
3
+ type ShellInput = Readable & {
4
+ isTTY?: boolean;
5
+ isRaw?: boolean;
6
+ setRawMode?: (mode: boolean) => unknown;
7
+ };
8
+ type ShellOutput = Writable & {
9
+ columns?: number;
10
+ rows?: number;
11
+ };
12
+ export type ShellWebSocket = Pick<WebSocket, "readyState" | "send" | "close" | "on" | "off">;
13
+ export type RunR5dctlShellOptions = {
14
+ baseUrl: string;
15
+ credential: string;
16
+ projectId: string;
17
+ branchName: string;
18
+ command?: string;
19
+ stdin?: ShellInput;
20
+ stdout?: ShellOutput;
21
+ signalTarget?: Pick<NodeJS.Process, "on" | "off">;
22
+ createWebSocket?: (url: string, options: ClientOptions) => ShellWebSocket;
23
+ };
24
+ export declare function createShellWebSocketUrl(baseUrl: string): string;
25
+ export declare function runR5dctlShell(options: RunR5dctlShellOptions): Promise<number>;
26
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5dctl",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/cli.cjs",
6
6
  "module": "./dist/mjs/cli.mjs",
@@ -26,7 +26,9 @@
26
26
  "r5dctl": "dist/cjs/main.cjs"
27
27
  },
28
28
  "dependencies": {
29
- "@ricsam/r5d-api": "^0.0.21"
29
+ "@ricsam/r5d-api": "^0.0.23",
30
+ "dotenv": "^17",
31
+ "ws": "^8.18.3"
30
32
  },
31
33
  "files": [
32
34
  "dist",