@codeagentswarm/cas-cloud 0.0.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/LICENSE +131 -0
- package/NOTICE +2 -0
- package/README.md +200 -0
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/caddy/Caddyfile.preview.example +12 -0
- package/dist/antigravity-mcp-launcher.js +5759 -0
- package/dist/cas-preview.js +761 -0
- package/dist/cas.js +48859 -0
- package/dist/mcp-stdio-server.js +5554 -0
- package/dist/templates/antigravity-md-titles-section.md +53 -0
- package/dist/templates/codex-md-titles-section.md +53 -0
- package/dist/templates/grok-md-titles-section.md +125 -0
- package/dist/templates/kimi-md-titles-section.md +89 -0
- package/dist/templates/opencode-md-titles-section.md +53 -0
- package/package.json +62 -0
- package/scripts/cas-cli-smoke.sh +30 -0
- package/systemd/cas-cli-update.service.example +13 -0
- package/systemd/cas-cli-update.timer.example +12 -0
- package/systemd/cas-cli.service.example +22 -0
- package/systemd/cas-preview.service.example +17 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
4
|
+
try {
|
|
5
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
6
|
+
} catch (e) {
|
|
7
|
+
throw mod = 0, e;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/infrastructure/headless/ephemeral-preview-service.js
|
|
12
|
+
var require_ephemeral_preview_service = __commonJS({
|
|
13
|
+
"src/infrastructure/headless/ephemeral-preview-service.js"(exports2, module2) {
|
|
14
|
+
var crypto = require("crypto");
|
|
15
|
+
var fs = require("fs");
|
|
16
|
+
var http2 = require("http");
|
|
17
|
+
var net = require("net");
|
|
18
|
+
var path2 = require("path");
|
|
19
|
+
var { spawn } = require("child_process");
|
|
20
|
+
var DEFAULT_TTL_MS2 = 2 * 60 * 60 * 1e3;
|
|
21
|
+
var DEFAULT_READY_TIMEOUT_MS = 3e4;
|
|
22
|
+
var MAX_CONTROL_BODY_BYTES = 64 * 1024;
|
|
23
|
+
var MAX_REWRITE_BYTES = 10 * 1024 * 1024;
|
|
24
|
+
var TOMBSTONE_TTL_MS = 10 * 60 * 1e3;
|
|
25
|
+
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
26
|
+
"connection",
|
|
27
|
+
"keep-alive",
|
|
28
|
+
"proxy-authenticate",
|
|
29
|
+
"proxy-authorization",
|
|
30
|
+
"te",
|
|
31
|
+
"trailer",
|
|
32
|
+
"transfer-encoding",
|
|
33
|
+
"upgrade"
|
|
34
|
+
]);
|
|
35
|
+
function isInside(parent, candidate) {
|
|
36
|
+
const relative = path2.relative(parent, candidate);
|
|
37
|
+
return relative === "" || !relative.startsWith(`..${path2.sep}`) && relative !== ".." && !path2.isAbsolute(relative);
|
|
38
|
+
}
|
|
39
|
+
function projectSlug(projectPath) {
|
|
40
|
+
const name = path2.basename(projectPath).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "project";
|
|
41
|
+
const digest = crypto.createHash("sha256").update(projectPath).digest("hex").slice(0, 8);
|
|
42
|
+
return `${name}-${digest}`;
|
|
43
|
+
}
|
|
44
|
+
function previewPrefix(lease) {
|
|
45
|
+
return `/${lease.projectSlug}/${lease.id}`;
|
|
46
|
+
}
|
|
47
|
+
function normalizePublicOrigin(value) {
|
|
48
|
+
let origin;
|
|
49
|
+
try {
|
|
50
|
+
origin = new URL(value);
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error("Preview public origin must be a valid HTTP or HTTPS origin");
|
|
53
|
+
}
|
|
54
|
+
if (!["http:", "https:"].includes(origin.protocol) || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash) {
|
|
55
|
+
throw new Error("Preview public origin must contain only an HTTP or HTTPS origin");
|
|
56
|
+
}
|
|
57
|
+
return origin.origin;
|
|
58
|
+
}
|
|
59
|
+
function rewritePreviewText(value, origin, proxyBase) {
|
|
60
|
+
const escapedOrigin = origin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
61
|
+
return value.replace(new RegExp(escapedOrigin, "g"), proxyBase).replace(/\b(href|src|action)=(['"])\/(?!\/)/gi, `$1=$2${proxyBase}/`).replace(/\b(url\(\s*['"]?)\/(?!\/)/gi, `$1${proxyBase}/`).replace(/\b((?:from|import)\s*\(?\s*['"])\/(?!\/)/g, `$1${proxyBase}/`).replace(/\b((?:fetch|new URL)\s*\(\s*['"])\/(?!\/)/g, `$1${proxyBase}/`).replace(/(sourceMappingURL=)\/(?!\/)/g, `$1${proxyBase}/`);
|
|
62
|
+
}
|
|
63
|
+
function canConnect(port, host = "127.0.0.1", timeoutMs = 300) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
const socket = net.createConnection({ host, port });
|
|
66
|
+
const finish = (value) => {
|
|
67
|
+
socket.destroy();
|
|
68
|
+
resolve(value);
|
|
69
|
+
};
|
|
70
|
+
socket.setTimeout(timeoutMs);
|
|
71
|
+
socket.once("connect", () => finish(true));
|
|
72
|
+
socket.once("timeout", () => finish(false));
|
|
73
|
+
socket.once("error", () => finish(false));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function delay(milliseconds) {
|
|
77
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
78
|
+
}
|
|
79
|
+
function canConnectSocket(socketPath, timeoutMs = 300) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const socket = net.createConnection({ path: socketPath });
|
|
82
|
+
const finish = (value) => {
|
|
83
|
+
socket.destroy();
|
|
84
|
+
resolve(value);
|
|
85
|
+
};
|
|
86
|
+
socket.setTimeout(timeoutMs);
|
|
87
|
+
socket.once("connect", () => finish(true));
|
|
88
|
+
socket.once("timeout", () => finish(false));
|
|
89
|
+
socket.once("error", () => finish(false));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function hasWildcardListener(port) {
|
|
93
|
+
if (process.platform !== "linux") return false;
|
|
94
|
+
const expectedPort = Number(port).toString(16).toUpperCase().padStart(4, "0");
|
|
95
|
+
for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
96
|
+
let rows;
|
|
97
|
+
try {
|
|
98
|
+
rows = fs.readFileSync(file, "utf8").trim().split("\n").slice(1);
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
const fields = row.trim().split(/\s+/);
|
|
104
|
+
if (fields[3] !== "0A") continue;
|
|
105
|
+
const [address, portHex] = fields[1].split(":");
|
|
106
|
+
if (portHex === expectedPort && /^0+$/.test(address)) return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
function signalProcessGroup(child, signal) {
|
|
112
|
+
if (!(child == null ? void 0 : child.pid)) return;
|
|
113
|
+
try {
|
|
114
|
+
if (process.platform !== "win32") process.kill(-child.pid, signal);
|
|
115
|
+
else child.kill(signal);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error.code !== "ESRCH") throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function childEnvironment(lease) {
|
|
121
|
+
const allowed = [
|
|
122
|
+
"COLORTERM",
|
|
123
|
+
"FORCE_COLOR",
|
|
124
|
+
"HOME",
|
|
125
|
+
"LANG",
|
|
126
|
+
"LC_ALL",
|
|
127
|
+
"LOGNAME",
|
|
128
|
+
"NO_COLOR",
|
|
129
|
+
"PATH",
|
|
130
|
+
"SHELL",
|
|
131
|
+
"TEMP",
|
|
132
|
+
"TERM",
|
|
133
|
+
"TMP",
|
|
134
|
+
"TMPDIR",
|
|
135
|
+
"USER",
|
|
136
|
+
"COREPACK_HOME",
|
|
137
|
+
"NVM_BIN",
|
|
138
|
+
"NVM_DIR",
|
|
139
|
+
"PNPM_HOME",
|
|
140
|
+
"XDG_CACHE_HOME",
|
|
141
|
+
"XDG_CONFIG_HOME",
|
|
142
|
+
"XDG_DATA_HOME"
|
|
143
|
+
];
|
|
144
|
+
const environment = {};
|
|
145
|
+
for (const name of allowed) {
|
|
146
|
+
if (process.env[name] !== void 0) environment[name] = process.env[name];
|
|
147
|
+
}
|
|
148
|
+
environment.PATH ||= "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
149
|
+
return {
|
|
150
|
+
...environment,
|
|
151
|
+
BROWSER: "none",
|
|
152
|
+
HOST: "127.0.0.1",
|
|
153
|
+
PORT: String(lease.port),
|
|
154
|
+
CAS_PREVIEW_BASE_PATH: `${previewPrefix(lease)}/`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function stripRoutingCookie(value) {
|
|
158
|
+
if (!value) return value;
|
|
159
|
+
return value.split(";").map((part) => part.trim()).filter((part) => !part.startsWith("cas_preview=")).join("; ");
|
|
160
|
+
}
|
|
161
|
+
function proxyHeaders(request, lease, targetPath) {
|
|
162
|
+
const headers = {};
|
|
163
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
164
|
+
if (!HOP_BY_HOP_HEADERS.has(name) && name !== "host" && name !== "content-length") headers[name] = value;
|
|
165
|
+
}
|
|
166
|
+
const prefix = previewPrefix(lease);
|
|
167
|
+
const upstreamOrigin = `http://127.0.0.1:${lease.port}`;
|
|
168
|
+
headers.host = `127.0.0.1:${lease.port}`;
|
|
169
|
+
headers["x-forwarded-host"] = request.headers.host || "";
|
|
170
|
+
headers["x-forwarded-proto"] = new URL(lease.publicOrigin).protocol.slice(0, -1);
|
|
171
|
+
headers["x-forwarded-prefix"] = prefix;
|
|
172
|
+
headers["accept-encoding"] = "identity";
|
|
173
|
+
if (request.headers.origin) headers.origin = upstreamOrigin;
|
|
174
|
+
if (request.headers.referer) headers.referer = `${upstreamOrigin}${targetPath}`;
|
|
175
|
+
const cookie = stripRoutingCookie(request.headers.cookie);
|
|
176
|
+
if (cookie) headers.cookie = cookie;
|
|
177
|
+
else delete headers.cookie;
|
|
178
|
+
return headers;
|
|
179
|
+
}
|
|
180
|
+
function parseCookies(value) {
|
|
181
|
+
return String(value || "").split(";").reduce((cookies, entry) => {
|
|
182
|
+
const separator = entry.indexOf("=");
|
|
183
|
+
if (separator <= 0) return cookies;
|
|
184
|
+
cookies[entry.slice(0, separator).trim()] = entry.slice(separator + 1).trim();
|
|
185
|
+
return cookies;
|
|
186
|
+
}, {});
|
|
187
|
+
}
|
|
188
|
+
var EphemeralPreviewService2 = class {
|
|
189
|
+
constructor({
|
|
190
|
+
root,
|
|
191
|
+
publicOrigin,
|
|
192
|
+
ttlMs = DEFAULT_TTL_MS2,
|
|
193
|
+
readyTimeoutMs = DEFAULT_READY_TIMEOUT_MS,
|
|
194
|
+
now = Date.now,
|
|
195
|
+
spawnImpl = spawn,
|
|
196
|
+
logger = console
|
|
197
|
+
} = {}) {
|
|
198
|
+
if (!root) throw new Error("EphemeralPreviewService requires a project root");
|
|
199
|
+
if (!publicOrigin) throw new Error("EphemeralPreviewService requires a public origin");
|
|
200
|
+
this.root = fs.realpathSync(root);
|
|
201
|
+
this.publicOrigin = normalizePublicOrigin(publicOrigin);
|
|
202
|
+
this.ttlMs = ttlMs;
|
|
203
|
+
this.readyTimeoutMs = readyTimeoutMs;
|
|
204
|
+
this.now = now;
|
|
205
|
+
this.spawn = spawnImpl;
|
|
206
|
+
this.logger = logger;
|
|
207
|
+
this.leases = /* @__PURE__ */ new Map();
|
|
208
|
+
this.tombstones = /* @__PURE__ */ new Map();
|
|
209
|
+
}
|
|
210
|
+
prune() {
|
|
211
|
+
const current = this.now();
|
|
212
|
+
for (const [id, stoppedAt] of this.tombstones) {
|
|
213
|
+
if (stoppedAt + TOMBSTONE_TTL_MS <= current) this.tombstones.delete(id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
resolveLease(id) {
|
|
217
|
+
const lease = this.leases.get(id);
|
|
218
|
+
if (lease && lease.expiresAt <= this.now()) {
|
|
219
|
+
this.stopLease(id, "expired").catch((error) => {
|
|
220
|
+
var _a, _b;
|
|
221
|
+
return (_b = (_a = this.logger).error) == null ? void 0 : _b.call(_a, error);
|
|
222
|
+
});
|
|
223
|
+
return { expired: true };
|
|
224
|
+
}
|
|
225
|
+
if (lease) return { lease };
|
|
226
|
+
if (this.tombstones.has(id)) return { expired: true };
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
publicLease(lease) {
|
|
230
|
+
return {
|
|
231
|
+
id: lease.id,
|
|
232
|
+
project: lease.projectSlug,
|
|
233
|
+
cwd: lease.cwd,
|
|
234
|
+
port: lease.port,
|
|
235
|
+
command: lease.command,
|
|
236
|
+
createdAt: lease.createdAt,
|
|
237
|
+
expiresAt: lease.expiresAt,
|
|
238
|
+
url: `${this.publicOrigin}${previewPrefix(lease)}/`
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
async startLease({ cwd, port, command }) {
|
|
242
|
+
var _a, _b, _c, _d;
|
|
243
|
+
this.prune();
|
|
244
|
+
const resolvedCwd = fs.realpathSync(path2.resolve(cwd || ""));
|
|
245
|
+
if (!isInside(this.root, resolvedCwd)) {
|
|
246
|
+
throw new Error(`Preview projects must be inside ${this.root}`);
|
|
247
|
+
}
|
|
248
|
+
const numericPort = Number(port);
|
|
249
|
+
if (!Number.isInteger(numericPort) || numericPort < 1024 || numericPort > 65535) {
|
|
250
|
+
throw new Error("Preview port must be an integer from 1024 to 65535");
|
|
251
|
+
}
|
|
252
|
+
if (!Array.isArray(command) || !command.length || !command[0] || command.some((value) => typeof value !== "string")) {
|
|
253
|
+
throw new Error("Preview command is required after --");
|
|
254
|
+
}
|
|
255
|
+
if ([...this.leases.values()].some((lease2) => lease2.port === numericPort)) {
|
|
256
|
+
throw new Error(`Port ${numericPort} already belongs to an active preview`);
|
|
257
|
+
}
|
|
258
|
+
if (await canConnect(numericPort)) {
|
|
259
|
+
throw new Error(`Port ${numericPort} is already in use; refusing to adopt an unmanaged process`);
|
|
260
|
+
}
|
|
261
|
+
const createdAt = this.now();
|
|
262
|
+
const lease = {
|
|
263
|
+
id: crypto.randomBytes(18).toString("base64url"),
|
|
264
|
+
projectSlug: projectSlug(resolvedCwd),
|
|
265
|
+
cwd: resolvedCwd,
|
|
266
|
+
port: numericPort,
|
|
267
|
+
command: [...command],
|
|
268
|
+
createdAt,
|
|
269
|
+
expiresAt: createdAt + this.ttlMs,
|
|
270
|
+
publicOrigin: this.publicOrigin,
|
|
271
|
+
child: null,
|
|
272
|
+
timer: null,
|
|
273
|
+
stopping: false,
|
|
274
|
+
spawnError: null
|
|
275
|
+
};
|
|
276
|
+
const child = this.spawn(command[0], command.slice(1), {
|
|
277
|
+
cwd: resolvedCwd,
|
|
278
|
+
detached: process.platform !== "win32",
|
|
279
|
+
env: childEnvironment(lease),
|
|
280
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
281
|
+
});
|
|
282
|
+
lease.child = child;
|
|
283
|
+
this.leases.set(lease.id, lease);
|
|
284
|
+
const log = (stream, chunk) => {
|
|
285
|
+
var _a2, _b2;
|
|
286
|
+
const output = String(chunk).trimEnd();
|
|
287
|
+
if (output) (_b2 = (_a2 = this.logger)[stream]) == null ? void 0 : _b2.call(_a2, `[preview ${lease.projectSlug}/${lease.id.slice(0, 8)}] ${output}`);
|
|
288
|
+
};
|
|
289
|
+
(_a = child.stdout) == null ? void 0 : _a.on("data", (chunk) => log("log", chunk));
|
|
290
|
+
(_b = child.stderr) == null ? void 0 : _b.on("data", (chunk) => log("error", chunk));
|
|
291
|
+
child.once("error", (error) => {
|
|
292
|
+
var _a2, _b2;
|
|
293
|
+
lease.spawnError = error;
|
|
294
|
+
(_b2 = (_a2 = this.logger).error) == null ? void 0 : _b2.call(_a2, `[preview ${lease.id.slice(0, 8)}] ${error.message}`);
|
|
295
|
+
});
|
|
296
|
+
child.once("close", () => {
|
|
297
|
+
if (lease.timer) clearTimeout(lease.timer);
|
|
298
|
+
if (this.leases.get(lease.id) === lease) this.leases.delete(lease.id);
|
|
299
|
+
this.tombstones.set(lease.id, this.now());
|
|
300
|
+
});
|
|
301
|
+
const deadline = this.now() + this.readyTimeoutMs;
|
|
302
|
+
while (this.now() < deadline) {
|
|
303
|
+
if (lease.spawnError || child.exitCode !== null || child.signalCode !== null || !this.leases.has(lease.id)) break;
|
|
304
|
+
if (await canConnect(numericPort)) {
|
|
305
|
+
if (hasWildcardListener(numericPort)) {
|
|
306
|
+
await this.stopLease(lease.id, "unsafe wildcard listener");
|
|
307
|
+
throw new Error(`Preview command must bind 127.0.0.1, not a wildcard address, on port ${numericPort}`);
|
|
308
|
+
}
|
|
309
|
+
lease.timer = setTimeout(() => {
|
|
310
|
+
this.stopLease(lease.id, "expired").catch((error) => {
|
|
311
|
+
var _a2, _b2;
|
|
312
|
+
return (_b2 = (_a2 = this.logger).error) == null ? void 0 : _b2.call(_a2, error);
|
|
313
|
+
});
|
|
314
|
+
}, Math.max(0, lease.expiresAt - this.now()));
|
|
315
|
+
(_d = (_c = lease.timer).unref) == null ? void 0 : _d.call(_c);
|
|
316
|
+
return this.publicLease(lease);
|
|
317
|
+
}
|
|
318
|
+
await delay(150);
|
|
319
|
+
}
|
|
320
|
+
await this.stopLease(lease.id, "startup failed");
|
|
321
|
+
if (lease.spawnError) throw new Error(`Could not start preview command: ${lease.spawnError.message}`);
|
|
322
|
+
throw new Error(`Preview command did not listen on 127.0.0.1:${numericPort} within ${this.readyTimeoutMs} ms`);
|
|
323
|
+
}
|
|
324
|
+
async stopLease(id, reason = "stopped") {
|
|
325
|
+
var _a, _b;
|
|
326
|
+
const lease = this.leases.get(id);
|
|
327
|
+
if (!lease) return false;
|
|
328
|
+
if (lease.stopping) return true;
|
|
329
|
+
lease.stopping = true;
|
|
330
|
+
if (lease.timer) clearTimeout(lease.timer);
|
|
331
|
+
(_b = (_a = this.logger).log) == null ? void 0 : _b.call(_a, `[preview ${lease.projectSlug}/${lease.id.slice(0, 8)}] ${reason}; terminating process group`);
|
|
332
|
+
signalProcessGroup(lease.child, "SIGTERM");
|
|
333
|
+
let forceTimer;
|
|
334
|
+
const exited = await Promise.race([
|
|
335
|
+
new Promise((resolve) => {
|
|
336
|
+
if (lease.child.exitCode !== null || lease.child.signalCode !== null) resolve(true);
|
|
337
|
+
else lease.child.once("close", () => resolve(true));
|
|
338
|
+
}),
|
|
339
|
+
new Promise((resolve) => {
|
|
340
|
+
forceTimer = setTimeout(() => resolve(false), 5e3);
|
|
341
|
+
})
|
|
342
|
+
]);
|
|
343
|
+
clearTimeout(forceTimer);
|
|
344
|
+
if (!exited) signalProcessGroup(lease.child, "SIGKILL");
|
|
345
|
+
this.leases.delete(id);
|
|
346
|
+
this.tombstones.set(id, this.now());
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
async stopAll(reason = "service stopping") {
|
|
350
|
+
await Promise.all([...this.leases.keys()].map((id) => this.stopLease(id, reason)));
|
|
351
|
+
}
|
|
352
|
+
list() {
|
|
353
|
+
this.prune();
|
|
354
|
+
return [...this.leases.values()].flatMap((lease) => {
|
|
355
|
+
const resolved = this.resolveLease(lease.id);
|
|
356
|
+
return (resolved == null ? void 0 : resolved.lease) ? [this.publicLease(resolved.lease)] : [];
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
resolveRequest(request) {
|
|
360
|
+
this.prune();
|
|
361
|
+
const requestUrl = new URL(request.url, "http://preview.internal");
|
|
362
|
+
const segments = requestUrl.pathname.split("/").filter(Boolean);
|
|
363
|
+
if (segments.length >= 2) {
|
|
364
|
+
const resolved2 = this.resolveLease(segments[1]);
|
|
365
|
+
if ((resolved2 == null ? void 0 : resolved2.lease) && resolved2.lease.projectSlug === segments[0]) {
|
|
366
|
+
const rest = `/${segments.slice(2).join("/")}`;
|
|
367
|
+
return { lease: resolved2.lease, targetPath: `${rest}${requestUrl.search}` };
|
|
368
|
+
}
|
|
369
|
+
if (resolved2 == null ? void 0 : resolved2.expired) return resolved2;
|
|
370
|
+
}
|
|
371
|
+
const referer = request.headers.referer;
|
|
372
|
+
if (referer) {
|
|
373
|
+
try {
|
|
374
|
+
const refererSegments = new URL(referer).pathname.split("/").filter(Boolean);
|
|
375
|
+
const resolved2 = this.resolveLease(refererSegments[1]);
|
|
376
|
+
if ((resolved2 == null ? void 0 : resolved2.lease) && resolved2.lease.projectSlug === refererSegments[0]) {
|
|
377
|
+
return { lease: resolved2.lease, targetPath: `${requestUrl.pathname}${requestUrl.search}` };
|
|
378
|
+
}
|
|
379
|
+
if (resolved2 == null ? void 0 : resolved2.expired) return resolved2;
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const cookieId = parseCookies(request.headers.cookie).cas_preview;
|
|
384
|
+
const resolved = this.resolveLease(cookieId);
|
|
385
|
+
if (resolved == null ? void 0 : resolved.lease) return { lease: resolved.lease, targetPath: `${requestUrl.pathname}${requestUrl.search}` };
|
|
386
|
+
if (resolved == null ? void 0 : resolved.expired) return resolved;
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
function copyResponseHeaders(upstream, response, lease, rewritten) {
|
|
391
|
+
const prefix = previewPrefix(lease);
|
|
392
|
+
const upstreamOrigin = `http://127.0.0.1:${lease.port}`;
|
|
393
|
+
for (const [name, value] of Object.entries(upstream.headers)) {
|
|
394
|
+
if (HOP_BY_HOP_HEADERS.has(name) || rewritten && name === "content-length") continue;
|
|
395
|
+
if (name === "location" && typeof value === "string") {
|
|
396
|
+
response.setHeader(name, value.startsWith("/") ? `${prefix}${value}` : value.replace(upstreamOrigin, prefix));
|
|
397
|
+
} else if (name === "set-cookie") {
|
|
398
|
+
const cookies = (Array.isArray(value) ? value : [value]).filter((cookie) => !/^cas_preview=/i.test(String(cookie))).map((cookie) => String(cookie).replace(/Path=\//i, `Path=${prefix}/`));
|
|
399
|
+
if (cookies.length) response.setHeader(name, cookies);
|
|
400
|
+
} else {
|
|
401
|
+
response.setHeader(name, value);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const remainingSeconds = Math.max(0, Math.ceil((lease.expiresAt - Date.now()) / 1e3));
|
|
405
|
+
const secure = new URL(lease.publicOrigin).protocol === "https:" ? "; Secure" : "";
|
|
406
|
+
response.appendHeader("set-cookie", `cas_preview=${lease.id}; Path=/; HttpOnly${secure}; SameSite=Lax; Max-Age=${remainingSeconds}`);
|
|
407
|
+
response.setHeader("cache-control", "no-store");
|
|
408
|
+
response.setHeader("x-content-type-options", "nosniff");
|
|
409
|
+
response.setHeader("x-robots-tag", "noindex, nofollow, noarchive");
|
|
410
|
+
}
|
|
411
|
+
function proxyHttp(request, response, resolved) {
|
|
412
|
+
const { lease, targetPath } = resolved;
|
|
413
|
+
const prefix = previewPrefix(lease);
|
|
414
|
+
const upstreamOrigin = `http://127.0.0.1:${lease.port}`;
|
|
415
|
+
const upstreamRequest = http2.request({
|
|
416
|
+
hostname: "127.0.0.1",
|
|
417
|
+
port: lease.port,
|
|
418
|
+
method: request.method,
|
|
419
|
+
path: targetPath,
|
|
420
|
+
headers: proxyHeaders(request, lease, targetPath)
|
|
421
|
+
}, (upstream) => {
|
|
422
|
+
const contentType = String(upstream.headers["content-type"] || "");
|
|
423
|
+
const rewritten = /(?:text\/|javascript|json|xml)/i.test(contentType) && !upstream.headers["content-encoding"];
|
|
424
|
+
response.statusCode = upstream.statusCode || 502;
|
|
425
|
+
copyResponseHeaders(upstream, response, lease, rewritten);
|
|
426
|
+
if (!rewritten || request.method === "HEAD" || [204, 304].includes(upstream.statusCode)) {
|
|
427
|
+
upstream.pipe(response);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const chunks = [];
|
|
431
|
+
let total = 0;
|
|
432
|
+
let tooLarge = false;
|
|
433
|
+
upstream.on("data", (chunk) => {
|
|
434
|
+
total += chunk.length;
|
|
435
|
+
if (total <= MAX_REWRITE_BYTES) chunks.push(chunk);
|
|
436
|
+
else tooLarge = true;
|
|
437
|
+
});
|
|
438
|
+
upstream.on("end", () => {
|
|
439
|
+
if (response.destroyed) return;
|
|
440
|
+
if (tooLarge) response.destroy(new Error("Preview response exceeded rewrite limit"));
|
|
441
|
+
else response.end(rewritePreviewText(Buffer.concat(chunks).toString(), upstreamOrigin, prefix));
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
upstreamRequest.setTimeout(3e4, () => upstreamRequest.destroy(new Error("Preview upstream timed out")));
|
|
445
|
+
upstreamRequest.on("error", (error) => {
|
|
446
|
+
if (response.headersSent) response.destroy(error);
|
|
447
|
+
else {
|
|
448
|
+
response.statusCode = 502;
|
|
449
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
450
|
+
response.end(`Could not reach the preview process: ${error.message}`);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
request.pipe(upstreamRequest);
|
|
454
|
+
}
|
|
455
|
+
function proxyUpgrade(request, socket, head, resolved) {
|
|
456
|
+
const { lease, targetPath } = resolved;
|
|
457
|
+
const upstreamRequest = http2.request({
|
|
458
|
+
hostname: "127.0.0.1",
|
|
459
|
+
port: lease.port,
|
|
460
|
+
method: request.method,
|
|
461
|
+
path: targetPath,
|
|
462
|
+
headers: { ...proxyHeaders(request, lease, targetPath), connection: "Upgrade", upgrade: request.headers.upgrade }
|
|
463
|
+
});
|
|
464
|
+
upstreamRequest.once("upgrade", (upstream, upstreamSocket, upstreamHead) => {
|
|
465
|
+
const lines = [`HTTP/1.1 ${upstream.statusCode} ${upstream.statusMessage}`];
|
|
466
|
+
for (let index = 0; index < upstream.rawHeaders.length; index += 2) {
|
|
467
|
+
lines.push(`${upstream.rawHeaders[index]}: ${upstream.rawHeaders[index + 1]}`);
|
|
468
|
+
}
|
|
469
|
+
socket.write(`${lines.join("\r\n")}\r
|
|
470
|
+
\r
|
|
471
|
+
`);
|
|
472
|
+
if (upstreamHead.length) socket.write(upstreamHead);
|
|
473
|
+
if (head.length) upstreamSocket.write(head);
|
|
474
|
+
upstreamSocket.pipe(socket).pipe(upstreamSocket);
|
|
475
|
+
});
|
|
476
|
+
upstreamRequest.once("response", (upstream) => {
|
|
477
|
+
socket.end(`HTTP/1.1 ${upstream.statusCode || 502} Preview upgrade rejected\r
|
|
478
|
+
Connection: close\r
|
|
479
|
+
\r
|
|
480
|
+
`);
|
|
481
|
+
});
|
|
482
|
+
upstreamRequest.once("error", () => socket.destroy());
|
|
483
|
+
upstreamRequest.end();
|
|
484
|
+
}
|
|
485
|
+
function sendJson(response, status, value) {
|
|
486
|
+
const body = `${JSON.stringify(value)}
|
|
487
|
+
`;
|
|
488
|
+
response.writeHead(status, {
|
|
489
|
+
"content-type": "application/json; charset=utf-8",
|
|
490
|
+
"content-length": Buffer.byteLength(body),
|
|
491
|
+
"cache-control": "no-store"
|
|
492
|
+
});
|
|
493
|
+
response.end(body);
|
|
494
|
+
}
|
|
495
|
+
function readJson(request) {
|
|
496
|
+
return new Promise((resolve, reject) => {
|
|
497
|
+
const chunks = [];
|
|
498
|
+
let total = 0;
|
|
499
|
+
request.on("data", (chunk) => {
|
|
500
|
+
total += chunk.length;
|
|
501
|
+
if (total > MAX_CONTROL_BODY_BYTES) request.destroy(new Error("Control request is too large"));
|
|
502
|
+
else chunks.push(chunk);
|
|
503
|
+
});
|
|
504
|
+
request.on("end", () => {
|
|
505
|
+
try {
|
|
506
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString() || "{}"));
|
|
507
|
+
} catch {
|
|
508
|
+
reject(new Error("Control request must contain valid JSON"));
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
request.on("error", reject);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function createPreviewServers2(service, {
|
|
515
|
+
listenHost = "127.0.0.1",
|
|
516
|
+
listenPort = 41820,
|
|
517
|
+
socketPath = "/run/cas-preview.sock"
|
|
518
|
+
} = {}) {
|
|
519
|
+
const publicServer = http2.createServer((request, response) => {
|
|
520
|
+
const resolved = service.resolveRequest(request);
|
|
521
|
+
if (resolved == null ? void 0 : resolved.expired) {
|
|
522
|
+
response.writeHead(410, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
523
|
+
response.end("This development preview has expired. Start it again from CAS Cloud.");
|
|
524
|
+
} else if (!resolved) {
|
|
525
|
+
response.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
526
|
+
response.end("Development preview not found.");
|
|
527
|
+
} else {
|
|
528
|
+
proxyHttp(request, response, resolved);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
publicServer.on("upgrade", (request, socket, head) => {
|
|
532
|
+
const resolved = service.resolveRequest(request);
|
|
533
|
+
if (!resolved || resolved.expired) {
|
|
534
|
+
socket.end(`HTTP/1.1 ${(resolved == null ? void 0 : resolved.expired) ? 410 : 404} Preview unavailable\r
|
|
535
|
+
Connection: close\r
|
|
536
|
+
\r
|
|
537
|
+
`);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
proxyUpgrade(request, socket, head, resolved);
|
|
541
|
+
});
|
|
542
|
+
const controlServer = http2.createServer(async (request, response) => {
|
|
543
|
+
try {
|
|
544
|
+
const requestUrl = new URL(request.url, "http://control.internal");
|
|
545
|
+
if (request.method === "GET" && requestUrl.pathname === "/leases") {
|
|
546
|
+
sendJson(response, 200, { leases: service.list() });
|
|
547
|
+
} else if (request.method === "POST" && requestUrl.pathname === "/leases") {
|
|
548
|
+
sendJson(response, 201, { lease: await service.startLease(await readJson(request)) });
|
|
549
|
+
} else if (request.method === "DELETE" && requestUrl.pathname.startsWith("/leases/")) {
|
|
550
|
+
const id = decodeURIComponent(requestUrl.pathname.slice("/leases/".length));
|
|
551
|
+
const stopped = await service.stopLease(id, "stopped by user");
|
|
552
|
+
sendJson(response, stopped ? 200 : 404, { stopped });
|
|
553
|
+
} else {
|
|
554
|
+
sendJson(response, 404, { error: "Not found" });
|
|
555
|
+
}
|
|
556
|
+
} catch (error) {
|
|
557
|
+
sendJson(response, 400, { error: error.message });
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
async function listen() {
|
|
561
|
+
if (fs.existsSync(socketPath)) {
|
|
562
|
+
if (await canConnectSocket(socketPath)) throw new Error(`Preview control socket is already active: ${socketPath}`);
|
|
563
|
+
fs.unlinkSync(socketPath);
|
|
564
|
+
}
|
|
565
|
+
await new Promise((resolve, reject) => controlServer.once("error", reject).listen(socketPath, resolve));
|
|
566
|
+
fs.chmodSync(socketPath, 384);
|
|
567
|
+
try {
|
|
568
|
+
await new Promise((resolve, reject) => publicServer.once("error", reject).listen(listenPort, listenHost, resolve));
|
|
569
|
+
} catch (error) {
|
|
570
|
+
await new Promise((resolve) => controlServer.close(resolve));
|
|
571
|
+
if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath);
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
return { listenHost, listenPort, socketPath };
|
|
575
|
+
}
|
|
576
|
+
async function close() {
|
|
577
|
+
await service.stopAll();
|
|
578
|
+
await Promise.all([publicServer, controlServer].map((server) => new Promise((resolve) => server.close(resolve))));
|
|
579
|
+
if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath);
|
|
580
|
+
}
|
|
581
|
+
return { publicServer, controlServer, listen, close };
|
|
582
|
+
}
|
|
583
|
+
module2.exports = {
|
|
584
|
+
DEFAULT_TTL_MS: DEFAULT_TTL_MS2,
|
|
585
|
+
EphemeralPreviewService: EphemeralPreviewService2,
|
|
586
|
+
createPreviewServers: createPreviewServers2,
|
|
587
|
+
normalizePublicOrigin,
|
|
588
|
+
previewPrefix,
|
|
589
|
+
projectSlug,
|
|
590
|
+
rewritePreviewText
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// src/cli/cas-preview.js
|
|
596
|
+
var http = require("http");
|
|
597
|
+
var os = require("os");
|
|
598
|
+
var path = require("path");
|
|
599
|
+
var {
|
|
600
|
+
DEFAULT_TTL_MS,
|
|
601
|
+
EphemeralPreviewService,
|
|
602
|
+
createPreviewServers
|
|
603
|
+
} = require_ephemeral_preview_service();
|
|
604
|
+
function defaultSocketPath() {
|
|
605
|
+
if (process.env.CAS_PREVIEW_SOCKET) return path.resolve(process.env.CAS_PREVIEW_SOCKET);
|
|
606
|
+
if (process.env.XDG_RUNTIME_DIR && path.isAbsolute(process.env.XDG_RUNTIME_DIR)) {
|
|
607
|
+
return path.join(process.env.XDG_RUNTIME_DIR, "cas-preview.sock");
|
|
608
|
+
}
|
|
609
|
+
const identity = typeof process.getuid === "function" ? process.getuid() : "user";
|
|
610
|
+
return path.join(os.tmpdir(), `cas-preview-${identity}.sock`);
|
|
611
|
+
}
|
|
612
|
+
var DEFAULT_SOCKET = defaultSocketPath();
|
|
613
|
+
function help() {
|
|
614
|
+
return `CAS ephemeral development previews
|
|
615
|
+
|
|
616
|
+
Usage:
|
|
617
|
+
cas-preview start --cwd DIR --port PORT -- COMMAND [ARGS...]
|
|
618
|
+
cas-preview list
|
|
619
|
+
cas-preview stop LEASE_ID
|
|
620
|
+
cas-preview serve [--listen HOST] [--port PORT] [--socket PATH]
|
|
621
|
+
[--root DIR] [--public-origin URL] [--ttl-seconds N]
|
|
622
|
+
|
|
623
|
+
Every process started by this service has a hard two-hour TTL by default.
|
|
624
|
+
The service refuses to adopt or terminate processes it did not start.`;
|
|
625
|
+
}
|
|
626
|
+
function takeOption(args, name, fallback) {
|
|
627
|
+
const index = args.indexOf(name);
|
|
628
|
+
if (index === -1) return fallback;
|
|
629
|
+
if (!args[index + 1]) throw new Error(`${name} requires a value`);
|
|
630
|
+
const value = args[index + 1];
|
|
631
|
+
args.splice(index, 2);
|
|
632
|
+
return value;
|
|
633
|
+
}
|
|
634
|
+
function controlRequest(socketPath, method, requestPath, body) {
|
|
635
|
+
return new Promise((resolve, reject) => {
|
|
636
|
+
const payload = body === void 0 ? null : Buffer.from(JSON.stringify(body));
|
|
637
|
+
const request = http.request({
|
|
638
|
+
socketPath,
|
|
639
|
+
method,
|
|
640
|
+
path: requestPath,
|
|
641
|
+
headers: payload ? { "content-type": "application/json", "content-length": payload.length } : {}
|
|
642
|
+
}, (response) => {
|
|
643
|
+
const chunks = [];
|
|
644
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
645
|
+
response.on("end", () => {
|
|
646
|
+
let value;
|
|
647
|
+
try {
|
|
648
|
+
value = JSON.parse(Buffer.concat(chunks).toString() || "{}");
|
|
649
|
+
} catch {
|
|
650
|
+
reject(new Error(`Preview service returned HTTP ${response.statusCode}`));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if ((response.statusCode || 500) >= 400) reject(new Error(value.error || `Preview service returned HTTP ${response.statusCode}`));
|
|
654
|
+
else resolve(value);
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
request.once("error", (error) => reject(new Error(`Cannot reach the preview service at ${socketPath}: ${error.message}`)));
|
|
658
|
+
if (payload) request.end(payload);
|
|
659
|
+
else request.end();
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
async function serve(args) {
|
|
663
|
+
const listenHost = takeOption(args, "--listen", "127.0.0.1");
|
|
664
|
+
const listenPort = Number(takeOption(args, "--port", "41820"));
|
|
665
|
+
const socketPath = path.resolve(takeOption(args, "--socket", DEFAULT_SOCKET));
|
|
666
|
+
const root = path.resolve(takeOption(args, "--root", process.env.CAS_PREVIEW_ROOT || process.cwd()));
|
|
667
|
+
const ttlSeconds = Number(takeOption(args, "--ttl-seconds", String(DEFAULT_TTL_MS / 1e3)));
|
|
668
|
+
const publicOrigin = takeOption(
|
|
669
|
+
args,
|
|
670
|
+
"--public-origin",
|
|
671
|
+
process.env.CAS_PREVIEW_PUBLIC_ORIGIN || `http://${listenHost}:${listenPort}`
|
|
672
|
+
);
|
|
673
|
+
if (args.length) throw new Error(`Unknown serve arguments: ${args.join(" ")}`);
|
|
674
|
+
if (!["127.0.0.1", "::1", "localhost"].includes(listenHost)) {
|
|
675
|
+
throw new Error("Preview service must listen on loopback; place a TLS reverse proxy in front of it");
|
|
676
|
+
}
|
|
677
|
+
if (!Number.isInteger(listenPort) || listenPort < 1 || listenPort > 65535) throw new Error("Invalid listen port");
|
|
678
|
+
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) throw new Error("Invalid TTL");
|
|
679
|
+
const service = new EphemeralPreviewService({ root, publicOrigin, ttlMs: ttlSeconds * 1e3 });
|
|
680
|
+
const servers = createPreviewServers(service, { listenHost, listenPort, socketPath });
|
|
681
|
+
await servers.listen();
|
|
682
|
+
console.log(`CAS preview service listening on http://${listenHost}:${listenPort}`);
|
|
683
|
+
console.log(`Public origin: ${service.publicOrigin}; root: ${service.root}`);
|
|
684
|
+
console.log(`Control socket: ${socketPath}; TTL: ${ttlSeconds} seconds`);
|
|
685
|
+
let closing = false;
|
|
686
|
+
const close = async (signal) => {
|
|
687
|
+
if (closing) return;
|
|
688
|
+
closing = true;
|
|
689
|
+
console.log(`${signal}; stopping ephemeral previews`);
|
|
690
|
+
await servers.close();
|
|
691
|
+
process.exitCode = 0;
|
|
692
|
+
};
|
|
693
|
+
process.once("SIGTERM", () => close("SIGTERM").catch((error) => {
|
|
694
|
+
console.error(error);
|
|
695
|
+
process.exitCode = 1;
|
|
696
|
+
}));
|
|
697
|
+
process.once("SIGINT", () => close("SIGINT").catch((error) => {
|
|
698
|
+
console.error(error);
|
|
699
|
+
process.exitCode = 1;
|
|
700
|
+
}));
|
|
701
|
+
}
|
|
702
|
+
async function start(args) {
|
|
703
|
+
const separator = args.indexOf("--");
|
|
704
|
+
if (separator === -1 || separator === args.length - 1) throw new Error("Pass the server command after --");
|
|
705
|
+
const options = args.slice(0, separator);
|
|
706
|
+
const command = args.slice(separator + 1);
|
|
707
|
+
const socketPath = path.resolve(takeOption(options, "--socket", DEFAULT_SOCKET));
|
|
708
|
+
const cwd = path.resolve(takeOption(options, "--cwd", process.cwd()));
|
|
709
|
+
const port = Number(takeOption(options, "--port"));
|
|
710
|
+
const json = options.includes("--json");
|
|
711
|
+
if (json) options.splice(options.indexOf("--json"), 1);
|
|
712
|
+
if (options.length) throw new Error(`Unknown start arguments: ${options.join(" ")}`);
|
|
713
|
+
const { lease } = await controlRequest(socketPath, "POST", "/leases", { cwd, port, command });
|
|
714
|
+
if (json) console.log(JSON.stringify(lease));
|
|
715
|
+
else {
|
|
716
|
+
console.log(lease.url);
|
|
717
|
+
console.log(`Lease: ${lease.id}`);
|
|
718
|
+
console.log(`Expires: ${new Date(lease.expiresAt).toISOString()}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async function list(args) {
|
|
722
|
+
const socketPath = path.resolve(takeOption(args, "--socket", DEFAULT_SOCKET));
|
|
723
|
+
if (args.length) throw new Error(`Unknown list arguments: ${args.join(" ")}`);
|
|
724
|
+
const { leases } = await controlRequest(socketPath, "GET", "/leases");
|
|
725
|
+
if (!leases.length) {
|
|
726
|
+
console.log("No active ephemeral previews.");
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
for (const lease of leases) console.log(`${lease.id} ${new Date(lease.expiresAt).toISOString()} ${lease.url}`);
|
|
730
|
+
}
|
|
731
|
+
async function stop(args) {
|
|
732
|
+
const socketPath = path.resolve(takeOption(args, "--socket", DEFAULT_SOCKET));
|
|
733
|
+
const id = args.shift();
|
|
734
|
+
if (!id || args.length) throw new Error("Usage: cas-preview stop LEASE_ID");
|
|
735
|
+
await controlRequest(socketPath, "DELETE", `/leases/${encodeURIComponent(id)}`);
|
|
736
|
+
console.log(`Stopped preview ${id}.`);
|
|
737
|
+
}
|
|
738
|
+
async function main(argv = process.argv.slice(2)) {
|
|
739
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10);
|
|
740
|
+
if (!Number.isInteger(nodeMajor) || nodeMajor < 20) throw new Error(`Node.js 20 or newer is required; found ${process.version}`);
|
|
741
|
+
const args = [...argv];
|
|
742
|
+
const command = args.shift();
|
|
743
|
+
if (!command || ["help", "--help", "-h"].includes(command)) {
|
|
744
|
+
console.log(help());
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (command === "serve") return serve(args);
|
|
748
|
+
if (command === "start") return start(args);
|
|
749
|
+
if (command === "list") return list(args);
|
|
750
|
+
if (command === "stop") return stop(args);
|
|
751
|
+
throw new Error(`Unknown command: ${command}
|
|
752
|
+
|
|
753
|
+
${help()}`);
|
|
754
|
+
}
|
|
755
|
+
if (require.main === module) {
|
|
756
|
+
Promise.resolve(main()).catch((error) => {
|
|
757
|
+
console.error(`cas-preview: ${error.message}`);
|
|
758
|
+
process.exitCode = 1;
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
module.exports = { controlRequest, defaultSocketPath, help, main };
|