@ratio-app/cli 0.1.1

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 (182) hide show
  1. package/README.md +504 -0
  2. package/bin/run.mjs +5 -0
  3. package/dist/commands/__test__/throw.d.ts +25 -0
  4. package/dist/commands/__test__/throw.js +238 -0
  5. package/dist/commands/__test__/throw.js.map +1 -0
  6. package/dist/commands/app/create.d.ts +55 -0
  7. package/dist/commands/app/create.js +609 -0
  8. package/dist/commands/app/create.js.map +1 -0
  9. package/dist/commands/app/deploy.d.ts +14 -0
  10. package/dist/commands/app/deploy.js +179 -0
  11. package/dist/commands/app/deploy.js.map +1 -0
  12. package/dist/commands/app/dev.d.ts +28 -0
  13. package/dist/commands/app/dev.js +701 -0
  14. package/dist/commands/app/dev.js.map +1 -0
  15. package/dist/commands/app/generate.d.ts +14 -0
  16. package/dist/commands/app/generate.js +179 -0
  17. package/dist/commands/app/generate.js.map +1 -0
  18. package/dist/commands/app/info.d.ts +14 -0
  19. package/dist/commands/app/info.js +179 -0
  20. package/dist/commands/app/info.js.map +1 -0
  21. package/dist/commands/app/link.d.ts +118 -0
  22. package/dist/commands/app/link.js +1120 -0
  23. package/dist/commands/app/link.js.map +1 -0
  24. package/dist/commands/auth/login.d.ts +43 -0
  25. package/dist/commands/auth/login.js +987 -0
  26. package/dist/commands/auth/login.js.map +1 -0
  27. package/dist/commands/auth/logout.d.ts +15 -0
  28. package/dist/commands/auth/logout.js +486 -0
  29. package/dist/commands/auth/logout.js.map +1 -0
  30. package/dist/commands/auth/whoami.d.ts +16 -0
  31. package/dist/commands/auth/whoami.js +531 -0
  32. package/dist/commands/auth/whoami.js.map +1 -0
  33. package/dist/commands/dev/listen.d.ts +84 -0
  34. package/dist/commands/dev/listen.js +1187 -0
  35. package/dist/commands/dev/listen.js.map +1 -0
  36. package/dist/commands/dev/trigger.d.ts +88 -0
  37. package/dist/commands/dev/trigger.js +729 -0
  38. package/dist/commands/dev/trigger.js.map +1 -0
  39. package/dist/index.d.ts +14 -0
  40. package/dist/index.js +617 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/lib/auth/authorize-code-exchange.d.ts +50 -0
  43. package/dist/lib/auth/authorize-code-exchange.js +114 -0
  44. package/dist/lib/auth/authorize-code-exchange.js.map +1 -0
  45. package/dist/lib/auth/authorize-url.d.ts +42 -0
  46. package/dist/lib/auth/authorize-url.js +35 -0
  47. package/dist/lib/auth/authorize-url.js.map +1 -0
  48. package/dist/lib/auth/browser.d.ts +16 -0
  49. package/dist/lib/auth/browser.js +48 -0
  50. package/dist/lib/auth/browser.js.map +1 -0
  51. package/dist/lib/auth/config.d.ts +41 -0
  52. package/dist/lib/auth/config.js +21 -0
  53. package/dist/lib/auth/config.js.map +1 -0
  54. package/dist/lib/auth/http-refresher.d.ts +30 -0
  55. package/dist/lib/auth/http-refresher.js +103 -0
  56. package/dist/lib/auth/http-refresher.js.map +1 -0
  57. package/dist/lib/auth/identity.d.ts +26 -0
  58. package/dist/lib/auth/identity.js +10 -0
  59. package/dist/lib/auth/identity.js.map +1 -0
  60. package/dist/lib/auth/index.d.ts +13 -0
  61. package/dist/lib/auth/index.js +527 -0
  62. package/dist/lib/auth/index.js.map +1 -0
  63. package/dist/lib/auth/loopback-server.d.ts +65 -0
  64. package/dist/lib/auth/loopback-server.js +245 -0
  65. package/dist/lib/auth/loopback-server.js.map +1 -0
  66. package/dist/lib/auth/manual-prompt.d.ts +17 -0
  67. package/dist/lib/auth/manual-prompt.js +20 -0
  68. package/dist/lib/auth/manual-prompt.js.map +1 -0
  69. package/dist/lib/auth/pkce.d.ts +24 -0
  70. package/dist/lib/auth/pkce.js +15 -0
  71. package/dist/lib/auth/pkce.js.map +1 -0
  72. package/dist/lib/auth/state.d.ts +20 -0
  73. package/dist/lib/auth/state.js +19 -0
  74. package/dist/lib/auth/state.js.map +1 -0
  75. package/dist/lib/auth/verbose-trace.d.ts +25 -0
  76. package/dist/lib/auth/verbose-trace.js +17 -0
  77. package/dist/lib/auth/verbose-trace.js.map +1 -0
  78. package/dist/lib/credentials/clock.d.ts +18 -0
  79. package/dist/lib/credentials/clock.js +10 -0
  80. package/dist/lib/credentials/clock.js.map +1 -0
  81. package/dist/lib/credentials/index.d.ts +5 -0
  82. package/dist/lib/credentials/index.js +340 -0
  83. package/dist/lib/credentials/index.js.map +1 -0
  84. package/dist/lib/credentials/path.d.ts +26 -0
  85. package/dist/lib/credentials/path.js +32 -0
  86. package/dist/lib/credentials/path.js.map +1 -0
  87. package/dist/lib/credentials/refresher.d.ts +26 -0
  88. package/dist/lib/credentials/refresher.js +34 -0
  89. package/dist/lib/credentials/refresher.js.map +1 -0
  90. package/dist/lib/credentials/schema.d.ts +138 -0
  91. package/dist/lib/credentials/schema.js +85 -0
  92. package/dist/lib/credentials/schema.js.map +1 -0
  93. package/dist/lib/credentials/store.d.ts +102 -0
  94. package/dist/lib/credentials/store.js +337 -0
  95. package/dist/lib/credentials/store.js.map +1 -0
  96. package/dist/lib/credentials/types.d.ts +65 -0
  97. package/dist/lib/credentials/types.js +1 -0
  98. package/dist/lib/credentials/types.js.map +1 -0
  99. package/dist/lib/dev-store-gate.d.ts +66 -0
  100. package/dist/lib/dev-store-gate.js +15 -0
  101. package/dist/lib/dev-store-gate.js.map +1 -0
  102. package/dist/lib/errors.d.ts +60 -0
  103. package/dist/lib/errors.js +101 -0
  104. package/dist/lib/errors.js.map +1 -0
  105. package/dist/lib/manifest/index.d.ts +2 -0
  106. package/dist/lib/manifest/index.js +144 -0
  107. package/dist/lib/manifest/index.js.map +1 -0
  108. package/dist/lib/manifest/read-manifest.d.ts +32 -0
  109. package/dist/lib/manifest/read-manifest.js +87 -0
  110. package/dist/lib/manifest/read-manifest.js.map +1 -0
  111. package/dist/lib/manifest/write-client-id.d.ts +33 -0
  112. package/dist/lib/manifest/write-client-id.js +94 -0
  113. package/dist/lib/manifest/write-client-id.js.map +1 -0
  114. package/dist/lib/messages.d.ts +15 -0
  115. package/dist/lib/messages.js +16 -0
  116. package/dist/lib/messages.js.map +1 -0
  117. package/dist/lib/orchestrator/child-runner.d.ts +140 -0
  118. package/dist/lib/orchestrator/child-runner.js +471 -0
  119. package/dist/lib/orchestrator/child-runner.js.map +1 -0
  120. package/dist/lib/preview/index.d.ts +11 -0
  121. package/dist/lib/preview/index.js +17 -0
  122. package/dist/lib/preview/index.js.map +1 -0
  123. package/dist/lib/preview/types.d.ts +16 -0
  124. package/dist/lib/preview/types.js +11 -0
  125. package/dist/lib/preview/types.js.map +1 -0
  126. package/dist/lib/ratio-command.d.ts +52 -0
  127. package/dist/lib/ratio-command.js +141 -0
  128. package/dist/lib/ratio-command.js.map +1 -0
  129. package/dist/lib/relay/index.d.ts +7 -0
  130. package/dist/lib/relay/index.js +362 -0
  131. package/dist/lib/relay/index.js.map +1 -0
  132. package/dist/lib/relay/relay-client.d.ts +91 -0
  133. package/dist/lib/relay/relay-client.js +362 -0
  134. package/dist/lib/relay/relay-client.js.map +1 -0
  135. package/dist/lib/relay/types.d.ts +71 -0
  136. package/dist/lib/relay/types.js +1 -0
  137. package/dist/lib/relay/types.js.map +1 -0
  138. package/dist/lib/render-error.d.ts +35 -0
  139. package/dist/lib/render-error.js +115 -0
  140. package/dist/lib/render-error.js.map +1 -0
  141. package/dist/lib/verbose-flag.d.ts +12 -0
  142. package/dist/lib/verbose-flag.js +17 -0
  143. package/dist/lib/verbose-flag.js.map +1 -0
  144. package/dist/render-CrHGqTPH.d.ts +115 -0
  145. package/dist/templates/.gitkeep +0 -0
  146. package/dist/templates/minimal/.env.example.tmpl +4 -0
  147. package/dist/templates/minimal/README.md.tmpl +26 -0
  148. package/dist/templates/minimal/gitignore +5 -0
  149. package/dist/templates/minimal/manifest.json +5 -0
  150. package/dist/templates/minimal/package.json.tmpl +25 -0
  151. package/dist/templates/minimal/ratio.config.jsonc.tmpl +17 -0
  152. package/dist/templates/minimal/src/index.ts.tmpl +26 -0
  153. package/dist/templates/minimal/src/install.ts.tmpl +37 -0
  154. package/dist/templates/minimal/src/webhooks.ts.tmpl +57 -0
  155. package/dist/templates/minimal/tsconfig.json +17 -0
  156. package/dist/templates/serverless/README.md.tmpl +28 -0
  157. package/dist/templates/serverless/gitignore +6 -0
  158. package/dist/templates/serverless/manifest.json +5 -0
  159. package/dist/templates/serverless/package.json.tmpl +22 -0
  160. package/dist/templates/serverless/ratio.config.jsonc.tmpl +17 -0
  161. package/dist/templates/serverless/src/index.ts.tmpl +20 -0
  162. package/dist/templates/serverless/src/install.ts.tmpl +20 -0
  163. package/dist/templates/serverless/src/webhooks.ts.tmpl +36 -0
  164. package/dist/templates/serverless/tsconfig.json +18 -0
  165. package/dist/templates/serverless/wrangler.toml.tmpl +9 -0
  166. package/dist/templates/with-admin-ui/.env.example.tmpl +4 -0
  167. package/dist/templates/with-admin-ui/README.md.tmpl +28 -0
  168. package/dist/templates/with-admin-ui/gitignore +6 -0
  169. package/dist/templates/with-admin-ui/manifest.json +5 -0
  170. package/dist/templates/with-admin-ui/package.json.tmpl +31 -0
  171. package/dist/templates/with-admin-ui/ratio.config.jsonc.tmpl +17 -0
  172. package/dist/templates/with-admin-ui/src/admin/app/globals.css +19 -0
  173. package/dist/templates/with-admin-ui/src/admin/app/layout.tsx.tmpl +13 -0
  174. package/dist/templates/with-admin-ui/src/admin/app/page.tsx.tmpl +8 -0
  175. package/dist/templates/with-admin-ui/src/admin/next-env.d.ts.tmpl +5 -0
  176. package/dist/templates/with-admin-ui/src/admin/next.config.mjs +3 -0
  177. package/dist/templates/with-admin-ui/src/admin/tsconfig.json +20 -0
  178. package/dist/templates/with-admin-ui/src/index.ts.tmpl +26 -0
  179. package/dist/templates/with-admin-ui/src/install.ts.tmpl +37 -0
  180. package/dist/templates/with-admin-ui/src/webhooks.ts.tmpl +57 -0
  181. package/dist/templates/with-admin-ui/tsconfig.json +17 -0
  182. package/package.json +78 -0
