@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.
- package/README.md +504 -0
- package/bin/run.mjs +5 -0
- package/dist/commands/__test__/throw.d.ts +25 -0
- package/dist/commands/__test__/throw.js +238 -0
- package/dist/commands/__test__/throw.js.map +1 -0
- package/dist/commands/app/create.d.ts +55 -0
- package/dist/commands/app/create.js +609 -0
- package/dist/commands/app/create.js.map +1 -0
- package/dist/commands/app/deploy.d.ts +14 -0
- package/dist/commands/app/deploy.js +179 -0
- package/dist/commands/app/deploy.js.map +1 -0
- package/dist/commands/app/dev.d.ts +28 -0
- package/dist/commands/app/dev.js +701 -0
- package/dist/commands/app/dev.js.map +1 -0
- package/dist/commands/app/generate.d.ts +14 -0
- package/dist/commands/app/generate.js +179 -0
- package/dist/commands/app/generate.js.map +1 -0
- package/dist/commands/app/info.d.ts +14 -0
- package/dist/commands/app/info.js +179 -0
- package/dist/commands/app/info.js.map +1 -0
- package/dist/commands/app/link.d.ts +118 -0
- package/dist/commands/app/link.js +1120 -0
- package/dist/commands/app/link.js.map +1 -0
- package/dist/commands/auth/login.d.ts +43 -0
- package/dist/commands/auth/login.js +987 -0
- package/dist/commands/auth/login.js.map +1 -0
- package/dist/commands/auth/logout.d.ts +15 -0
- package/dist/commands/auth/logout.js +486 -0
- package/dist/commands/auth/logout.js.map +1 -0
- package/dist/commands/auth/whoami.d.ts +16 -0
- package/dist/commands/auth/whoami.js +531 -0
- package/dist/commands/auth/whoami.js.map +1 -0
- package/dist/commands/dev/listen.d.ts +84 -0
- package/dist/commands/dev/listen.js +1187 -0
- package/dist/commands/dev/listen.js.map +1 -0
- package/dist/commands/dev/trigger.d.ts +88 -0
- package/dist/commands/dev/trigger.js +729 -0
- package/dist/commands/dev/trigger.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +617 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/auth/authorize-code-exchange.d.ts +50 -0
- package/dist/lib/auth/authorize-code-exchange.js +114 -0
- package/dist/lib/auth/authorize-code-exchange.js.map +1 -0
- package/dist/lib/auth/authorize-url.d.ts +42 -0
- package/dist/lib/auth/authorize-url.js +35 -0
- package/dist/lib/auth/authorize-url.js.map +1 -0
- package/dist/lib/auth/browser.d.ts +16 -0
- package/dist/lib/auth/browser.js +48 -0
- package/dist/lib/auth/browser.js.map +1 -0
- package/dist/lib/auth/config.d.ts +41 -0
- package/dist/lib/auth/config.js +21 -0
- package/dist/lib/auth/config.js.map +1 -0
- package/dist/lib/auth/http-refresher.d.ts +30 -0
- package/dist/lib/auth/http-refresher.js +103 -0
- package/dist/lib/auth/http-refresher.js.map +1 -0
- package/dist/lib/auth/identity.d.ts +26 -0
- package/dist/lib/auth/identity.js +10 -0
- package/dist/lib/auth/identity.js.map +1 -0
- package/dist/lib/auth/index.d.ts +13 -0
- package/dist/lib/auth/index.js +527 -0
- package/dist/lib/auth/index.js.map +1 -0
- package/dist/lib/auth/loopback-server.d.ts +65 -0
- package/dist/lib/auth/loopback-server.js +245 -0
- package/dist/lib/auth/loopback-server.js.map +1 -0
- package/dist/lib/auth/manual-prompt.d.ts +17 -0
- package/dist/lib/auth/manual-prompt.js +20 -0
- package/dist/lib/auth/manual-prompt.js.map +1 -0
- package/dist/lib/auth/pkce.d.ts +24 -0
- package/dist/lib/auth/pkce.js +15 -0
- package/dist/lib/auth/pkce.js.map +1 -0
- package/dist/lib/auth/state.d.ts +20 -0
- package/dist/lib/auth/state.js +19 -0
- package/dist/lib/auth/state.js.map +1 -0
- package/dist/lib/auth/verbose-trace.d.ts +25 -0
- package/dist/lib/auth/verbose-trace.js +17 -0
- package/dist/lib/auth/verbose-trace.js.map +1 -0
- package/dist/lib/credentials/clock.d.ts +18 -0
- package/dist/lib/credentials/clock.js +10 -0
- package/dist/lib/credentials/clock.js.map +1 -0
- package/dist/lib/credentials/index.d.ts +5 -0
- package/dist/lib/credentials/index.js +340 -0
- package/dist/lib/credentials/index.js.map +1 -0
- package/dist/lib/credentials/path.d.ts +26 -0
- package/dist/lib/credentials/path.js +32 -0
- package/dist/lib/credentials/path.js.map +1 -0
- package/dist/lib/credentials/refresher.d.ts +26 -0
- package/dist/lib/credentials/refresher.js +34 -0
- package/dist/lib/credentials/refresher.js.map +1 -0
- package/dist/lib/credentials/schema.d.ts +138 -0
- package/dist/lib/credentials/schema.js +85 -0
- package/dist/lib/credentials/schema.js.map +1 -0
- package/dist/lib/credentials/store.d.ts +102 -0
- package/dist/lib/credentials/store.js +337 -0
- package/dist/lib/credentials/store.js.map +1 -0
- package/dist/lib/credentials/types.d.ts +65 -0
- package/dist/lib/credentials/types.js +1 -0
- package/dist/lib/credentials/types.js.map +1 -0
- package/dist/lib/dev-store-gate.d.ts +66 -0
- package/dist/lib/dev-store-gate.js +15 -0
- package/dist/lib/dev-store-gate.js.map +1 -0
- package/dist/lib/errors.d.ts +60 -0
- package/dist/lib/errors.js +101 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/manifest/index.d.ts +2 -0
- package/dist/lib/manifest/index.js +144 -0
- package/dist/lib/manifest/index.js.map +1 -0
- package/dist/lib/manifest/read-manifest.d.ts +32 -0
- package/dist/lib/manifest/read-manifest.js +87 -0
- package/dist/lib/manifest/read-manifest.js.map +1 -0
- package/dist/lib/manifest/write-client-id.d.ts +33 -0
- package/dist/lib/manifest/write-client-id.js +94 -0
- package/dist/lib/manifest/write-client-id.js.map +1 -0
- package/dist/lib/messages.d.ts +15 -0
- package/dist/lib/messages.js +16 -0
- package/dist/lib/messages.js.map +1 -0
- package/dist/lib/orchestrator/child-runner.d.ts +140 -0
- package/dist/lib/orchestrator/child-runner.js +471 -0
- package/dist/lib/orchestrator/child-runner.js.map +1 -0
- package/dist/lib/preview/index.d.ts +11 -0
- package/dist/lib/preview/index.js +17 -0
- package/dist/lib/preview/index.js.map +1 -0
- package/dist/lib/preview/types.d.ts +16 -0
- package/dist/lib/preview/types.js +11 -0
- package/dist/lib/preview/types.js.map +1 -0
- package/dist/lib/ratio-command.d.ts +52 -0
- package/dist/lib/ratio-command.js +141 -0
- package/dist/lib/ratio-command.js.map +1 -0
- package/dist/lib/relay/index.d.ts +7 -0
- package/dist/lib/relay/index.js +362 -0
- package/dist/lib/relay/index.js.map +1 -0
- package/dist/lib/relay/relay-client.d.ts +91 -0
- package/dist/lib/relay/relay-client.js +362 -0
- package/dist/lib/relay/relay-client.js.map +1 -0
- package/dist/lib/relay/types.d.ts +71 -0
- package/dist/lib/relay/types.js +1 -0
- package/dist/lib/relay/types.js.map +1 -0
- package/dist/lib/render-error.d.ts +35 -0
- package/dist/lib/render-error.js +115 -0
- package/dist/lib/render-error.js.map +1 -0
- package/dist/lib/verbose-flag.d.ts +12 -0
- package/dist/lib/verbose-flag.js +17 -0
- package/dist/lib/verbose-flag.js.map +1 -0
- package/dist/render-CrHGqTPH.d.ts +115 -0
- package/dist/templates/.gitkeep +0 -0
- package/dist/templates/minimal/.env.example.tmpl +4 -0
- package/dist/templates/minimal/README.md.tmpl +26 -0
- package/dist/templates/minimal/gitignore +5 -0
- package/dist/templates/minimal/manifest.json +5 -0
- package/dist/templates/minimal/package.json.tmpl +25 -0
- package/dist/templates/minimal/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/minimal/src/index.ts.tmpl +26 -0
- package/dist/templates/minimal/src/install.ts.tmpl +37 -0
- package/dist/templates/minimal/src/webhooks.ts.tmpl +57 -0
- package/dist/templates/minimal/tsconfig.json +17 -0
- package/dist/templates/serverless/README.md.tmpl +28 -0
- package/dist/templates/serverless/gitignore +6 -0
- package/dist/templates/serverless/manifest.json +5 -0
- package/dist/templates/serverless/package.json.tmpl +22 -0
- package/dist/templates/serverless/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/serverless/src/index.ts.tmpl +20 -0
- package/dist/templates/serverless/src/install.ts.tmpl +20 -0
- package/dist/templates/serverless/src/webhooks.ts.tmpl +36 -0
- package/dist/templates/serverless/tsconfig.json +18 -0
- package/dist/templates/serverless/wrangler.toml.tmpl +9 -0
- package/dist/templates/with-admin-ui/.env.example.tmpl +4 -0
- package/dist/templates/with-admin-ui/README.md.tmpl +28 -0
- package/dist/templates/with-admin-ui/gitignore +6 -0
- package/dist/templates/with-admin-ui/manifest.json +5 -0
- package/dist/templates/with-admin-ui/package.json.tmpl +31 -0
- package/dist/templates/with-admin-ui/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/with-admin-ui/src/admin/app/globals.css +19 -0
- package/dist/templates/with-admin-ui/src/admin/app/layout.tsx.tmpl +13 -0
- package/dist/templates/with-admin-ui/src/admin/app/page.tsx.tmpl +8 -0
- package/dist/templates/with-admin-ui/src/admin/next-env.d.ts.tmpl +5 -0
- package/dist/templates/with-admin-ui/src/admin/next.config.mjs +3 -0
- package/dist/templates/with-admin-ui/src/admin/tsconfig.json +20 -0
- package/dist/templates/with-admin-ui/src/index.ts.tmpl +26 -0
- package/dist/templates/with-admin-ui/src/install.ts.tmpl +37 -0
- package/dist/templates/with-admin-ui/src/webhooks.ts.tmpl +57 -0
- package/dist/templates/with-admin-ui/tsconfig.json +17 -0
- package/package.json +78 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// src/lib/orchestrator/child-runner.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
|
|
4
|
+
// src/lib/errors.ts
|
|
5
|
+
var RatioCLIError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
exitCode;
|
|
8
|
+
hint;
|
|
9
|
+
docsUrl;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
super(opts.message);
|
|
12
|
+
this.name = new.target.name;
|
|
13
|
+
this.code = opts.code;
|
|
14
|
+
this.exitCode = opts.exitCode;
|
|
15
|
+
if (opts.hint !== void 0) this.hint = opts.hint;
|
|
16
|
+
if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var ValidationError = class extends RatioCLIError {
|
|
20
|
+
constructor(message, hint) {
|
|
21
|
+
const opts = {
|
|
22
|
+
code: "VALIDATION_ERROR",
|
|
23
|
+
exitCode: 8,
|
|
24
|
+
message
|
|
25
|
+
};
|
|
26
|
+
if (hint !== void 0) opts.hint = hint;
|
|
27
|
+
super(opts);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var OrchestratorSpawnError = class extends RatioCLIError {
|
|
31
|
+
constructor(message, hint = "Could not start your app's dev command \u2014 check that the command in ratio.config.jsonc runs from this directory.") {
|
|
32
|
+
super({ code: "ORCHESTRATOR_SPAWN", exitCode: 10, message, hint });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var OrchestratorDrainTimeoutError = class extends RatioCLIError {
|
|
36
|
+
constructor(message, hint = "Your dev command did not exit cleanly within 5 seconds \u2014 it was forcibly terminated.") {
|
|
37
|
+
super({ code: "ORCHESTRATOR_DRAIN", exitCode: 11, message, hint });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/lib/manifest/read-manifest.ts
|
|
42
|
+
import fs2 from "fs";
|
|
43
|
+
import path2 from "path";
|
|
44
|
+
|
|
45
|
+
// src/lib/manifest/write-client-id.ts
|
|
46
|
+
import crypto from "crypto";
|
|
47
|
+
import fs from "fs";
|
|
48
|
+
import path from "path";
|
|
49
|
+
|
|
50
|
+
// src/lib/manifest/read-manifest.ts
|
|
51
|
+
var DEV_COMMAND_HINT = 'Add a "devCommand" entry to ratio.config.jsonc (e.g. "npm run dev") and re-run.';
|
|
52
|
+
|
|
53
|
+
// src/lib/orchestrator/child-runner.ts
|
|
54
|
+
var DRAIN_MS = 5e3;
|
|
55
|
+
var RACE_WINDOW_MS = 200;
|
|
56
|
+
var SIGNAL_FORCE_EXIT_WINDOW_MS = 2e3;
|
|
57
|
+
var CHILD_BUFFER_MAX_BYTES = 64 * 1024;
|
|
58
|
+
var DEV_COMMAND_TOKENIZE_HINT = "devCommand must be a simple whitespace-separated command; complex shell pipelines are not supported.";
|
|
59
|
+
var DEFAULT_FORWARD_PORT = 3001;
|
|
60
|
+
function tokenizeDevCommand(raw) {
|
|
61
|
+
const tokens = [];
|
|
62
|
+
let current = "";
|
|
63
|
+
let quote;
|
|
64
|
+
let hasToken = false;
|
|
65
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
66
|
+
const ch = raw.charAt(i);
|
|
67
|
+
if (quote !== void 0) {
|
|
68
|
+
if (ch === quote) {
|
|
69
|
+
quote = void 0;
|
|
70
|
+
} else {
|
|
71
|
+
current += ch;
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (ch === '"' || ch === "'") {
|
|
76
|
+
quote = ch;
|
|
77
|
+
hasToken = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
81
|
+
if (hasToken) {
|
|
82
|
+
tokens.push(current);
|
|
83
|
+
current = "";
|
|
84
|
+
hasToken = false;
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (ch === "|" || ch === "&" || ch === ";" || ch === "<" || ch === ">" || ch === "`") {
|
|
89
|
+
throw new ValidationError(
|
|
90
|
+
`devCommand contains an unsupported shell operator: ${ch}`,
|
|
91
|
+
DEV_COMMAND_TOKENIZE_HINT
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (ch === "$" && raw.charAt(i + 1) === "(") {
|
|
95
|
+
throw new ValidationError(
|
|
96
|
+
"devCommand contains an unsupported shell substitution.",
|
|
97
|
+
DEV_COMMAND_TOKENIZE_HINT
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
current += ch;
|
|
101
|
+
hasToken = true;
|
|
102
|
+
}
|
|
103
|
+
if (quote !== void 0) {
|
|
104
|
+
throw new ValidationError(
|
|
105
|
+
"devCommand has an unterminated quote.",
|
|
106
|
+
DEV_COMMAND_TOKENIZE_HINT
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (hasToken) {
|
|
110
|
+
tokens.push(current);
|
|
111
|
+
}
|
|
112
|
+
if (tokens.length === 0 || tokens[0] === "") {
|
|
113
|
+
throw new ValidationError("devCommand is empty.", DEV_COMMAND_HINT);
|
|
114
|
+
}
|
|
115
|
+
return tokens;
|
|
116
|
+
}
|
|
117
|
+
var LinePrefixer = class {
|
|
118
|
+
constructor(prefix, write) {
|
|
119
|
+
this.prefix = prefix;
|
|
120
|
+
this.write = write;
|
|
121
|
+
}
|
|
122
|
+
prefix;
|
|
123
|
+
write;
|
|
124
|
+
buffer = Buffer.alloc(0);
|
|
125
|
+
push(chunk) {
|
|
126
|
+
const incoming = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
127
|
+
if (incoming.length === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.buffer = this.buffer.length === 0 ? incoming : Buffer.concat([this.buffer, incoming]);
|
|
131
|
+
this.drainCompleteLines();
|
|
132
|
+
if (this.buffer.length > CHILD_BUFFER_MAX_BYTES) {
|
|
133
|
+
this.flush();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Emit any buffered partial line (with prefix + newline), then reset. */
|
|
137
|
+
flush() {
|
|
138
|
+
if (this.buffer.length > 0) {
|
|
139
|
+
this.write(`${this.prefix}${this.buffer.toString("utf8")}
|
|
140
|
+
`);
|
|
141
|
+
this.buffer = Buffer.alloc(0);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
drainCompleteLines() {
|
|
145
|
+
let newlineAt = this.buffer.indexOf(10);
|
|
146
|
+
while (newlineAt !== -1) {
|
|
147
|
+
const line = this.buffer.subarray(0, newlineAt).toString("utf8");
|
|
148
|
+
this.buffer = this.buffer.subarray(newlineAt + 1);
|
|
149
|
+
this.write(`${this.prefix}${line}
|
|
150
|
+
`);
|
|
151
|
+
newlineAt = this.buffer.indexOf(10);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
var SIGNAL_EXIT_CODES = {
|
|
156
|
+
SIGHUP: 129,
|
|
157
|
+
SIGINT: 130,
|
|
158
|
+
SIGQUIT: 131,
|
|
159
|
+
SIGKILL: 137,
|
|
160
|
+
SIGTERM: 143
|
|
161
|
+
};
|
|
162
|
+
function exitCodeFrom(code, signal) {
|
|
163
|
+
if (code !== null) {
|
|
164
|
+
return code;
|
|
165
|
+
}
|
|
166
|
+
if (signal !== null) {
|
|
167
|
+
return SIGNAL_EXIT_CODES[signal] ?? 1;
|
|
168
|
+
}
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
var ExitArbiter = class {
|
|
172
|
+
decidingChildCode;
|
|
173
|
+
signalCode;
|
|
174
|
+
get shutdownInitiated() {
|
|
175
|
+
return this.decidingChildCode !== void 0 || this.signalCode !== void 0;
|
|
176
|
+
}
|
|
177
|
+
observeChildExit(code, signal) {
|
|
178
|
+
if (this.shutdownInitiated) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.decidingChildCode = exitCodeFrom(code, signal);
|
|
182
|
+
}
|
|
183
|
+
observeSignal(sig) {
|
|
184
|
+
if (this.shutdownInitiated) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.signalCode = sig === "SIGINT" ? 130 : 143;
|
|
188
|
+
}
|
|
189
|
+
finalCode() {
|
|
190
|
+
return this.signalCode ?? this.decidingChildCode ?? 0;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
var defaultSpawn = (command, args, options) => spawn(command, [...args], {
|
|
194
|
+
cwd: options.cwd,
|
|
195
|
+
env: options.env,
|
|
196
|
+
stdio: options.stdio,
|
|
197
|
+
shell: options.shell
|
|
198
|
+
});
|
|
199
|
+
function buildListenInvocation(env, devServerPort, argv1) {
|
|
200
|
+
const override = env.RATIO_DEV_LISTEN_BIN;
|
|
201
|
+
const listenBin = override ?? argv1;
|
|
202
|
+
if (listenBin === void 0 || listenBin === "") {
|
|
203
|
+
throw new OrchestratorSpawnError(
|
|
204
|
+
"Could not locate the CLI binary to start the webhook listener.",
|
|
205
|
+
"Re-install the CLI, or invoke it through its bin entry point."
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const listenArgs = override ? [] : [
|
|
209
|
+
"dev",
|
|
210
|
+
"listen",
|
|
211
|
+
"--forward-to",
|
|
212
|
+
`localhost:${devServerPort ?? DEFAULT_FORWARD_PORT}/webhooks`
|
|
213
|
+
];
|
|
214
|
+
return { command: process.execPath, args: [listenBin, ...listenArgs] };
|
|
215
|
+
}
|
|
216
|
+
function describeCause(cause) {
|
|
217
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
218
|
+
}
|
|
219
|
+
async function orchestrate(options) {
|
|
220
|
+
const { io } = options;
|
|
221
|
+
const drainMs = options.drainMs ?? DRAIN_MS;
|
|
222
|
+
const spawnApp = options.spawnApp ?? defaultSpawn;
|
|
223
|
+
const spawnListen = options.spawnListen ?? defaultSpawn;
|
|
224
|
+
const trace = (line) => {
|
|
225
|
+
if (options.verbose) {
|
|
226
|
+
io.writeErr(`[verbose] ${line}
|
|
227
|
+
`);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const appCommand = options.appArgv[0];
|
|
231
|
+
if (appCommand === void 0 || appCommand === "") {
|
|
232
|
+
throw new ValidationError("devCommand is empty.", DEV_COMMAND_HINT);
|
|
233
|
+
}
|
|
234
|
+
const appEnv = {
|
|
235
|
+
...options.env,
|
|
236
|
+
...options.devServerPort !== void 0 ? { PORT: String(options.devServerPort) } : {},
|
|
237
|
+
RATIO_ORCHESTRATED: "1"
|
|
238
|
+
};
|
|
239
|
+
let app;
|
|
240
|
+
try {
|
|
241
|
+
app = spawnApp(appCommand, options.appArgv.slice(1), {
|
|
242
|
+
cwd: options.cwd,
|
|
243
|
+
env: appEnv,
|
|
244
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
245
|
+
shell: false
|
|
246
|
+
});
|
|
247
|
+
} catch (cause) {
|
|
248
|
+
throw new OrchestratorSpawnError(
|
|
249
|
+
`Could not start the dev command (${appCommand}): ${describeCause(cause)}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
trace(`spawn app: ${appCommand} (${app.pid ?? "-"})`);
|
|
253
|
+
const invocation = buildListenInvocation(
|
|
254
|
+
options.env,
|
|
255
|
+
options.devServerPort,
|
|
256
|
+
options.argv1 ?? process.argv[1]
|
|
257
|
+
);
|
|
258
|
+
const listenEnv = { ...options.env, RATIO_ORCHESTRATED: "1" };
|
|
259
|
+
let listen;
|
|
260
|
+
try {
|
|
261
|
+
listen = spawnListen(invocation.command, invocation.args, {
|
|
262
|
+
cwd: options.cwd,
|
|
263
|
+
env: listenEnv,
|
|
264
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
265
|
+
shell: false
|
|
266
|
+
});
|
|
267
|
+
} catch (cause) {
|
|
268
|
+
app.kill("SIGTERM");
|
|
269
|
+
throw new OrchestratorSpawnError(
|
|
270
|
+
`Could not start the webhook listener: ${describeCause(cause)}`,
|
|
271
|
+
"The bundled webhook listener could not be started; re-install the CLI if this persists."
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
trace(`spawn listen: (${listen.pid ?? "-"})`);
|
|
275
|
+
const appOut = new LinePrefixer("[app] ", (chunk) => io.writeOut(chunk));
|
|
276
|
+
const appErrOut = new LinePrefixer("[app] ", (chunk) => io.writeOut(chunk));
|
|
277
|
+
const listenOut = new LinePrefixer("[listen] ", (chunk) => io.writeOut(chunk));
|
|
278
|
+
const listenErrOut = new LinePrefixer("[listen] ", (chunk) => io.writeOut(chunk));
|
|
279
|
+
const pipe = (stream, prefixer) => {
|
|
280
|
+
if (stream !== null) {
|
|
281
|
+
stream.on("data", (chunk) => prefixer.push(chunk));
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
pipe(app.stdout, appOut);
|
|
285
|
+
pipe(app.stderr, appErrOut);
|
|
286
|
+
pipe(listen.stdout, listenOut);
|
|
287
|
+
pipe(listen.stderr, listenErrOut);
|
|
288
|
+
const arbiter = new ExitArbiter();
|
|
289
|
+
let appExited = false;
|
|
290
|
+
let listenExited = false;
|
|
291
|
+
let spawnFailure;
|
|
292
|
+
let drainEscalated = false;
|
|
293
|
+
let drainTimer;
|
|
294
|
+
let drainStartedAt = 0;
|
|
295
|
+
let firstSignalAt = 0;
|
|
296
|
+
return await new Promise((resolve, reject) => {
|
|
297
|
+
let settled = false;
|
|
298
|
+
const cleanup = () => {
|
|
299
|
+
options.signalSource.removeListener("SIGINT", onSigint);
|
|
300
|
+
options.signalSource.removeListener("SIGTERM", onSigterm);
|
|
301
|
+
if (drainTimer !== void 0) {
|
|
302
|
+
clearTimeout(drainTimer);
|
|
303
|
+
drainTimer = void 0;
|
|
304
|
+
}
|
|
305
|
+
appOut.flush();
|
|
306
|
+
appErrOut.flush();
|
|
307
|
+
listenOut.flush();
|
|
308
|
+
listenErrOut.flush();
|
|
309
|
+
};
|
|
310
|
+
const settle = (code) => {
|
|
311
|
+
if (!settled) {
|
|
312
|
+
settled = true;
|
|
313
|
+
cleanup();
|
|
314
|
+
resolve(code);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const fail = (err) => {
|
|
318
|
+
if (!settled) {
|
|
319
|
+
settled = true;
|
|
320
|
+
cleanup();
|
|
321
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
const startDrain = () => {
|
|
325
|
+
if (drainTimer !== void 0) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
drainStartedAt = Date.now();
|
|
329
|
+
drainTimer = setTimeout(() => {
|
|
330
|
+
if (!appExited) {
|
|
331
|
+
app.kill("SIGKILL");
|
|
332
|
+
drainEscalated = true;
|
|
333
|
+
}
|
|
334
|
+
if (!listenExited) {
|
|
335
|
+
listen.kill("SIGKILL");
|
|
336
|
+
drainEscalated = true;
|
|
337
|
+
}
|
|
338
|
+
}, drainMs);
|
|
339
|
+
};
|
|
340
|
+
const finishIfDone = () => {
|
|
341
|
+
if (!appExited || !listenExited) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (drainStartedAt > 0) {
|
|
345
|
+
trace(`drain elapsed: ${Date.now() - drainStartedAt}ms`);
|
|
346
|
+
}
|
|
347
|
+
const code = arbiter.finalCode();
|
|
348
|
+
void options.preview.onShutdown().then(
|
|
349
|
+
() => {
|
|
350
|
+
if (spawnFailure !== void 0) {
|
|
351
|
+
fail(spawnFailure);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (drainEscalated && code === 0) {
|
|
355
|
+
fail(
|
|
356
|
+
new OrchestratorDrainTimeoutError(
|
|
357
|
+
"A child process had to be forcibly terminated during shutdown."
|
|
358
|
+
)
|
|
359
|
+
);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
settle(code);
|
|
363
|
+
},
|
|
364
|
+
(err) => fail(err)
|
|
365
|
+
);
|
|
366
|
+
};
|
|
367
|
+
const onAppExit = (code, signal) => {
|
|
368
|
+
appExited = true;
|
|
369
|
+
appOut.flush();
|
|
370
|
+
appErrOut.flush();
|
|
371
|
+
trace(`app exited: code=${code} signal=${signal}`);
|
|
372
|
+
const alreadyShuttingDown = arbiter.shutdownInitiated;
|
|
373
|
+
arbiter.observeChildExit(code, signal);
|
|
374
|
+
if (!alreadyShuttingDown && !listenExited) {
|
|
375
|
+
listen.kill("SIGTERM");
|
|
376
|
+
startDrain();
|
|
377
|
+
}
|
|
378
|
+
finishIfDone();
|
|
379
|
+
};
|
|
380
|
+
const onListenExit = (code, signal) => {
|
|
381
|
+
listenExited = true;
|
|
382
|
+
listenOut.flush();
|
|
383
|
+
listenErrOut.flush();
|
|
384
|
+
trace(`listen exited: code=${code} signal=${signal}`);
|
|
385
|
+
const alreadyShuttingDown = arbiter.shutdownInitiated;
|
|
386
|
+
arbiter.observeChildExit(code, signal);
|
|
387
|
+
if (!alreadyShuttingDown && !appExited) {
|
|
388
|
+
app.kill("SIGTERM");
|
|
389
|
+
startDrain();
|
|
390
|
+
}
|
|
391
|
+
finishIfDone();
|
|
392
|
+
};
|
|
393
|
+
const onAppError = (err) => {
|
|
394
|
+
appExited = true;
|
|
395
|
+
if (spawnFailure === void 0) {
|
|
396
|
+
spawnFailure = new OrchestratorSpawnError(
|
|
397
|
+
`Could not start the dev command (${appCommand}): ${err.message}`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
arbiter.observeChildExit(1, null);
|
|
401
|
+
if (!listenExited) {
|
|
402
|
+
listen.kill("SIGTERM");
|
|
403
|
+
startDrain();
|
|
404
|
+
}
|
|
405
|
+
finishIfDone();
|
|
406
|
+
};
|
|
407
|
+
const onListenError = (err) => {
|
|
408
|
+
listenExited = true;
|
|
409
|
+
if (spawnFailure === void 0) {
|
|
410
|
+
spawnFailure = new OrchestratorSpawnError(
|
|
411
|
+
`Could not start the webhook listener: ${err.message}`,
|
|
412
|
+
"The bundled webhook listener could not be started; re-install the CLI if this persists."
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
arbiter.observeChildExit(1, null);
|
|
416
|
+
if (!appExited) {
|
|
417
|
+
app.kill("SIGTERM");
|
|
418
|
+
startDrain();
|
|
419
|
+
}
|
|
420
|
+
finishIfDone();
|
|
421
|
+
};
|
|
422
|
+
const onSignal = (sig) => {
|
|
423
|
+
trace(`signal received: ${sig}`);
|
|
424
|
+
if (arbiter.shutdownInitiated) {
|
|
425
|
+
if (firstSignalAt === 0) {
|
|
426
|
+
firstSignalAt = Date.now();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (Date.now() - firstSignalAt <= SIGNAL_FORCE_EXIT_WINDOW_MS) {
|
|
430
|
+
options.hardExit(130);
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
firstSignalAt = Date.now();
|
|
435
|
+
arbiter.observeSignal(sig);
|
|
436
|
+
io.writeErr("shutting down: stopping the dev command and the webhook listener\n");
|
|
437
|
+
if (!appExited) {
|
|
438
|
+
app.kill("SIGTERM");
|
|
439
|
+
}
|
|
440
|
+
if (!listenExited) {
|
|
441
|
+
listen.kill("SIGTERM");
|
|
442
|
+
}
|
|
443
|
+
startDrain();
|
|
444
|
+
};
|
|
445
|
+
const onSigint = () => onSignal("SIGINT");
|
|
446
|
+
const onSigterm = () => onSignal("SIGTERM");
|
|
447
|
+
options.signalSource.on("SIGINT", onSigint);
|
|
448
|
+
options.signalSource.on("SIGTERM", onSigterm);
|
|
449
|
+
app.once("exit", onAppExit);
|
|
450
|
+
app.once("error", onAppError);
|
|
451
|
+
listen.once("exit", onListenExit);
|
|
452
|
+
listen.once("error", onListenError);
|
|
453
|
+
trace(`preview: onDevServerReady(${options.devServerPort ?? 0})`);
|
|
454
|
+
void options.preview.onDevServerReady(options.devServerPort ?? 0).catch((err) => fail(err));
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
export {
|
|
458
|
+
CHILD_BUFFER_MAX_BYTES,
|
|
459
|
+
DEFAULT_FORWARD_PORT,
|
|
460
|
+
DEV_COMMAND_TOKENIZE_HINT,
|
|
461
|
+
DRAIN_MS,
|
|
462
|
+
ExitArbiter,
|
|
463
|
+
LinePrefixer,
|
|
464
|
+
RACE_WINDOW_MS,
|
|
465
|
+
SIGNAL_FORCE_EXIT_WINDOW_MS,
|
|
466
|
+
buildListenInvocation,
|
|
467
|
+
exitCodeFrom,
|
|
468
|
+
orchestrate,
|
|
469
|
+
tokenizeDevCommand
|
|
470
|
+
};
|
|
471
|
+
//# sourceMappingURL=child-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/orchestrator/child-runner.ts","../../../src/lib/errors.ts","../../../src/lib/manifest/read-manifest.ts","../../../src/lib/manifest/write-client-id.ts"],"sourcesContent":["/**\n * Child-process runner for `ratio app dev`.\n *\n * Spawns two children — the app's own dev command and the webhook\n * listener — multiplexes their output onto stdout with per-child line\n * prefixes, and arbitrates a single deterministic exit code out of child\n * crashes and delivery of SIGINT/SIGTERM to the orchestrator.\n *\n * Everything process-shaped is injectable (spawn functions, signal\n * source, output sinks, hard-exit) so the full lifecycle is testable\n * without real dev servers or real signals.\n */\n\nimport { spawn } from 'node:child_process';\nimport {\n OrchestratorDrainTimeoutError,\n OrchestratorSpawnError,\n ValidationError,\n} from '../errors.js';\nimport { DEV_COMMAND_HINT } from '../manifest/read-manifest.js';\nimport type { PreviewController } from '../preview/index.js';\n\n/** SIGTERM → SIGKILL grace window during shutdown. */\nexport const DRAIN_MS = 5_000;\n\n/**\n * Window within which \"both children crashed\" is arbitrated to the\n * first-observed exit. The arbiter is deterministic for every ordering\n * (first observed terminating cause always wins), which satisfies the\n * race rule for this window and beyond.\n */\nexport const RACE_WINDOW_MS = 200;\n\n/** A second interrupt inside this window forces an immediate exit (130). */\nexport const SIGNAL_FORCE_EXIT_WINDOW_MS = 2_000;\n\n/** Per-child line-buffer bound; overflow flushes a partial line. */\nexport const CHILD_BUFFER_MAX_BYTES = 64 * 1024;\n\n/** Hint shown when the dev command cannot be tokenized safely. */\nexport const DEV_COMMAND_TOKENIZE_HINT =\n 'devCommand must be a simple whitespace-separated command; complex shell pipelines are not supported.';\n\n/** Default forwarded-to port for the listener when the manifest has none. */\nexport const DEFAULT_FORWARD_PORT = 3001;\n\n// ── Tokenizer ──────────────────────────────────────────────────────────────\n\n/**\n * Split a dev command string into an argv array: whitespace-separated,\n * with single- or double-quoted segments kept together. Shell operators\n * (pipes, redirects, substitutions) are rejected outright — the command\n * is spawned without a shell, so they could never mean what the author\n * intended.\n */\nexport function tokenizeDevCommand(raw: string): string[] {\n const tokens: string[] = [];\n let current = '';\n let quote: '\"' | \"'\" | undefined;\n let hasToken = false;\n\n for (let i = 0; i < raw.length; i += 1) {\n const ch = raw.charAt(i);\n if (quote !== undefined) {\n if (ch === quote) {\n quote = undefined;\n } else {\n current += ch;\n }\n continue;\n }\n if (ch === '\"' || ch === \"'\") {\n quote = ch;\n hasToken = true;\n continue;\n }\n if (ch === ' ' || ch === '\\t' || ch === '\\n' || ch === '\\r') {\n if (hasToken) {\n tokens.push(current);\n current = '';\n hasToken = false;\n }\n continue;\n }\n if (ch === '|' || ch === '&' || ch === ';' || ch === '<' || ch === '>' || ch === '`') {\n throw new ValidationError(\n `devCommand contains an unsupported shell operator: ${ch}`,\n DEV_COMMAND_TOKENIZE_HINT,\n );\n }\n if (ch === '$' && raw.charAt(i + 1) === '(') {\n throw new ValidationError(\n 'devCommand contains an unsupported shell substitution.',\n DEV_COMMAND_TOKENIZE_HINT,\n );\n }\n current += ch;\n hasToken = true;\n }\n\n if (quote !== undefined) {\n throw new ValidationError(\n 'devCommand has an unterminated quote.',\n DEV_COMMAND_TOKENIZE_HINT,\n );\n }\n if (hasToken) {\n tokens.push(current);\n }\n if (tokens.length === 0 || tokens[0] === '') {\n throw new ValidationError('devCommand is empty.', DEV_COMMAND_HINT);\n }\n return tokens;\n}\n\n// ── Log multiplexer ────────────────────────────────────────────────────────\n\n/**\n * Per-stream line buffer that emits `<prefix><line>\\n` only on newline\n * boundaries, so chunks split mid-line (or mid-UTF-8-character) never\n * corrupt the prefixed output. Bounded to CHILD_BUFFER_MAX_BYTES: on\n * overflow, the partial line is flushed with the prefix and a newline.\n */\nexport class LinePrefixer {\n private buffer: Buffer = Buffer.alloc(0);\n\n constructor(\n private readonly prefix: string,\n private readonly write: (chunk: string) => void,\n ) {}\n\n push(chunk: Buffer | string): void {\n const incoming = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;\n if (incoming.length === 0) {\n return;\n }\n this.buffer = this.buffer.length === 0 ? incoming : Buffer.concat([this.buffer, incoming]);\n this.drainCompleteLines();\n if (this.buffer.length > CHILD_BUFFER_MAX_BYTES) {\n this.flush();\n }\n }\n\n /** Emit any buffered partial line (with prefix + newline), then reset. */\n flush(): void {\n if (this.buffer.length > 0) {\n this.write(`${this.prefix}${this.buffer.toString('utf8')}\\n`);\n this.buffer = Buffer.alloc(0);\n }\n }\n\n private drainCompleteLines(): void {\n let newlineAt = this.buffer.indexOf(0x0a);\n while (newlineAt !== -1) {\n const line = this.buffer.subarray(0, newlineAt).toString('utf8');\n this.buffer = this.buffer.subarray(newlineAt + 1);\n this.write(`${this.prefix}${line}\\n`);\n newlineAt = this.buffer.indexOf(0x0a);\n }\n }\n}\n\n// ── Exit-code arbitration ──────────────────────────────────────────────────\n\n/** POSIX-conventional exit codes for signal-terminated children. */\nconst SIGNAL_EXIT_CODES: Readonly<Partial<Record<NodeJS.Signals, number>>> = {\n SIGHUP: 129,\n SIGINT: 130,\n SIGQUIT: 131,\n SIGKILL: 137,\n SIGTERM: 143,\n};\n\n/** Map a child's (code, signal) exit pair to a single numeric exit code. */\nexport function exitCodeFrom(\n code: number | null,\n signal: NodeJS.Signals | null,\n): number {\n if (code !== null) {\n return code;\n }\n if (signal !== null) {\n return SIGNAL_EXIT_CODES[signal] ?? 1;\n }\n return 1;\n}\n\n/**\n * Deterministic exit-code arbiter: the FIRST observed terminating cause\n * (a child exit, or a signal delivered to the orchestrator) decides the\n * final code. Exits observed after shutdown started are the expected\n * result of our own SIGTERM and never change the outcome.\n */\nexport class ExitArbiter {\n private decidingChildCode: number | undefined;\n private signalCode: number | undefined;\n\n get shutdownInitiated(): boolean {\n return this.decidingChildCode !== undefined || this.signalCode !== undefined;\n }\n\n observeChildExit(code: number | null, signal: NodeJS.Signals | null): void {\n if (this.shutdownInitiated) {\n return;\n }\n this.decidingChildCode = exitCodeFrom(code, signal);\n }\n\n observeSignal(sig: 'SIGINT' | 'SIGTERM'): void {\n if (this.shutdownInitiated) {\n return;\n }\n this.signalCode = sig === 'SIGINT' ? 130 : 143;\n }\n\n finalCode(): number {\n return this.signalCode ?? this.decidingChildCode ?? 0;\n }\n}\n\n// ── Child spawning ─────────────────────────────────────────────────────────\n\n/** The exact spawn options the orchestrator uses for both children. */\nexport interface SpawnChildOptions {\n cwd: string;\n env: NodeJS.ProcessEnv;\n stdio: ['ignore', 'pipe', 'pipe'];\n shell: false;\n}\n\n/** Structural subset of ChildProcess the orchestrator relies on. */\nexport interface ChildLike {\n readonly pid?: number | undefined;\n readonly stdout: NodeJS.ReadableStream | null;\n readonly stderr: NodeJS.ReadableStream | null;\n kill(signal?: NodeJS.Signals): boolean;\n once(\n event: 'exit',\n listener: (code: number | null, signal: NodeJS.Signals | null) => void,\n ): unknown;\n once(event: 'error', listener: (err: Error) => void): unknown;\n}\n\nexport type SpawnFn = (\n command: string,\n args: readonly string[],\n options: SpawnChildOptions,\n) => ChildLike;\n\nconst defaultSpawn: SpawnFn = (command, args, options) =>\n spawn(command, [...args], {\n cwd: options.cwd,\n env: options.env,\n stdio: options.stdio,\n shell: options.shell,\n });\n\n/** Minimal process-signal surface, injectable so tests never send real signals. */\nexport interface SignalSource {\n on(event: 'SIGINT' | 'SIGTERM', listener: () => void): unknown;\n removeListener(event: 'SIGINT' | 'SIGTERM', listener: () => void): unknown;\n}\n\n/** How the listener child is invoked: always the current node executable. */\nexport interface ListenInvocation {\n command: string;\n args: string[];\n}\n\n/**\n * Build the listener child's invocation. By default the listener runs as\n * a sub-command of this same binary. The RATIO_DEV_LISTEN_BIN environment\n * variable substitutes a different script for the listener (a test seam;\n * unset in normal use, in which case the default binary path is used).\n */\nexport function buildListenInvocation(\n env: NodeJS.ProcessEnv,\n devServerPort: number | undefined,\n argv1: string | undefined,\n): ListenInvocation {\n const override = env.RATIO_DEV_LISTEN_BIN;\n const listenBin = override ?? argv1;\n if (listenBin === undefined || listenBin === '') {\n throw new OrchestratorSpawnError(\n 'Could not locate the CLI binary to start the webhook listener.',\n 'Re-install the CLI, or invoke it through its bin entry point.',\n );\n }\n const listenArgs = override\n ? []\n : [\n 'dev',\n 'listen',\n '--forward-to',\n `localhost:${devServerPort ?? DEFAULT_FORWARD_PORT}/webhooks`,\n ];\n return { command: process.execPath, args: [listenBin, ...listenArgs] };\n}\n\n// ── Orchestration ──────────────────────────────────────────────────────────\n\nexport interface OrchestratorIo {\n writeOut(chunk: string): void;\n writeErr(chunk: string): void;\n}\n\nexport interface OrchestrateOptions {\n /** Tokenized dev command; index 0 is the executable. */\n appArgv: string[];\n /** Optional port hint exposed to the app child as PORT. */\n devServerPort: number | undefined;\n cwd: string;\n env: NodeJS.ProcessEnv;\n preview: PreviewController;\n verbose: boolean;\n io: OrchestratorIo;\n signalSource: SignalSource;\n /** Immediate process exit for the second-interrupt fast path. */\n hardExit: (code: number) => void;\n /** Test seam: spawner for the app child. */\n spawnApp?: SpawnFn;\n /** Test seam: spawner for the listener child. */\n spawnListen?: SpawnFn;\n /** Test seam: shrink the SIGTERM → SIGKILL grace window. */\n drainMs?: number;\n /** Test seam: stand-in for the CLI's own script path. */\n argv1?: string;\n}\n\nfunction describeCause(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n\n/**\n * Run both children to completion and resolve the final exit code.\n * Rejects with a typed error when a child cannot be spawned (exit 10) or\n * when an otherwise-clean shutdown needed a SIGKILL escalation (exit 11).\n */\nexport async function orchestrate(options: OrchestrateOptions): Promise<number> {\n const { io } = options;\n const drainMs = options.drainMs ?? DRAIN_MS;\n const spawnApp = options.spawnApp ?? defaultSpawn;\n const spawnListen = options.spawnListen ?? defaultSpawn;\n const trace = (line: string): void => {\n if (options.verbose) {\n io.writeErr(`[verbose] ${line}\\n`);\n }\n };\n\n const appCommand = options.appArgv[0];\n if (appCommand === undefined || appCommand === '') {\n throw new ValidationError('devCommand is empty.', DEV_COMMAND_HINT);\n }\n\n // Child A: the app's own dev command. PORT is set only when the\n // manifest declares a devServerPort.\n const appEnv: NodeJS.ProcessEnv = {\n ...options.env,\n ...(options.devServerPort !== undefined\n ? { PORT: String(options.devServerPort) }\n : {}),\n RATIO_ORCHESTRATED: '1',\n };\n let app: ChildLike;\n try {\n app = spawnApp(appCommand, options.appArgv.slice(1), {\n cwd: options.cwd,\n env: appEnv,\n stdio: ['ignore', 'pipe', 'pipe'],\n shell: false,\n });\n } catch (cause) {\n throw new OrchestratorSpawnError(\n `Could not start the dev command (${appCommand}): ${describeCause(cause)}`,\n );\n }\n trace(`spawn app: ${appCommand} (${app.pid ?? '-'})`);\n\n // Child B: the webhook listener, run as a sub-command of this binary.\n const invocation = buildListenInvocation(\n options.env,\n options.devServerPort,\n options.argv1 ?? process.argv[1],\n );\n const listenEnv: NodeJS.ProcessEnv = { ...options.env, RATIO_ORCHESTRATED: '1' };\n let listen: ChildLike;\n try {\n listen = spawnListen(invocation.command, invocation.args, {\n cwd: options.cwd,\n env: listenEnv,\n stdio: ['ignore', 'pipe', 'pipe'],\n shell: false,\n });\n } catch (cause) {\n app.kill('SIGTERM');\n throw new OrchestratorSpawnError(\n `Could not start the webhook listener: ${describeCause(cause)}`,\n 'The bundled webhook listener could not be started; re-install the CLI if this persists.',\n );\n }\n trace(`spawn listen: (${listen.pid ?? '-'})`);\n\n // Log multiplexer: per-stream line buffers, one prefix per child.\n const appOut = new LinePrefixer('[app] ', (chunk) => io.writeOut(chunk));\n const appErrOut = new LinePrefixer('[app] ', (chunk) => io.writeOut(chunk));\n const listenOut = new LinePrefixer('[listen] ', (chunk) => io.writeOut(chunk));\n const listenErrOut = new LinePrefixer('[listen] ', (chunk) => io.writeOut(chunk));\n const pipe = (stream: NodeJS.ReadableStream | null, prefixer: LinePrefixer): void => {\n if (stream !== null) {\n stream.on('data', (chunk: Buffer | string) => prefixer.push(chunk));\n }\n };\n pipe(app.stdout, appOut);\n pipe(app.stderr, appErrOut);\n pipe(listen.stdout, listenOut);\n pipe(listen.stderr, listenErrOut);\n\n const arbiter = new ExitArbiter();\n let appExited = false;\n let listenExited = false;\n let spawnFailure: OrchestratorSpawnError | undefined;\n let drainEscalated = false;\n let drainTimer: NodeJS.Timeout | undefined;\n let drainStartedAt = 0;\n let firstSignalAt = 0;\n\n return await new Promise<number>((resolve, reject) => {\n let settled = false;\n\n const cleanup = (): void => {\n options.signalSource.removeListener('SIGINT', onSigint);\n options.signalSource.removeListener('SIGTERM', onSigterm);\n if (drainTimer !== undefined) {\n clearTimeout(drainTimer);\n drainTimer = undefined;\n }\n appOut.flush();\n appErrOut.flush();\n listenOut.flush();\n listenErrOut.flush();\n };\n\n const settle = (code: number): void => {\n if (!settled) {\n settled = true;\n cleanup();\n resolve(code);\n }\n };\n const fail = (err: unknown): void => {\n if (!settled) {\n settled = true;\n cleanup();\n reject(err instanceof Error ? err : new Error(String(err)));\n }\n };\n\n const startDrain = (): void => {\n if (drainTimer !== undefined) {\n return;\n }\n drainStartedAt = Date.now();\n drainTimer = setTimeout(() => {\n if (!appExited) {\n app.kill('SIGKILL');\n drainEscalated = true;\n }\n if (!listenExited) {\n listen.kill('SIGKILL');\n drainEscalated = true;\n }\n }, drainMs);\n };\n\n const finishIfDone = (): void => {\n if (!appExited || !listenExited) {\n return;\n }\n if (drainStartedAt > 0) {\n trace(`drain elapsed: ${Date.now() - drainStartedAt}ms`);\n }\n const code = arbiter.finalCode();\n void options.preview.onShutdown().then(\n () => {\n if (spawnFailure !== undefined) {\n fail(spawnFailure);\n return;\n }\n if (drainEscalated && code === 0) {\n fail(\n new OrchestratorDrainTimeoutError(\n 'A child process had to be forcibly terminated during shutdown.',\n ),\n );\n return;\n }\n settle(code);\n },\n (err: unknown) => fail(err),\n );\n };\n\n const onAppExit = (code: number | null, signal: NodeJS.Signals | null): void => {\n appExited = true;\n appOut.flush();\n appErrOut.flush();\n trace(`app exited: code=${code} signal=${signal}`);\n const alreadyShuttingDown = arbiter.shutdownInitiated;\n arbiter.observeChildExit(code, signal);\n if (!alreadyShuttingDown && !listenExited) {\n listen.kill('SIGTERM');\n startDrain();\n }\n finishIfDone();\n };\n\n const onListenExit = (code: number | null, signal: NodeJS.Signals | null): void => {\n listenExited = true;\n listenOut.flush();\n listenErrOut.flush();\n trace(`listen exited: code=${code} signal=${signal}`);\n const alreadyShuttingDown = arbiter.shutdownInitiated;\n arbiter.observeChildExit(code, signal);\n if (!alreadyShuttingDown && !appExited) {\n app.kill('SIGTERM');\n startDrain();\n }\n finishIfDone();\n };\n\n const onAppError = (err: Error): void => {\n appExited = true;\n if (spawnFailure === undefined) {\n spawnFailure = new OrchestratorSpawnError(\n `Could not start the dev command (${appCommand}): ${err.message}`,\n );\n }\n arbiter.observeChildExit(1, null);\n if (!listenExited) {\n listen.kill('SIGTERM');\n startDrain();\n }\n finishIfDone();\n };\n\n const onListenError = (err: Error): void => {\n listenExited = true;\n if (spawnFailure === undefined) {\n spawnFailure = new OrchestratorSpawnError(\n `Could not start the webhook listener: ${err.message}`,\n 'The bundled webhook listener could not be started; re-install the CLI if this persists.',\n );\n }\n arbiter.observeChildExit(1, null);\n if (!appExited) {\n app.kill('SIGTERM');\n startDrain();\n }\n finishIfDone();\n };\n\n const onSignal = (sig: 'SIGINT' | 'SIGTERM'): void => {\n trace(`signal received: ${sig}`);\n if (arbiter.shutdownInitiated) {\n if (firstSignalAt === 0) {\n // Shutdown was child-initiated; arm the force-exit window so a\n // follow-up interrupt can still cut the drain short.\n firstSignalAt = Date.now();\n return;\n }\n if (Date.now() - firstSignalAt <= SIGNAL_FORCE_EXIT_WINDOW_MS) {\n options.hardExit(130);\n }\n return;\n }\n firstSignalAt = Date.now();\n arbiter.observeSignal(sig);\n io.writeErr('shutting down: stopping the dev command and the webhook listener\\n');\n if (!appExited) {\n app.kill('SIGTERM');\n }\n if (!listenExited) {\n listen.kill('SIGTERM');\n }\n startDrain();\n };\n const onSigint = (): void => onSignal('SIGINT');\n const onSigterm = (): void => onSignal('SIGTERM');\n options.signalSource.on('SIGINT', onSigint);\n options.signalSource.on('SIGTERM', onSigterm);\n\n app.once('exit', onAppExit);\n app.once('error', onAppError);\n listen.once('exit', onListenExit);\n listen.once('error', onListenError);\n\n // Both children are up: notify the preview seam (a no-op in v1).\n trace(`preview: onDevServerReady(${options.devServerPort ?? 0})`);\n void options.preview\n .onDevServerReady(options.devServerPort ?? 0)\n .catch((err: unknown) => fail(err));\n });\n}\n","/**\n * Error hierarchy for the Ratio CLI. Every error thrown from a command's\n * run() is expected to be an instance of RatioCLIError so the global\n * render-error handler can map it to a deterministic stderr line and exit\n * code. Uncaught Error subclasses render as \"INTERNAL: <msg>\" with exit 1.\n *\n * No import from @oclif/core — this file is dependency-free so tests\n * can import it without oclif overhead.\n */\n\nexport class RatioCLIError extends Error {\n readonly code: string;\n readonly exitCode: number;\n readonly hint?: string;\n readonly docsUrl?: string;\n\n constructor(opts: {\n code: string;\n exitCode: number;\n message: string;\n hint?: string;\n docsUrl?: string;\n }) {\n super(opts.message);\n this.name = new.target.name;\n this.code = opts.code;\n this.exitCode = opts.exitCode;\n if (opts.hint !== undefined) this.hint = opts.hint;\n if (opts.docsUrl !== undefined) this.docsUrl = opts.docsUrl;\n }\n}\n\n/** Coming-soon stub UX. Renders on stdout with exit 0. */\nexport class ComingSoonError extends RatioCLIError {\n constructor(message: string) {\n super({ code: 'COMING_SOON', exitCode: 0, message });\n }\n}\n\n/** User has no credentials and command requires auth. */\nexport class NotLoggedInError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to sign in.\",\n ) {\n super({ code: 'NOT_LOGGED_IN', exitCode: 3, message, hint });\n }\n}\n\nexport class TokenExpiredError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to refresh your session.\",\n ) {\n super({ code: 'TOKEN_EXPIRED', exitCode: 3, message, hint });\n }\n}\n\nexport class ScopeDeniedError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'SCOPE_DENIED',\n exitCode: 4,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class NetworkError extends RatioCLIError {\n constructor(message: string, hint: string = 'Check your network and retry.') {\n super({ code: 'NETWORK_ERROR', exitCode: 5, message, hint });\n }\n}\n\nexport class RelayConnectError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Verify the tunnel URL and that the auth-server is reachable.',\n ) {\n super({ code: 'RELAY_CONNECT', exitCode: 6, message, hint });\n }\n}\n\nexport class TemplateExistsError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Choose a different name or remove the existing directory.',\n ) {\n super({ code: 'TEMPLATE_EXISTS', exitCode: 7, message, hint });\n }\n}\n\nexport class ValidationError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'VALIDATION_ERROR',\n exitCode: 8,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class RelayResumeGapError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Reconnect from the current cursor; older events are outside the buffer window.',\n ) {\n super({ code: 'RELAY_RESUME_GAP', exitCode: 9, message, hint });\n }\n}\n\nexport class OrchestratorSpawnError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Could not start your app's dev command — check that the command in ratio.config.jsonc runs from this directory.\",\n ) {\n super({ code: 'ORCHESTRATOR_SPAWN', exitCode: 10, message, hint });\n }\n}\n\nexport class OrchestratorDrainTimeoutError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Your dev command did not exit cleanly within 5 seconds — it was forcibly terminated.',\n ) {\n super({ code: 'ORCHESTRATOR_DRAIN', exitCode: 11, message, hint });\n }\n}\n\nexport function isRatioCLIError(err: unknown): err is RatioCLIError {\n return err instanceof RatioCLIError;\n}\n","/**\n * Read-only view over the app manifest (ratio.config.jsonc).\n *\n * The file is treated as an opaque UTF-8 string: no JSON/JSONC parser is\n * involved, so comments and unknown keys are irrelevant to the read and\n * survive untouched on disk. Each field is extracted with one targeted\n * regex, mirroring the write-side idiom in write-client-id.ts. This module\n * never writes the file.\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { ValidationError } from '../errors.js';\nimport { MANIFEST_FILE_NAME } from './write-client-id.js';\n\n/** Read-only extraction regex for the dev command line. */\nconst DEV_COMMAND_ROW = /^\\s*\"devCommand\"\\s*:\\s*\"([^\"]*)\"/m;\n\n/** Read-only extraction regex for the dev-server port (integer, unquoted). */\nconst DEV_SERVER_PORT_ROW = /^\\s*\"devServerPort\"\\s*:\\s*(\\d+)/m;\n\n/** Key-presence probe: distinguishes \"absent\" from \"present but malformed\". */\nconst DEV_SERVER_PORT_KEY = /^\\s*\"devServerPort\"\\s*:/m;\n\n/** Read-only extraction regex for the appName echo. Never used to write. */\nconst APP_NAME_ROW = /^\\s*\"appName\"\\s*:\\s*\"([^\"]*)\"/m;\n\n/** Read-only extraction regex for the clientId echo. Never used to write. */\nconst CLIENT_ID_READ_ROW = /^\\s*\"clientId\"\\s*:\\s*\"([^\"]*)\"/m;\n\n/** Hint shown whenever the dev command entry is missing or unusable. */\nexport const DEV_COMMAND_HINT =\n 'Add a \"devCommand\" entry to ratio.config.jsonc (e.g. \"npm run dev\") and re-run.';\n\nexport interface ManifestView {\n /** Absolute path to the manifest file that was read. */\n path: string;\n /** The command used to start the app's own dev server. Always present. */\n devCommand: string;\n /** Optional local dev-server port (1-65535); undefined when absent. */\n devServerPort: number | undefined;\n /** Parsed from the manifest's appName row, when present. */\n appName: string | undefined;\n /** Parsed from the manifest's clientId row, when present. */\n clientId: string | undefined;\n}\n\n/**\n * Locate `ratio.config.jsonc` under `cwd` and extract the fields the dev\n * orchestrator needs. Fails fast with a typed error when the file is\n * missing, when `devCommand` is absent/empty, or when `devServerPort` is\n * present but not an integer in 1-65535.\n */\nexport function readManifest(cwd: string): ManifestView {\n const filePath = path.join(cwd, MANIFEST_FILE_NAME);\n\n let content: string;\n try {\n content = fs.readFileSync(filePath, 'utf8');\n } catch {\n throw new ValidationError(\n `${MANIFEST_FILE_NAME} not found in ${cwd}.`,\n `Run this command from the app's repo root; ${MANIFEST_FILE_NAME} not found in ${cwd}.`,\n );\n }\n\n const devCommand = DEV_COMMAND_ROW.exec(content)?.[1];\n if (devCommand === undefined || devCommand.trim() === '') {\n throw new ValidationError(\n `${MANIFEST_FILE_NAME} has no usable \"devCommand\" entry.`,\n DEV_COMMAND_HINT,\n );\n }\n\n let devServerPort: number | undefined;\n if (DEV_SERVER_PORT_KEY.test(content)) {\n const rawPort = DEV_SERVER_PORT_ROW.exec(content)?.[1];\n const port = rawPort === undefined ? Number.NaN : Number.parseInt(rawPort, 10);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new ValidationError(\n `${MANIFEST_FILE_NAME} has an invalid \"devServerPort\" value.`,\n '\"devServerPort\" must be an unquoted integer between 1 and 65535 (e.g. \"devServerPort\": 3001).',\n );\n }\n devServerPort = port;\n }\n\n return {\n path: filePath,\n devCommand,\n devServerPort,\n appName: APP_NAME_ROW.exec(content)?.[1],\n clientId: CLIENT_ID_READ_ROW.exec(content)?.[1],\n };\n}\n","/**\n * Atomically rewrite the top-level \"clientId\" value in a ratio.config.jsonc\n * file. Preserves every comment, whitespace token, and unknown key.\n *\n * The file is treated as an opaque UTF-8 string: no JSON/JSONC parser is\n * involved, so comments survive verbatim. Exactly one targeted regex is\n * applied to the `\"clientId\": \"<value>\"` line; everything else is copied\n * byte-for-byte.\n *\n * Throws ValidationError if the file is not shaped like a canonical\n * ratio.config.jsonc (no top-level \"clientId\" key on its own line).\n */\n\nimport crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { ValidationError } from '../errors.js';\n\n/** Manifest file name expected in the app repo root. */\nexport const MANIFEST_FILE_NAME = 'ratio.config.jsonc';\n\n/** Targeted single-key rewrite regex for the clientId value. */\nconst CLIENT_ID_ROW = /^(\\s*\"clientId\"\\s*:\\s*\")([^\"]*)(\"\\s*,?)/m;\n\n/** Read-only extraction regex for the appName echo. Never used to write. */\nconst APP_NAME_ROW = /^\\s*\"appName\"\\s*:\\s*\"([^\"]*)\"/m;\n\nexport interface WriteClientIdResult {\n /** false when the file already had the same clientId. */\n written: boolean;\n /** Absolute path to the manifest file. */\n path: string;\n /** Parsed from the manifest's appName row, when present. */\n appName?: string;\n /** The value that was replaced, when it was non-empty and different. */\n previousClientId?: string;\n}\n\n/**\n * Locate `ratio.config.jsonc` under `cwd`, rewrite ONLY the top-level\n * clientId value, and persist via atomic tmp+rename with 0600 discipline\n * (mirrors the credential store's write contract). Same-value re-runs are\n * a no-op (`written: false`, file untouched).\n */\nexport async function writeClientId(\n cwd: string,\n clientId: string,\n): Promise<WriteClientIdResult> {\n const filePath = path.join(cwd, MANIFEST_FILE_NAME);\n\n let content: string;\n try {\n content = fs.readFileSync(filePath, 'utf8');\n } catch {\n throw new ValidationError(\n `${MANIFEST_FILE_NAME} not found in ${cwd}.`,\n `Run this command from the app's repo root; ${MANIFEST_FILE_NAME} not found in ${cwd}.`,\n );\n }\n\n const appNameMatch = APP_NAME_ROW.exec(content);\n const appName = appNameMatch?.[1];\n\n const rowMatch = CLIENT_ID_ROW.exec(content);\n if (rowMatch === null) {\n throw new ValidationError(\n `${MANIFEST_FILE_NAME} has no top-level \"clientId\" entry.`,\n 'Restore the \"clientId\" line from the app template, then re-run.',\n );\n }\n\n const existing = rowMatch[2] ?? '';\n\n const base: WriteClientIdResult = {\n written: false,\n path: filePath,\n ...(appName !== undefined ? { appName } : {}),\n };\n\n if (existing === clientId) {\n return base;\n }\n\n // Function replacer: the new value is inserted verbatim, immune to `$`\n // pattern expansion in String.replace replacement strings.\n const updated = content.replace(\n CLIENT_ID_ROW,\n (_row, open: string, _old: string, close: string) =>\n `${open}${clientId}${close}`,\n );\n\n const tmpPath = `${filePath}.${process.pid}.${crypto\n .randomBytes(8)\n .toString('hex')}.tmp`;\n try {\n // mode at open time closes the create-then-chmod race window.\n fs.writeFileSync(tmpPath, updated, { mode: 0o600 });\n fs.renameSync(tmpPath, filePath);\n } catch (err) {\n try {\n fs.unlinkSync(tmpPath);\n } catch {\n // Best-effort cleanup: the tmp file may never have been created.\n }\n throw err;\n }\n if (process.platform !== 'win32') {\n // Belt-and-braces: rename may preserve a prior destination mode.\n fs.chmodSync(filePath, 0o600);\n }\n\n return {\n ...base,\n written: true,\n ...(existing !== '' ? { previousClientId: existing } : {}),\n };\n}\n"],"mappings":"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","names":["fs","path"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PreviewController } from './types.js';
|
|
2
|
+
export { NullPreviewController } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* v1: always returns NullPreviewController. The active-preview binding
|
|
6
|
+
* is introduced by a future change; the orchestrator does not need to
|
|
7
|
+
* change when that happens.
|
|
8
|
+
*/
|
|
9
|
+
declare function createPreviewController(): PreviewController;
|
|
10
|
+
|
|
11
|
+
export { PreviewController, createPreviewController };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/lib/preview/types.ts
|
|
2
|
+
var NullPreviewController = class {
|
|
3
|
+
async onDevServerReady(_port) {
|
|
4
|
+
}
|
|
5
|
+
async onShutdown() {
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// src/lib/preview/index.ts
|
|
10
|
+
function createPreviewController() {
|
|
11
|
+
return new NullPreviewController();
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
NullPreviewController,
|
|
15
|
+
createPreviewController
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/preview/types.ts","../../../src/lib/preview/index.ts"],"sourcesContent":["/**\n * Reserved seam for a future active-preview implementation. In v1 the\n * only implementation is a no-op; the interface exists so a future\n * change can bind an active implementation without editing the\n * orchestrator.\n */\nexport interface PreviewController {\n onDevServerReady(port: number): Promise<void>;\n onShutdown(): Promise<void>;\n}\n\nexport class NullPreviewController implements PreviewController {\n async onDevServerReady(_port: number): Promise<void> {}\n async onShutdown(): Promise<void> {}\n}\n","export { NullPreviewController } from './types.js';\nexport type { PreviewController } from './types.js';\n\n/**\n * v1: always returns NullPreviewController. The active-preview binding\n * is introduced by a future change; the orchestrator does not need to\n * change when that happens.\n */\nexport function createPreviewController(): PreviewController {\n return new NullPreviewController();\n}\n\nimport { NullPreviewController } from './types.js';\nimport type { PreviewController } from './types.js';\n"],"mappings":";AAWO,IAAM,wBAAN,MAAyD;AAAA,EAC9D,MAAM,iBAAiB,OAA8B;AAAA,EAAC;AAAA,EACtD,MAAM,aAA4B;AAAA,EAAC;AACrC;;;ACNO,SAAS,0BAA6C;AAC3D,SAAO,IAAI,sBAAsB;AACnC;","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserved seam for a future active-preview implementation. In v1 the
|
|
3
|
+
* only implementation is a no-op; the interface exists so a future
|
|
4
|
+
* change can bind an active implementation without editing the
|
|
5
|
+
* orchestrator.
|
|
6
|
+
*/
|
|
7
|
+
interface PreviewController {
|
|
8
|
+
onDevServerReady(port: number): Promise<void>;
|
|
9
|
+
onShutdown(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
declare class NullPreviewController implements PreviewController {
|
|
12
|
+
onDevServerReady(_port: number): Promise<void>;
|
|
13
|
+
onShutdown(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { NullPreviewController, type PreviewController };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/preview/types.ts"],"sourcesContent":["/**\n * Reserved seam for a future active-preview implementation. In v1 the\n * only implementation is a no-op; the interface exists so a future\n * change can bind an active implementation without editing the\n * orchestrator.\n */\nexport interface PreviewController {\n onDevServerReady(port: number): Promise<void>;\n onShutdown(): Promise<void>;\n}\n\nexport class NullPreviewController implements PreviewController {\n async onDevServerReady(_port: number): Promise<void> {}\n async onShutdown(): Promise<void> {}\n}\n"],"mappings":";AAWO,IAAM,wBAAN,MAAyD;AAAA,EAC9D,MAAM,iBAAiB,OAA8B;AAAA,EAAC;AAAA,EACtD,MAAM,aAA4B;AAAA,EAAC;AACrC;","names":[]}
|