@@ -0,0 +1,701 @@
1
+ // src/lib/manifest/write-client-id.ts
2
+ import crypto from "crypto";
3
+ import fs from "fs";
4
+ import path from "path";
5
+
6
+ // src/lib/errors.ts
7
+ var RatioCLIError = class extends Error {
8
+ code;
9
+ exitCode;
10
+ hint;
11
+ docsUrl;
12
+ constructor(opts) {
13
+ super(opts.message);
14
+ this.name = new.target.name;
15
+ this.code = opts.code;
16
+ this.exitCode = opts.exitCode;
17
+ if (opts.hint !== void 0) this.hint = opts.hint;
18
+ if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
19
+ }
20
+ };
21
+ var ValidationError = class extends RatioCLIError {
22
+ constructor(message, hint) {
23
+ const opts = {
24
+ code: "VALIDATION_ERROR",
25
+ exitCode: 8,
26
+ message
27
+ };
28
+ if (hint !== void 0) opts.hint = hint;
29
+ super(opts);
30
+ }
31
+ };
32
+ var OrchestratorSpawnError = class extends RatioCLIError {
33
+ constructor(message, hint = "Could not start your app's dev command \u2014 check that the command in ratio.config.jsonc runs from this directory.") {
34
+ super({ code: "ORCHESTRATOR_SPAWN", exitCode: 10, message, hint });
35
+ }
36
+ };
37
+ var OrchestratorDrainTimeoutError = class extends RatioCLIError {
38
+ constructor(message, hint = "Your dev command did not exit cleanly within 5 seconds \u2014 it was forcibly terminated.") {
39
+ super({ code: "ORCHESTRATOR_DRAIN", exitCode: 11, message, hint });
40
+ }
41
+ };
42
+ function isRatioCLIError(err) {
43
+ return err instanceof RatioCLIError;
44
+ }
45
+
46
+ // src/lib/manifest/write-client-id.ts
47
+ var MANIFEST_FILE_NAME = "ratio.config.jsonc";
48
+
49
+ // src/lib/manifest/read-manifest.ts
50
+ import fs2 from "fs";
51
+ import path2 from "path";
52
+ var DEV_COMMAND_ROW = /^\s*"devCommand"\s*:\s*"([^"]*)"/m;
53
+ var DEV_SERVER_PORT_ROW = /^\s*"devServerPort"\s*:\s*(\d+)/m;
54
+ var DEV_SERVER_PORT_KEY = /^\s*"devServerPort"\s*:/m;
55
+ var APP_NAME_ROW = /^\s*"appName"\s*:\s*"([^"]*)"/m;
56
+ var CLIENT_ID_READ_ROW = /^\s*"clientId"\s*:\s*"([^"]*)"/m;
57
+ var DEV_COMMAND_HINT = 'Add a "devCommand" entry to ratio.config.jsonc (e.g. "npm run dev") and re-run.';
58
+ function readManifest(cwd) {
59
+ const filePath = path2.join(cwd, MANIFEST_FILE_NAME);
60
+ let content;
61
+ try {
62
+ content = fs2.readFileSync(filePath, "utf8");
63
+ } catch {
64
+ throw new ValidationError(
65
+ `${MANIFEST_FILE_NAME} not found in ${cwd}.`,
66
+ `Run this command from the app's repo root; ${MANIFEST_FILE_NAME} not found in ${cwd}.`
67
+ );
68
+ }
69
+ const devCommand = DEV_COMMAND_ROW.exec(content)?.[1];
70
+ if (devCommand === void 0 || devCommand.trim() === "") {
71
+ throw new ValidationError(
72
+ `${MANIFEST_FILE_NAME} has no usable "devCommand" entry.`,
73
+ DEV_COMMAND_HINT
74
+ );
75
+ }
76
+ let devServerPort;
77
+ if (DEV_SERVER_PORT_KEY.test(content)) {
78
+ const rawPort = DEV_SERVER_PORT_ROW.exec(content)?.[1];
79
+ const port = rawPort === void 0 ? Number.NaN : Number.parseInt(rawPort, 10);
80
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
81
+ throw new ValidationError(
82
+ `${MANIFEST_FILE_NAME} has an invalid "devServerPort" value.`,
83
+ '"devServerPort" must be an unquoted integer between 1 and 65535 (e.g. "devServerPort": 3001).'
84
+ );
85
+ }
86
+ devServerPort = port;
87
+ }
88
+ return {
89
+ path: filePath,
90
+ devCommand,
91
+ devServerPort,
92
+ appName: APP_NAME_ROW.exec(content)?.[1],
93
+ clientId: CLIENT_ID_READ_ROW.exec(content)?.[1]
94
+ };
95
+ }
96
+
97
+ // src/lib/orchestrator/child-runner.ts
98
+ import { spawn } from "child_process";
99
+ var DRAIN_MS = 5e3;
100
+ var SIGNAL_FORCE_EXIT_WINDOW_MS = 2e3;
101
+ var CHILD_BUFFER_MAX_BYTES = 64 * 1024;
102
+ var DEV_COMMAND_TOKENIZE_HINT = "devCommand must be a simple whitespace-separated command; complex shell pipelines are not supported.";
103
+ var DEFAULT_FORWARD_PORT = 3001;
104
+ function tokenizeDevCommand(raw) {
105
+ const tokens = [];
106
+ let current = "";
107
+ let quote;
108
+ let hasToken = false;
109
+ for (let i = 0; i < raw.length; i += 1) {
110
+ const ch = raw.charAt(i);
111
+ if (quote !== void 0) {
112
+ if (ch === quote) {
113
+ quote = void 0;
114
+ } else {
115
+ current += ch;
116
+ }
117
+ continue;
118
+ }
119
+ if (ch === '"' || ch === "'") {
120
+ quote = ch;
121
+ hasToken = true;
122
+ continue;
123
+ }
124
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
125
+ if (hasToken) {
126
+ tokens.push(current);
127
+ current = "";
128
+ hasToken = false;
129
+ }
130
+ continue;
131
+ }
132
+ if (ch === "|" || ch === "&" || ch === ";" || ch === "<" || ch === ">" || ch === "`") {
133
+ throw new ValidationError(
134
+ `devCommand contains an unsupported shell operator: ${ch}`,
135
+ DEV_COMMAND_TOKENIZE_HINT
136
+ );
137
+ }
138
+ if (ch === "$" && raw.charAt(i + 1) === "(") {
139
+ throw new ValidationError(
140
+ "devCommand contains an unsupported shell substitution.",
141
+ DEV_COMMAND_TOKENIZE_HINT
142
+ );
143
+ }
144
+ current += ch;
145
+ hasToken = true;
146
+ }
147
+ if (quote !== void 0) {
148
+ throw new ValidationError(
149
+ "devCommand has an unterminated quote.",
150
+ DEV_COMMAND_TOKENIZE_HINT
151
+ );
152
+ }
153
+ if (hasToken) {
154
+ tokens.push(current);
155
+ }
156
+ if (tokens.length === 0 || tokens[0] === "") {
157
+ throw new ValidationError("devCommand is empty.", DEV_COMMAND_HINT);
158
+ }
159
+ return tokens;
160
+ }
161
+ var LinePrefixer = class {
162
+ constructor(prefix, write) {
163
+ this.prefix = prefix;
164
+ this.write = write;
165
+ }
166
+ prefix;
167
+ write;
168
+ buffer = Buffer.alloc(0);
169
+ push(chunk) {
170
+ const incoming = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
171
+ if (incoming.length === 0) {
172
+ return;
173
+ }
174
+ this.buffer = this.buffer.length === 0 ? incoming : Buffer.concat([this.buffer, incoming]);
175
+ this.drainCompleteLines();
176
+ if (this.buffer.length > CHILD_BUFFER_MAX_BYTES) {
177
+ this.flush();
178
+ }
179
+ }
180
+ /** Emit any buffered partial line (with prefix + newline), then reset. */
181
+ flush() {
182
+ if (this.buffer.length > 0) {
183
+ this.write(`${this.prefix}${this.buffer.toString("utf8")}
184
+ `);
185
+ this.buffer = Buffer.alloc(0);
186
+ }
187
+ }
188
+ drainCompleteLines() {
189
+ let newlineAt = this.buffer.indexOf(10);
190
+ while (newlineAt !== -1) {
191
+ const line = this.buffer.subarray(0, newlineAt).toString("utf8");
192
+ this.buffer = this.buffer.subarray(newlineAt + 1);
193
+ this.write(`${this.prefix}${line}
194
+ `);
195
+ newlineAt = this.buffer.indexOf(10);
196
+ }
197
+ }
198
+ };
199
+ var SIGNAL_EXIT_CODES = {
200
+ SIGHUP: 129,
201
+ SIGINT: 130,
202
+ SIGQUIT: 131,
203
+ SIGKILL: 137,
204
+ SIGTERM: 143
205
+ };
206
+ function exitCodeFrom(code, signal) {
207
+ if (code !== null) {
208
+ return code;
209
+ }
210
+ if (signal !== null) {
211
+ return SIGNAL_EXIT_CODES[signal] ?? 1;
212
+ }
213
+ return 1;
214
+ }
215
+ var ExitArbiter = class {
216
+ decidingChildCode;
217
+ signalCode;
218
+ get shutdownInitiated() {
219
+ return this.decidingChildCode !== void 0 || this.signalCode !== void 0;
220
+ }
221
+ observeChildExit(code, signal) {
222
+ if (this.shutdownInitiated) {
223
+ return;
224
+ }
225
+ this.decidingChildCode = exitCodeFrom(code, signal);
226
+ }
227
+ observeSignal(sig) {
228
+ if (this.shutdownInitiated) {
229
+ return;
230
+ }
231
+ this.signalCode = sig === "SIGINT" ? 130 : 143;
232
+ }
233
+ finalCode() {
234
+ return this.signalCode ?? this.decidingChildCode ?? 0;
235
+ }
236
+ };
237
+ var defaultSpawn = (command, args, options) => spawn(command, [...args], {
238
+ cwd: options.cwd,
239
+ env: options.env,
240
+ stdio: options.stdio,
241
+ shell: options.shell
242
+ });
243
+ function buildListenInvocation(env, devServerPort, argv1) {
244
+ const override = env.RATIO_DEV_LISTEN_BIN;
245
+ const listenBin = override ?? argv1;
246
+ if (listenBin === void 0 || listenBin === "") {
247
+ throw new OrchestratorSpawnError(
248
+ "Could not locate the CLI binary to start the webhook listener.",
249
+ "Re-install the CLI, or invoke it through its bin entry point."
250
+ );
251
+ }
252
+ const listenArgs = override ? [] : [
253
+ "dev",
254
+ "listen",
255
+ "--forward-to",
256
+ `localhost:${devServerPort ?? DEFAULT_FORWARD_PORT}/webhooks`
257
+ ];
258
+ return { command: process.execPath, args: [listenBin, ...listenArgs] };
259
+ }
260
+ function describeCause(cause) {
261
+ return cause instanceof Error ? cause.message : String(cause);
262
+ }
263
+ async function orchestrate(options) {
264
+ const { io } = options;
265
+ const drainMs = options.drainMs ?? DRAIN_MS;
266
+ const spawnApp = options.spawnApp ?? defaultSpawn;
267
+ const spawnListen = options.spawnListen ?? defaultSpawn;
268
+ const trace = (line) => {
269
+ if (options.verbose) {
270
+ io.writeErr(`[verbose] ${line}
271
+ `);
272
+ }
273
+ };
274
+ const appCommand = options.appArgv[0];
275
+ if (appCommand === void 0 || appCommand === "") {
276
+ throw new ValidationError("devCommand is empty.", DEV_COMMAND_HINT);
277
+ }
278
+ const appEnv = {
279
+ ...options.env,
280
+ ...options.devServerPort !== void 0 ? { PORT: String(options.devServerPort) } : {},
281
+ RATIO_ORCHESTRATED: "1"
282
+ };
283
+ let app;
284
+ try {
285
+ app = spawnApp(appCommand, options.appArgv.slice(1), {
286
+ cwd: options.cwd,
287
+ env: appEnv,
288
+ stdio: ["ignore", "pipe", "pipe"],
289
+ shell: false
290
+ });
291
+ } catch (cause) {
292
+ throw new OrchestratorSpawnError(
293
+ `Could not start the dev command (${appCommand}): ${describeCause(cause)}`
294
+ );
295
+ }
296
+ trace(`spawn app: ${appCommand} (${app.pid ?? "-"})`);
297
+ const invocation = buildListenInvocation(
298
+ options.env,
299
+ options.devServerPort,
300
+ options.argv1 ?? process.argv[1]
301
+ );
302
+ const listenEnv = { ...options.env, RATIO_ORCHESTRATED: "1" };
303
+ let listen;
304
+ try {
305
+ listen = spawnListen(invocation.command, invocation.args, {
306
+ cwd: options.cwd,
307
+ env: listenEnv,
308
+ stdio: ["ignore", "pipe", "pipe"],
309
+ shell: false
310
+ });
311
+ } catch (cause) {
312
+ app.kill("SIGTERM");
313
+ throw new OrchestratorSpawnError(
314
+ `Could not start the webhook listener: ${describeCause(cause)}`,
315
+ "The bundled webhook listener could not be started; re-install the CLI if this persists."
316
+ );
317
+ }
318
+ trace(`spawn listen: (${listen.pid ?? "-"})`);
319
+ const appOut = new LinePrefixer("[app] ", (chunk) => io.writeOut(chunk));
320
+ const appErrOut = new LinePrefixer("[app] ", (chunk) => io.writeOut(chunk));
321
+ const listenOut = new LinePrefixer("[listen] ", (chunk) => io.writeOut(chunk));
322
+ const listenErrOut = new LinePrefixer("[listen] ", (chunk) => io.writeOut(chunk));
323
+ const pipe = (stream, prefixer) => {
324
+ if (stream !== null) {
325
+ stream.on("data", (chunk) => prefixer.push(chunk));
326
+ }
327
+ };
328
+ pipe(app.stdout, appOut);
329
+ pipe(app.stderr, appErrOut);
330
+ pipe(listen.stdout, listenOut);
331
+ pipe(listen.stderr, listenErrOut);
332
+ const arbiter = new ExitArbiter();
333
+ let appExited = false;
334
+ let listenExited = false;
335
+ let spawnFailure;
336
+ let drainEscalated = false;
337
+ let drainTimer;
338
+ let drainStartedAt = 0;
339
+ let firstSignalAt = 0;
340
+ return await new Promise((resolve, reject) => {
341
+ let settled = false;
342
+ const cleanup = () => {
343
+ options.signalSource.removeListener("SIGINT", onSigint);
344
+ options.signalSource.removeListener("SIGTERM", onSigterm);
345
+ if (drainTimer !== void 0) {
346
+ clearTimeout(drainTimer);
347
+ drainTimer = void 0;
348
+ }
349
+ appOut.flush();
350
+ appErrOut.flush();
351
+ listenOut.flush();
352
+ listenErrOut.flush();
353
+ };
354
+ const settle = (code) => {
355
+ if (!settled) {
356
+ settled = true;
357
+ cleanup();
358
+ resolve(code);
359
+ }
360
+ };
361
+ const fail = (err) => {
362
+ if (!settled) {
363
+ settled = true;
364
+ cleanup();
365
+ reject(err instanceof Error ? err : new Error(String(err)));
366
+ }
367
+ };
368
+ const startDrain = () => {
369
+ if (drainTimer !== void 0) {
370
+ return;
371
+ }
372
+ drainStartedAt = Date.now();
373
+ drainTimer = setTimeout(() => {
374
+ if (!appExited) {
375
+ app.kill("SIGKILL");
376
+ drainEscalated = true;
377
+ }
378
+ if (!listenExited) {
379
+ listen.kill("SIGKILL");
380
+ drainEscalated = true;
381
+ }
382
+ }, drainMs);
383
+ };
384
+ const finishIfDone = () => {
385
+ if (!appExited || !listenExited) {
386
+ return;
387
+ }
388
+ if (drainStartedAt > 0) {
389
+ trace(`drain elapsed: ${Date.now() - drainStartedAt}ms`);
390
+ }
391
+ const code = arbiter.finalCode();
392
+ void options.preview.onShutdown().then(
393
+ () => {
394
+ if (spawnFailure !== void 0) {
395
+ fail(spawnFailure);
396
+ return;
397
+ }
398
+ if (drainEscalated && code === 0) {
399
+ fail(
400
+ new OrchestratorDrainTimeoutError(
401
+ "A child process had to be forcibly terminated during shutdown."
402
+ )
403
+ );
404
+ return;
405
+ }
406
+ settle(code);
407
+ },
408
+ (err) => fail(err)
409
+ );
410
+ };
411
+ const onAppExit = (code, signal) => {
412
+ appExited = true;
413
+ appOut.flush();
414
+ appErrOut.flush();
415
+ trace(`app exited: code=${code} signal=${signal}`);
416
+ const alreadyShuttingDown = arbiter.shutdownInitiated;
417
+ arbiter.observeChildExit(code, signal);
418
+ if (!alreadyShuttingDown && !listenExited) {
419
+ listen.kill("SIGTERM");
420
+ startDrain();
421
+ }
422
+ finishIfDone();
423
+ };
424
+ const onListenExit = (code, signal) => {
425
+ listenExited = true;
426
+ listenOut.flush();
427
+ listenErrOut.flush();
428
+ trace(`listen exited: code=${code} signal=${signal}`);
429
+ const alreadyShuttingDown = arbiter.shutdownInitiated;
430
+ arbiter.observeChildExit(code, signal);
431
+ if (!alreadyShuttingDown && !appExited) {
432
+ app.kill("SIGTERM");
433
+ startDrain();
434
+ }
435
+ finishIfDone();
436
+ };
437
+ const onAppError = (err) => {
438
+ appExited = true;
439
+ if (spawnFailure === void 0) {
440
+ spawnFailure = new OrchestratorSpawnError(
441
+ `Could not start the dev command (${appCommand}): ${err.message}`
442
+ );
443
+ }
444
+ arbiter.observeChildExit(1, null);
445
+ if (!listenExited) {
446
+ listen.kill("SIGTERM");
447
+ startDrain();
448
+ }
449
+ finishIfDone();
450
+ };
451
+ const onListenError = (err) => {
452
+ listenExited = true;
453
+ if (spawnFailure === void 0) {
454
+ spawnFailure = new OrchestratorSpawnError(
455
+ `Could not start the webhook listener: ${err.message}`,
456
+ "The bundled webhook listener could not be started; re-install the CLI if this persists."
457
+ );
458
+ }
459
+ arbiter.observeChildExit(1, null);
460
+ if (!appExited) {
461
+ app.kill("SIGTERM");
462
+ startDrain();
463
+ }
464
+ finishIfDone();
465
+ };
466
+ const onSignal = (sig) => {
467
+ trace(`signal received: ${sig}`);
468
+ if (arbiter.shutdownInitiated) {
469
+ if (firstSignalAt === 0) {
470
+ firstSignalAt = Date.now();
471
+ return;
472
+ }
473
+ if (Date.now() - firstSignalAt <= SIGNAL_FORCE_EXIT_WINDOW_MS) {
474
+ options.hardExit(130);
475
+ }
476
+ return;
477
+ }
478
+ firstSignalAt = Date.now();
479
+ arbiter.observeSignal(sig);
480
+ io.writeErr("shutting down: stopping the dev command and the webhook listener\n");
481
+ if (!appExited) {
482
+ app.kill("SIGTERM");
483
+ }
484
+ if (!listenExited) {
485
+ listen.kill("SIGTERM");
486
+ }
487
+ startDrain();
488
+ };
489
+ const onSigint = () => onSignal("SIGINT");
490
+ const onSigterm = () => onSignal("SIGTERM");
491
+ options.signalSource.on("SIGINT", onSigint);
492
+ options.signalSource.on("SIGTERM", onSigterm);
493
+ app.once("exit", onAppExit);
494
+ app.once("error", onAppError);
495
+ listen.once("exit", onListenExit);
496
+ listen.once("error", onListenError);
497
+ trace(`preview: onDevServerReady(${options.devServerPort ?? 0})`);
498
+ void options.preview.onDevServerReady(options.devServerPort ?? 0).catch((err) => fail(err));
499
+ });
500
+ }
501
+
502
+ // src/lib/preview/types.ts
503
+ var NullPreviewController = class {
504
+ async onDevServerReady(_port) {
505
+ }
506
+ async onShutdown() {
507
+ }
508
+ };
509
+
510
+ // src/lib/preview/index.ts
511
+ function createPreviewController() {
512
+ return new NullPreviewController();
513
+ }
514
+
515
+ // src/lib/ratio-command.ts
516
+ import { Command, Errors } from "@oclif/core";
517
+
518
+ // src/lib/render-error.ts
519
+ function isOclifParserError(err) {
520
+ return err instanceof Error && "parse" in err && err.parse !== null && err.parse !== void 0;
521
+ }
522
+ function buildErrorEnvelope(code, message, hint, docsUrl) {
523
+ const obj = { ok: false, code, message };
524
+ if (hint !== void 0) obj["hint"] = hint;
525
+ if (docsUrl !== void 0) obj["docsUrl"] = docsUrl;
526
+ return JSON.stringify(obj) + "\n";
527
+ }
528
+ function renderError(input) {
529
+ const { err, verbose, json } = input;
530
+ if (isRatioCLIError(err)) {
531
+ const ratioErr = err;
532
+ if (ratioErr.exitCode === 0) {
533
+ if (json) {
534
+ return {
535
+ stdout: JSON.stringify({ ok: true, data: null }) + "\n",
536
+ stderr: "",
537
+ exitCode: 0
538
+ };
539
+ }
540
+ return {
541
+ stdout: ratioErr.message + "\n",
542
+ stderr: "",
543
+ exitCode: 0
544
+ };
545
+ }
546
+ let stderrLines = `${ratioErr.code}: ${ratioErr.message}
547
+ `;
548
+ if (ratioErr.hint !== void 0) stderrLines += `${ratioErr.hint}
549
+ `;
550
+ if (ratioErr.docsUrl !== void 0) stderrLines += `${ratioErr.docsUrl}
551
+ `;
552
+ if (json) {
553
+ return {
554
+ stdout: buildErrorEnvelope(ratioErr.code, ratioErr.message, ratioErr.hint, ratioErr.docsUrl),
555
+ stderr: verbose && ratioErr.stack ? ratioErr.stack + "\n" : "",
556
+ exitCode: ratioErr.exitCode
557
+ };
558
+ }
559
+ if (verbose && ratioErr.stack) {
560
+ stderrLines += ratioErr.stack + "\n";
561
+ }
562
+ return { stdout: "", stderr: stderrLines, exitCode: ratioErr.exitCode };
563
+ }
564
+ if (isOclifParserError(err)) {
565
+ const parserErr = err;
566
+ const msg = parserErr.message || "Unknown parse error";
567
+ const coercedErr = new ValidationError(msg);
568
+ let stderrText2 = `${coercedErr.code}: ${msg}
569
+ `;
570
+ if (json) {
571
+ return {
572
+ stdout: buildErrorEnvelope(coercedErr.code, msg),
573
+ stderr: verbose && parserErr.stack ? parserErr.stack + "\n" : "",
574
+ exitCode: 8
575
+ };
576
+ }
577
+ if (verbose && parserErr.stack) {
578
+ stderrText2 += parserErr.stack + "\n";
579
+ }
580
+ return { stdout: "", stderr: stderrText2, exitCode: 8 };
581
+ }
582
+ const message = err instanceof Error ? err.message : String(err);
583
+ const stack = err instanceof Error ? err.stack : void 0;
584
+ const stderrPrefix = `INTERNAL: ${message}
585
+ `;
586
+ if (json) {
587
+ return {
588
+ stdout: buildErrorEnvelope("INTERNAL", message),
589
+ stderr: verbose && stack ? stack + "\n" : "",
590
+ exitCode: 1
591
+ };
592
+ }
593
+ let stderrText = stderrPrefix;
594
+ if (verbose && stack) {
595
+ stderrText += stack + "\n";
596
+ }
597
+ return { stdout: "", stderr: stderrText, exitCode: 1 };
598
+ }
599
+
600
+ // src/lib/ratio-command.ts
601
+ var RatioCommand = class extends Command {
602
+ _getRenderFlags() {
603
+ const argv = this.argv;
604
+ return {
605
+ verbose: argv.includes("--verbose"),
606
+ json: argv.includes("--json")
607
+ };
608
+ }
609
+ async catch(err) {
610
+ const { verbose, json } = this._getRenderFlags();
611
+ const output = renderError({ err, verbose, json });
612
+ if (output.stdout) process.stdout.write(output.stdout);
613
+ if (output.stderr) process.stderr.write(output.stderr);
614
+ if (output.exitCode === 0) {
615
+ throw new Errors.ExitError(0);
616
+ }
617
+ err.skipOclifErrorHandling = true;
618
+ err.oclif = { exit: output.exitCode };
619
+ throw err;
620
+ }
621
+ };
622
+
623
+ // src/lib/verbose-flag.ts
624
+ import { Flags } from "@oclif/core";
625
+ var verboseFlag = {
626
+ verbose: Flags.boolean({
627
+ description: "Show diagnostic detail: HTTP status/timing lines for auth requests (stderr) and stack traces for internal errors."
628
+ })
629
+ };
630
+ var jsonFlag = {
631
+ json: Flags.boolean({
632
+ description: "Emit machine-readable JSON on stdout."
633
+ })
634
+ };
635
+
636
+ // src/commands/app/dev.ts
637
+ var AppDev = class _AppDev extends RatioCommand {
638
+ static description = `Run the current app locally: starts the manifest's dev command and a webhook listener side by side with prefixed logs from both.
639
+
640
+ Reads ratio.config.jsonc from the current directory ("devCommand" is required; "devServerPort" is optional and, when set, is passed to your dev command as PORT). If either process stops, the other is shut down and the failing exit code is propagated. Ctrl+C stops both cleanly.
641
+
642
+ Exit codes: 0 success; 3 not signed in; 5 network failure; 8 invalid configuration; 10 a child process could not be started; 11 a child had to be forcibly terminated during shutdown; 130/143 interrupted or terminated.`;
643
+ static examples = [
644
+ "<%= config.bin %> app dev",
645
+ "<%= config.bin %> app dev --verbose"
646
+ ];
647
+ static flags = {
648
+ ...verboseFlag
649
+ };
650
+ // ── Seams: overridable in tests; defaults hit the real process. ──────────
651
+ writeOut = (chunk) => {
652
+ process.stdout.write(chunk);
653
+ };
654
+ writeErr = (chunk) => {
655
+ process.stderr.write(chunk);
656
+ };
657
+ signalSource = process;
658
+ hardExit = (code) => {
659
+ process.exit(code);
660
+ };
661
+ getCwd = () => process.cwd();
662
+ envReader = () => process.env;
663
+ /** Test seam: spawner for the app child (real spawn when undefined). */
664
+ spawnApp;
665
+ /** Test seam: spawner for the listener child (real spawn when undefined). */
666
+ spawnListen;
667
+ /** Test seam: SIGTERM → SIGKILL grace override (default drain when undefined). */
668
+ drainMs;
669
+ async run() {
670
+ const { flags } = await this.parse(_AppDev);
671
+ const cwd = this.getCwd();
672
+ const env = this.envReader();
673
+ const manifest = readManifest(cwd);
674
+ const appArgv = tokenizeDevCommand(manifest.devCommand);
675
+ const preview = createPreviewController();
676
+ const code = await orchestrate({
677
+ appArgv,
678
+ devServerPort: manifest.devServerPort,
679
+ cwd,
680
+ env,
681
+ preview,
682
+ verbose: flags.verbose === true,
683
+ io: {
684
+ writeOut: (chunk) => this.writeOut(chunk),
685
+ writeErr: (chunk) => this.writeErr(chunk)
686
+ },
687
+ signalSource: this.signalSource,
688
+ hardExit: (exitCode) => this.hardExit(exitCode),
689
+ ...this.spawnApp !== void 0 ? { spawnApp: this.spawnApp } : {},
690
+ ...this.spawnListen !== void 0 ? { spawnListen: this.spawnListen } : {},
691
+ ...this.drainMs !== void 0 ? { drainMs: this.drainMs } : {}
692
+ });
693
+ if (code !== 0) {
694
+ this.hardExit(code);
695
+ }
696
+ }
697
+ };
698
+ export {
699
+ AppDev as default
700
+ };
701
+ //# sourceMappingURL=dev.js.map