@vivero/stoma-cli 0.1.0-rc.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/dist/bin.js ADDED
@@ -0,0 +1,1050 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Builtins, Cli } from "clipanion";
5
+
6
+ // src/commands/run.ts
7
+ import path2 from "path";
8
+ import { Command, Option } from "clipanion";
9
+
10
+ // src/gateway/resolve.ts
11
+ import { existsSync } from "fs";
12
+ import { mkdtemp, rm, unlink, writeFile } from "fs/promises";
13
+ import { createRequire } from "module";
14
+ import { tmpdir } from "os";
15
+ import path from "path";
16
+ import { pathToFileURL } from "url";
17
+ import { build } from "esbuild";
18
+ var TS_EXTENSIONS = [".ts", ".tsx", ".mts"];
19
+ async function resolveGateway(filePathOrUrl, options = {}) {
20
+ if (isRemoteUrl(filePathOrUrl)) {
21
+ if (!options.trustRemote) {
22
+ throw new Error(
23
+ "Remote URLs require the --trust-remote flag.\nThis will download and execute code from the URL. Only use with trusted sources."
24
+ );
25
+ }
26
+ return resolveRemoteGateway(filePathOrUrl, options);
27
+ }
28
+ if (!existsSync(filePathOrUrl)) {
29
+ throw new Error(`File not found: ${filePathOrUrl}`);
30
+ }
31
+ return resolveLocalFile(filePathOrUrl, options);
32
+ }
33
+ async function resolveLocalFile(filePath, options) {
34
+ const isTypeScript = TS_EXTENSIONS.some((ext) => filePath.endsWith(ext));
35
+ let mod;
36
+ if (isTypeScript) {
37
+ mod = await importTypeScript(filePath);
38
+ } else {
39
+ try {
40
+ mod = await import(pathToFileURL(filePath).href);
41
+ } catch (err) {
42
+ throw new Error(
43
+ `Failed to import: ${filePath}
44
+ ${err instanceof Error ? err.message : String(err)}`
45
+ );
46
+ }
47
+ }
48
+ return resolveFromModule(mod, options);
49
+ }
50
+ function isRemoteUrl(input) {
51
+ return input.startsWith("http://") || input.startsWith("https://");
52
+ }
53
+ async function resolveRemoteGateway(url, options) {
54
+ const res = await fetch(url);
55
+ if (!res.ok) {
56
+ throw new Error(
57
+ `Failed to fetch remote gateway: ${res.status} ${res.statusText}`
58
+ );
59
+ }
60
+ const filename = filenameFromUrl(url, res.headers.get("content-type"));
61
+ const tmpDir = await mkdtemp(path.join(tmpdir(), "stoma-remote-"));
62
+ const tmpPath = path.join(tmpDir, filename);
63
+ try {
64
+ await writeFile(tmpPath, await res.text(), "utf-8");
65
+ return await resolveLocalFile(tmpPath, options);
66
+ } finally {
67
+ await unlink(tmpPath).catch(() => {
68
+ });
69
+ await rm(tmpDir, { recursive: true, force: true }).catch(() => {
70
+ });
71
+ }
72
+ }
73
+ function filenameFromUrl(url, contentType) {
74
+ const pathname = new URL(url).pathname;
75
+ const basename = path.basename(pathname);
76
+ if (/\.(ts|tsx|mts|js|mjs|cjs)$/.test(basename)) {
77
+ return basename;
78
+ }
79
+ if (contentType?.includes("javascript")) {
80
+ return "gateway.mjs";
81
+ }
82
+ if (contentType?.includes("typescript")) {
83
+ return "gateway.ts";
84
+ }
85
+ return basename ? `${basename}.ts` : "gateway.ts";
86
+ }
87
+ function getCliNodePaths() {
88
+ const require2 = createRequire(import.meta.url);
89
+ return (require2.resolve.paths("@vivero/stoma") ?? []).filter(existsSync);
90
+ }
91
+ async function importTypeScript(filePath) {
92
+ const tmpFile = filePath.replace(/\.tsx?$/, `.stoma-tmp-${Date.now()}.mjs`);
93
+ try {
94
+ const result = await build({
95
+ entryPoints: [filePath],
96
+ bundle: true,
97
+ format: "esm",
98
+ platform: "node",
99
+ target: "node20",
100
+ nodePaths: getCliNodePaths(),
101
+ write: false,
102
+ logLevel: "silent"
103
+ });
104
+ if (!result.outputFiles?.length) {
105
+ throw new Error("TypeScript transpilation produced no output");
106
+ }
107
+ await writeFile(tmpFile, result.outputFiles[0].text, "utf-8");
108
+ return await import(pathToFileURL(tmpFile).href);
109
+ } catch (err) {
110
+ if (err instanceof Error && err.message.includes("TypeScript transpilation")) {
111
+ throw err;
112
+ }
113
+ throw new Error(
114
+ `Failed to transpile TypeScript: ${filePath}
115
+ ${err instanceof Error ? err.message : String(err)}`
116
+ );
117
+ } finally {
118
+ await unlink(tmpFile).catch(() => {
119
+ });
120
+ }
121
+ }
122
+ async function resolveFromModule(mod, _options = {}) {
123
+ if (typeof mod.createPlaygroundGateway === "function") {
124
+ const result = await mod.createPlaygroundGateway();
125
+ return asGatewayInstance(result, "createPlaygroundGateway()");
126
+ }
127
+ if (typeof mod.default === "function") {
128
+ const result = await mod.default();
129
+ return asGatewayInstance(result, "default()");
130
+ }
131
+ if (isGatewayInstance(mod.default)) {
132
+ return mod.default;
133
+ }
134
+ if (isHonoApp(mod.default)) {
135
+ return {
136
+ app: mod.default,
137
+ name: "unnamed-gateway",
138
+ routeCount: 0,
139
+ _registry: { routes: [], policies: [], gatewayName: "unnamed-gateway" }
140
+ };
141
+ }
142
+ throw new Error(
143
+ "Could not resolve a gateway from the module exports.\n\nThe file must export a gateway in one of these forms:\n export default createGateway({ ... })\n export default async function() { return createGateway({ ... }) }\n export function createPlaygroundGateway() { return createGateway({ ... }) }\n export default app // a Hono app with a .fetch method"
144
+ );
145
+ }
146
+ function isGatewayInstance(value) {
147
+ return typeof value === "object" && value !== null && "app" in value && "_registry" in value;
148
+ }
149
+ function isHonoApp(value) {
150
+ return typeof value === "object" && value !== null && "fetch" in value && typeof value.fetch === "function";
151
+ }
152
+ function asGatewayInstance(value, source) {
153
+ if (isGatewayInstance(value)) {
154
+ return value;
155
+ }
156
+ if (isHonoApp(value)) {
157
+ return {
158
+ app: value,
159
+ name: "unnamed-gateway",
160
+ routeCount: 0,
161
+ _registry: { routes: [], policies: [], gatewayName: "unnamed-gateway" }
162
+ };
163
+ }
164
+ throw new Error(
165
+ `${source} did not return a valid gateway instance. Expected an object with .app and ._registry properties, or a Hono app with a .fetch method.`
166
+ );
167
+ }
168
+
169
+ // src/playground/html.ts
170
+ function playgroundHtml(registry) {
171
+ const registryJson = JSON.stringify(registry).replace(/</g, "\\u003c");
172
+ return `<!DOCTYPE html>
173
+ <html lang="en">
174
+ <head>
175
+ <meta charset="utf-8" />
176
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
177
+ <title>${esc(registry.gatewayName)} \u2014 Stoma Playground</title>
178
+ <style>
179
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
180
+ :root{
181
+ --bg:#1e1e1e;--bg2:#252526;--bg3:#2d2d2d;
182
+ --border:#3c3c3c;--text:#d4d4d4;--muted:#969696;--dim:#555;
183
+ --teal:#4ec9b0;--blue:#0e639c;--blue2:#1177bb;
184
+ --yellow:#dcdcaa;--red:#f48771;--blue-text:#569cd6;
185
+ --mono:"Cascadia Code","Fira Code","JetBrains Mono","SF Mono",monospace;
186
+ --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
187
+ }
188
+ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans)}
189
+ body{display:flex;flex-direction:column;overflow:hidden}
190
+
191
+ /* Header */
192
+ .hdr{display:flex;align-items:center;justify-content:space-between;height:3rem;padding:0 1rem;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}
193
+ .hdr-left{display:flex;align-items:center;gap:.5rem}
194
+ .logo{font-weight:700;font-size:.875rem;color:var(--teal);text-decoration:none}
195
+ .hdr-div{color:var(--dim);font-size:.875rem}
196
+ .hdr-title{font-size:.8125rem;color:var(--muted)}
197
+
198
+ /* Routes bar */
199
+ .routes-bar{padding:.5rem 1rem;display:flex;flex-wrap:wrap;gap:.25rem;flex-shrink:0;border-bottom:1px solid var(--border);background:var(--bg)}
200
+ .chip{all:unset;display:inline-flex;align-items:center;gap:.375rem;height:1.5rem;padding:0 .5rem;border-radius:.25rem;border:1px solid var(--border);background:var(--bg3);font-family:var(--mono);font-size:.6875rem;cursor:pointer;user-select:none;transition:border-color .15s,background .15s}
201
+ .chip:hover{border-color:var(--blue);background:#333}
202
+ .method{font-size:.5625rem;font-weight:700;letter-spacing:.04em;padding:.0625rem .25rem;border-radius:.125rem}
203
+ .m-get{background:var(--teal);color:var(--bg)}
204
+ .m-post{background:var(--blue-text);color:#fff}
205
+ .m-put,.m-patch{background:var(--yellow);color:var(--bg)}
206
+ .m-delete{background:var(--red);color:var(--bg)}
207
+
208
+ /* Workspace \u2014 two pane split */
209
+ .workspace{flex:1;display:flex;gap:.75rem;padding:.75rem 1rem;overflow:hidden}
210
+ .pane-left{flex:1;display:flex;flex-direction:column;gap:.625rem;overflow-y:auto;min-width:0}
211
+ .pane-right{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden}
212
+
213
+ /* Token Store */
214
+ .token-store{border:1px solid var(--border);border-radius:.25rem;background:var(--bg2);overflow:hidden;flex-shrink:0}
215
+ .token-header{display:flex;align-items:center;justify-content:space-between;padding:.375rem .625rem;background:var(--bg3);border-bottom:1px solid var(--border)}
216
+ .token-title{font-size:.625rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}
217
+ .token-item{display:flex;align-items:center;gap:.375rem;padding:.25rem .625rem;border-bottom:1px solid rgba(60,60,60,.2)}
218
+ .token-item:last-child{border-bottom:none}
219
+ .token-name{font-size:.6875rem;font-family:var(--mono);color:var(--teal);min-width:6rem;flex-shrink:0}
220
+ .token-val{font-size:.6875rem;font-family:var(--mono);color:var(--dim);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
221
+ .token-actions{display:flex;gap:.25rem;flex-shrink:0}
222
+ .token-btn{all:unset;display:inline-flex;align-items:center;height:1.25rem;padding:0 .375rem;border-radius:.1875rem;font-size:.5625rem;font-weight:600;cursor:pointer;transition:background .15s;user-select:none}
223
+ .token-btn-use{background:var(--blue);color:#fff}
224
+ .token-btn-use:hover{background:var(--blue2)}
225
+ .token-btn-del{background:var(--bg3);color:var(--muted);border:1px solid var(--border)}
226
+ .token-btn-del:hover{background:#3a3a3a}
227
+
228
+ /* Form */
229
+ .form-row{display:flex;gap:.375rem}
230
+ .sel{width:5.5rem;height:1.75rem;padding:0 .375rem;border-radius:.25rem;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:.75rem;font-family:var(--mono);cursor:pointer;outline:none;flex-shrink:0}
231
+ .sel:focus{border-color:var(--blue)}
232
+ .inp{flex:1;height:1.75rem;padding:0 .5rem;border-radius:.25rem;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:.75rem;font-family:var(--mono);outline:none;min-width:0}
233
+ .inp:focus{border-color:var(--blue)}
234
+ .btn{all:unset;display:inline-flex;align-items:center;height:1.75rem;padding:0 .75rem;border-radius:.25rem;background:var(--blue);color:#fff;font-size:.75rem;font-weight:600;cursor:pointer;transition:background .15s;user-select:none;white-space:nowrap}
235
+ .btn:hover{background:var(--blue2)}
236
+ .btn:disabled{opacity:.4;cursor:not-allowed}
237
+
238
+ .form-sections{display:flex;flex-direction:column;gap:.375rem;margin-top:.375rem}
239
+ .field{display:flex;flex-direction:column;gap:.125rem}
240
+ .lbl{font-size:.5625rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}
241
+ .lbl-row{display:flex;align-items:center;justify-content:space-between}
242
+ .sm-btn{all:unset;font-size:.5625rem;font-weight:600;color:var(--blue-text);cursor:pointer;user-select:none}
243
+ .sm-btn:hover{text-decoration:underline}
244
+ .ta{width:100%;padding:.375rem .5rem;border-radius:.25rem;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:.6875rem;font-family:var(--mono);line-height:1.4;resize:vertical;outline:none;flex:1;min-height:3.5rem}
245
+ .ta:focus{border-color:var(--blue)}
246
+ .ta::placeholder{color:var(--dim)}
247
+
248
+ /* Header rows */
249
+ .hdr-rows{display:flex;flex-direction:column;gap:.25rem}
250
+ .hdr-row{display:flex;gap:.25rem;align-items:center}
251
+ .hdr-input{height:1.5rem;padding:0 .375rem;border-radius:.25rem;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:.6875rem;font-family:var(--mono);outline:none}
252
+ .hdr-input:focus{border-color:var(--blue)}
253
+ .hdr-name{width:10rem;flex-shrink:0}
254
+ .hdr-val{flex:1;min-width:0}
255
+ .hdr-del{all:unset;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:.25rem;font-size:.75rem;color:var(--dim);cursor:pointer;flex-shrink:0}
256
+ .hdr-del:hover{color:var(--red);background:rgba(244,135,113,.1)}
257
+
258
+ /* Callback Banner */
259
+ .callback-banner{padding:.625rem .75rem;background:#1a2332;border:1px solid var(--blue-text);border-radius:.25rem;font-size:.75rem;color:var(--blue-text);flex-shrink:0}
260
+ .callback-banner strong{color:#fff}
261
+
262
+ /* Response panel */
263
+ .res{flex:1;display:flex;flex-direction:column;border:1px solid var(--border);border-radius:.25rem;background:var(--bg2);overflow:hidden}
264
+ .res-empty{padding:2rem;text-align:center;color:var(--dim);font-size:.8125rem}
265
+ .res-top{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}
266
+ .badge{display:inline-flex;align-items:center;height:1.25rem;padding:0 .375rem;border-radius:.1875rem;font-family:var(--mono);font-size:.6875rem;font-weight:700;line-height:1}
267
+ .b-2xx{background:var(--teal);color:var(--bg)}
268
+ .b-3xx{background:var(--blue-text);color:#fff}
269
+ .b-4xx{background:var(--yellow);color:var(--bg)}
270
+ .b-5xx{background:var(--red);color:var(--bg)}
271
+ .timing{font-size:.75rem;font-family:var(--mono);color:var(--muted)}
272
+
273
+ /* Response tabs */
274
+ .res-tabs{display:flex;background:var(--bg3);border-bottom:1px solid var(--border);flex-shrink:0}
275
+ .res-tab{all:unset;padding:.375rem .75rem;font-size:.6875rem;font-weight:600;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent;transition:color .15s,border-color .15s;user-select:none}
276
+ .res-tab:hover{color:var(--text)}
277
+ .res-tab.active{color:var(--teal);border-bottom-color:var(--teal)}
278
+ .res-tab-count{font-weight:400;color:var(--dim);margin-left:.25rem}
279
+
280
+ /* Tab content */
281
+ .res-tab-content{flex:1;overflow:auto;display:none}
282
+ .res-tab-content.active{display:block}
283
+ .res-body{padding:.5rem .75rem}
284
+ .res-body pre{font-family:var(--mono);font-size:.6875rem;line-height:1.5;white-space:pre-wrap;word-break:break-word;margin:0}
285
+ .htable{width:100%;border-collapse:collapse;font-size:.6875rem;font-family:var(--mono)}
286
+ .htable td{padding:.25rem .75rem;border-bottom:1px solid rgba(60,60,60,.2);vertical-align:top}
287
+ .htable td:first-child{color:var(--muted);white-space:nowrap;width:1%;padding-right:.375rem}
288
+ .htable td:last-child{word-break:break-all}
289
+
290
+ .res-error{padding:1rem;color:var(--red);font-family:var(--mono);font-size:.8125rem;text-align:center;word-break:break-word}
291
+
292
+ /* Redirect banner (OAuth) */
293
+ .res-redirect{padding:.75rem;background:#1a2332;border-bottom:1px solid var(--border);flex-shrink:0}
294
+ .res-redirect-title{font-size:.75rem;font-weight:700;color:var(--blue-text);margin-bottom:.25rem}
295
+ .res-redirect-url{font-size:.6875rem;font-family:var(--mono);color:var(--muted);word-break:break-all;margin-bottom:.5rem}
296
+ .res-redirect-btn{all:unset;display:inline-flex;align-items:center;height:1.5rem;padding:0 .625rem;border-radius:.25rem;background:var(--blue-text);color:#fff;font-size:.6875rem;font-weight:600;cursor:pointer;transition:background .15s;user-select:none}
297
+ .res-redirect-btn:hover{background:#3d8fd6}
298
+ .res-redirect-notice{font-size:.5625rem;color:var(--dim);margin-top:.375rem}
299
+
300
+ /* JSON syntax highlighting */
301
+ .j-key{color:#9cdcfe}
302
+ .j-str{color:#ce9178}
303
+ .j-num{color:#b5cea8}
304
+ .j-lit{color:#569cd6}
305
+ </style>
306
+ </head>
307
+ <body>
308
+
309
+ <header class="hdr">
310
+ <div class="hdr-left">
311
+ <span class="logo">Stoma</span>
312
+ <span class="hdr-div">/</span>
313
+ <span class="hdr-title" id="gw-name"></span>
314
+ </div>
315
+ </header>
316
+
317
+ <div class="routes-bar" id="routes"></div>
318
+
319
+ <div class="workspace">
320
+ <!-- Left pane: request -->
321
+ <div class="pane-left">
322
+ <div class="token-store" id="token-store" style="display:none">
323
+ <div class="token-header">
324
+ <span class="token-title">Saved Tokens</span>
325
+ <button class="sm-btn" id="clear-tokens" type="button">Clear All</button>
326
+ </div>
327
+ <div id="token-list"></div>
328
+ </div>
329
+
330
+ <form id="form" autocomplete="off">
331
+ <div class="form-row">
332
+ <select class="sel" id="method">
333
+ <option>GET</option><option>POST</option><option>PUT</option><option>PATCH</option><option>DELETE</option>
334
+ </select>
335
+ <input class="inp" id="path" placeholder="/path" value="/" />
336
+ <button class="btn" type="submit" id="send">Send</button>
337
+ </div>
338
+ <div class="form-sections">
339
+ <div class="field">
340
+ <div class="lbl-row">
341
+ <label class="lbl">Headers</label>
342
+ <button class="sm-btn" id="add-header" type="button">+ Add</button>
343
+ </div>
344
+ <div class="hdr-rows" id="header-rows"></div>
345
+ </div>
346
+ <div class="field" style="flex:1;display:flex;flex-direction:column">
347
+ <label class="lbl" for="body">Body</label>
348
+ <textarea class="ta" id="body" placeholder='{"key":"value"}'></textarea>
349
+ </div>
350
+ </div>
351
+ </form>
352
+
353
+ <div class="callback-banner" id="callback-banner" style="display:none"></div>
354
+ </div>
355
+
356
+ <!-- Right pane: response -->
357
+ <div class="pane-right">
358
+ <div class="res" id="response">
359
+ <div class="res-empty">Send a request to see the response</div>
360
+ </div>
361
+ </div>
362
+ </div>
363
+
364
+ <script>
365
+ var registry = ${registryJson};
366
+ document.getElementById("gw-name").textContent = registry.gatewayName || "Playground";
367
+
368
+ // \u2500\u2500 Route chips \u2500\u2500
369
+
370
+ var routesEl = document.getElementById("routes");
371
+ for (var ri = 0; ri < registry.routes.length; ri++) {
372
+ var route = registry.routes[ri];
373
+ for (var mi = 0; mi < route.methods.length; mi++) {
374
+ (function(m, path) {
375
+ var chip = document.createElement("button");
376
+ chip.type = "button";
377
+ chip.className = "chip";
378
+ chip.innerHTML = '<span class="method m-' + m.toLowerCase() + '">' + m + '</span><span>' + esc(path) + '</span>';
379
+ chip.onclick = function() {
380
+ document.getElementById("method").value = m;
381
+ document.getElementById("path").value = path;
382
+ document.getElementById("form").requestSubmit();
383
+ };
384
+ routesEl.appendChild(chip);
385
+ })(route.methods[mi], route.path.replace(/\\*$/, ""));
386
+ }
387
+ }
388
+
389
+ // \u2500\u2500 Header table management \u2500\u2500
390
+
391
+ var headerRowsEl = document.getElementById("header-rows");
392
+
393
+ function addHeaderRow(name, value) {
394
+ var row = document.createElement("div");
395
+ row.className = "hdr-row";
396
+ var nameInp = document.createElement("input");
397
+ nameInp.className = "hdr-input hdr-name";
398
+ nameInp.placeholder = "Header name";
399
+ nameInp.value = name || "";
400
+ var valInp = document.createElement("input");
401
+ valInp.className = "hdr-input hdr-val";
402
+ valInp.placeholder = "Value";
403
+ valInp.value = value || "";
404
+ var del = document.createElement("button");
405
+ del.type = "button";
406
+ del.className = "hdr-del";
407
+ del.textContent = "\\u00d7";
408
+ del.onclick = function() { row.remove(); };
409
+ row.appendChild(nameInp);
410
+ row.appendChild(valInp);
411
+ row.appendChild(del);
412
+ headerRowsEl.appendChild(row);
413
+ return row;
414
+ }
415
+
416
+ function getHeaders() {
417
+ var headers = {};
418
+ var rows = headerRowsEl.querySelectorAll(".hdr-row");
419
+ for (var i = 0; i < rows.length; i++) {
420
+ var inputs = rows[i].querySelectorAll("input");
421
+ var n = inputs[0].value.trim();
422
+ var v = inputs[1].value.trim();
423
+ if (n) headers[n] = v;
424
+ }
425
+ return headers;
426
+ }
427
+
428
+ function setHeader(name, value) {
429
+ var rows = headerRowsEl.querySelectorAll(".hdr-row");
430
+ var lowerName = name.toLowerCase();
431
+ for (var i = 0; i < rows.length; i++) {
432
+ var inputs = rows[i].querySelectorAll("input");
433
+ if (inputs[0].value.trim().toLowerCase() === lowerName) {
434
+ inputs[1].value = value;
435
+ return;
436
+ }
437
+ }
438
+ addHeaderRow(name, value);
439
+ }
440
+
441
+ document.getElementById("add-header").onclick = function() { addHeaderRow(); };
442
+
443
+ // \u2500\u2500 Token store (localStorage) \u2500\u2500
444
+
445
+ var TOKEN_KEY = "stoma-playground-tokens";
446
+
447
+ function loadTokens() {
448
+ try {
449
+ var raw = localStorage.getItem(TOKEN_KEY);
450
+ return raw ? JSON.parse(raw) : [];
451
+ } catch (e) { return []; }
452
+ }
453
+
454
+ function persistTokens(tokens) {
455
+ try { localStorage.setItem(TOKEN_KEY, JSON.stringify(tokens)); } catch (e) {}
456
+ }
457
+
458
+ function saveToken(label, value) {
459
+ var tokens = loadTokens();
460
+ for (var i = 0; i < tokens.length; i++) {
461
+ if (tokens[i].label === label) {
462
+ tokens[i].value = value;
463
+ persistTokens(tokens);
464
+ renderTokenStore();
465
+ return;
466
+ }
467
+ }
468
+ tokens.push({ label: label, value: value });
469
+ persistTokens(tokens);
470
+ renderTokenStore();
471
+ }
472
+
473
+ function removeToken(idx) {
474
+ var tokens = loadTokens();
475
+ tokens.splice(idx, 1);
476
+ persistTokens(tokens);
477
+ renderTokenStore();
478
+ }
479
+
480
+ function clearTokens() {
481
+ persistTokens([]);
482
+ renderTokenStore();
483
+ }
484
+
485
+ function applyToken(idx) {
486
+ var tokens = loadTokens();
487
+ if (tokens[idx]) setHeader("Authorization", "Bearer " + tokens[idx].value);
488
+ }
489
+
490
+ function renderTokenStore() {
491
+ var tokens = loadTokens();
492
+ var storeEl = document.getElementById("token-store");
493
+ var listEl = document.getElementById("token-list");
494
+ if (!tokens.length) {
495
+ storeEl.style.display = "none";
496
+ listEl.innerHTML = "";
497
+ return;
498
+ }
499
+ storeEl.style.display = "";
500
+ var html = "";
501
+ for (var i = 0; i < tokens.length; i++) {
502
+ var shortVal = tokens[i].value.length > 40 ? tokens[i].value.slice(0, 40) + "\\u2026" : tokens[i].value;
503
+ html +=
504
+ '<div class="token-item">' +
505
+ '<span class="token-name">' + esc(tokens[i].label) + '</span>' +
506
+ '<span class="token-val">' + esc(shortVal) + '</span>' +
507
+ '<span class="token-actions">' +
508
+ '<button class="token-btn token-btn-use" data-idx="' + i + '">Use</button>' +
509
+ '<button class="token-btn token-btn-del" data-idx="' + i + '">\\u00d7</button>' +
510
+ '</span>' +
511
+ '</div>';
512
+ }
513
+ listEl.innerHTML = html;
514
+ listEl.querySelectorAll(".token-btn-use").forEach(function(btn) {
515
+ btn.onclick = function() { applyToken(Number(btn.dataset.idx)); };
516
+ });
517
+ listEl.querySelectorAll(".token-btn-del").forEach(function(btn) {
518
+ btn.onclick = function() { removeToken(Number(btn.dataset.idx)); };
519
+ });
520
+ }
521
+
522
+ document.getElementById("clear-tokens").onclick = clearTokens;
523
+
524
+ // Restore tokens on load
525
+ renderTokenStore();
526
+ var initTokens = loadTokens();
527
+ if (initTokens.length) applyToken(initTokens.length - 1);
528
+
529
+ // \u2500\u2500 Token auto-detection from responses \u2500\u2500
530
+
531
+ function detectAndSaveTokens(data) {
532
+ if (!data || !data.body) return;
533
+ try {
534
+ var parsed = JSON.parse(data.body);
535
+ var token = parsed.access_token || parsed.token;
536
+ if (typeof token === "string" && token.length > 0) {
537
+ var label = parsed.token_type ? parsed.token_type + "_token" : "access_token";
538
+ saveToken(label, token);
539
+ setHeader("Authorization", "Bearer " + token);
540
+ }
541
+ } catch (e) {}
542
+ }
543
+
544
+ // \u2500\u2500 Send request \u2500\u2500
545
+ //
546
+ // Direct fetch to the gateway (same origin). For redirect responses the
547
+ // browser returns an opaque response (redirect:"manual"), so we fall back
548
+ // to the server-side proxy which can read the full 3xx details.
549
+
550
+ function sendViaProxy(method, path, headers, bodyText) {
551
+ return fetch("/__playground/send", {
552
+ method: "POST",
553
+ headers: { "content-type": "application/json" },
554
+ body: JSON.stringify({ method: method, path: path, headers: headers, body: bodyText || undefined }),
555
+ }).then(function(proxyRes) {
556
+ if (!proxyRes.ok) {
557
+ return proxyRes.json().catch(function() { return null; }).then(function(err) {
558
+ renderError(err && err.error ? err.error : "Request failed: " + proxyRes.status);
559
+ });
560
+ }
561
+ return proxyRes.json().then(function(data) {
562
+ if (data.error) renderError(data.error);
563
+ else { renderResponse(data); detectAndSaveTokens(data); }
564
+ });
565
+ });
566
+ }
567
+
568
+ document.getElementById("form").addEventListener("submit", function(e) {
569
+ e.preventDefault();
570
+ var callbackBanner = document.getElementById("callback-banner");
571
+ callbackBanner.style.display = "none";
572
+
573
+ var method = document.getElementById("method").value;
574
+ var path = document.getElementById("path").value;
575
+ var bodyText = document.getElementById("body").value;
576
+ var sendBtn = document.getElementById("send");
577
+ var headers = getHeaders();
578
+
579
+ sendBtn.disabled = true;
580
+ sendBtn.textContent = "Sending\\u2026";
581
+
582
+ var start = performance.now();
583
+ var init = { method: method, headers: headers, redirect: "manual" };
584
+ if (bodyText && method !== "GET" && method !== "HEAD") init.body = bodyText;
585
+
586
+ fetch(path, init).then(function(res) {
587
+ // Opaque redirect \u2014 fall back to proxy for full 3xx details
588
+ if (res.type === "opaqueredirect") return null;
589
+
590
+ var status = res.status;
591
+ var statusText = res.statusText;
592
+ var resHeaders = {};
593
+ res.headers.forEach(function(v, k) { resHeaders[k] = v; });
594
+
595
+ return res.text().then(function(bodyStr) {
596
+ return { status: status, statusText: statusText, headers: resHeaders, body: bodyStr, elapsed: performance.now() - start };
597
+ });
598
+ }).catch(function() {
599
+ return null;
600
+ }).then(function(data) {
601
+ if (data) {
602
+ renderResponse(data);
603
+ detectAndSaveTokens(data);
604
+ return;
605
+ }
606
+ return sendViaProxy(method, path, headers, bodyText);
607
+ }).catch(function(err) {
608
+ renderError(err.message || "Request failed");
609
+ }).finally(function() {
610
+ sendBtn.disabled = false;
611
+ sendBtn.textContent = "Send";
612
+ });
613
+ });
614
+
615
+ // \u2500\u2500 Render helpers \u2500\u2500
616
+
617
+ function escHtml(s) {
618
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
619
+ }
620
+
621
+ function highlightJson(prettyStr) {
622
+ var s = escHtml(prettyStr);
623
+ // Keys
624
+ s = s.replace(/"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"s*:/g, '<span class="j-key">"$1"</span>:');
625
+ // String values (after : or in arrays after , or [)
626
+ s = s.replace(/:\\s*"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"/g, ': <span class="j-str">"$1"</span>');
627
+ // Numbers
628
+ s = s.replace(/:\\s*(-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)/g, ': <span class="j-num">$1</span>');
629
+ // Booleans and null
630
+ s = s.replace(/:\\s*(true|false|null)\\b/g, ': <span class="j-lit">$1</span>');
631
+ return s;
632
+ }
633
+
634
+ function renderResponse(ref) {
635
+ var status = ref.status, statusText = ref.statusText, headers = ref.headers, body = ref.body, elapsed = ref.elapsed;
636
+ var cls = status < 300 ? "b-2xx" : status < 400 ? "b-3xx" : status < 500 ? "b-4xx" : "b-5xx";
637
+
638
+ var rawBody = body || "";
639
+ var prettyBody = rawBody;
640
+ var isJson = false;
641
+ try {
642
+ prettyBody = JSON.stringify(JSON.parse(rawBody), null, 2);
643
+ isJson = true;
644
+ } catch (e) {}
645
+
646
+ var headerCount = 0;
647
+ var headerRows = "";
648
+ for (var k in headers) {
649
+ if (headers.hasOwnProperty(k)) {
650
+ headerCount++;
651
+ headerRows += "<tr><td>" + esc(k) + "</td><td>" + esc(String(headers[k])) + "</td></tr>";
652
+ }
653
+ }
654
+
655
+ // Redirect banner (OAuth flows)
656
+ var redirectBanner = "";
657
+ var locationUrl = headers && headers.location;
658
+ if (status >= 300 && status < 400 && locationUrl && !locationUrl.startsWith("/")) {
659
+ lastRedirectUrl = locationUrl;
660
+ redirectBanner =
661
+ '<div class="res-redirect">' +
662
+ '<div class="res-redirect-title">Redirect Detected</div>' +
663
+ '<div class="res-redirect-url">' + esc(locationUrl) + '</div>' +
664
+ '<button class="res-redirect-btn" onclick="openAuthPopup(lastRedirectUrl)">Open Authorization</button>' +
665
+ '<div class="res-redirect-notice">Opens in a popup. After authorizing, the callback parameters will be sent back here.</div>' +
666
+ '</div>';
667
+ }
668
+
669
+ var prettyContent = isJson
670
+ ? highlightJson(prettyBody)
671
+ : escHtml(prettyBody);
672
+
673
+ var resEl = document.getElementById("response");
674
+ resEl.innerHTML =
675
+ '<div class="res-top">' +
676
+ '<span class="badge ' + cls + '">' + status + " " + esc(statusText || "") + '</span>' +
677
+ '<span class="timing">' + (elapsed || 0).toFixed(1) + ' ms</span>' +
678
+ '</div>' +
679
+ redirectBanner +
680
+ '<div class="res-tabs">' +
681
+ '<button class="res-tab active" data-tab="pretty">Pretty</button>' +
682
+ '<button class="res-tab" data-tab="raw">Raw</button>' +
683
+ '<button class="res-tab" data-tab="headers">Headers<span class="res-tab-count">(' + headerCount + ')</span></button>' +
684
+ '</div>' +
685
+ '<div class="res-tab-content active" data-tab="pretty"><div class="res-body"><pre>' + prettyContent + '</pre></div></div>' +
686
+ '<div class="res-tab-content" data-tab="raw"><div class="res-body"><pre>' + esc(rawBody) + '</pre></div></div>' +
687
+ '<div class="res-tab-content" data-tab="headers"><table class="htable">' + headerRows + '</table></div>';
688
+
689
+ // Tab switching
690
+ resEl.querySelectorAll(".res-tab").forEach(function(tab) {
691
+ tab.onclick = function() {
692
+ var target = tab.dataset.tab;
693
+ resEl.querySelectorAll(".res-tab").forEach(function(t) { t.classList.toggle("active", t.dataset.tab === target); });
694
+ resEl.querySelectorAll(".res-tab-content").forEach(function(c) { c.classList.toggle("active", c.dataset.tab === target); });
695
+ };
696
+ });
697
+ }
698
+
699
+ function renderError(msg) {
700
+ document.getElementById("response").innerHTML = '<div class="res-error">' + esc(msg) + '</div>';
701
+ }
702
+
703
+ function esc(s) {
704
+ var d = document.createElement("div");
705
+ d.textContent = s;
706
+ return d.innerHTML;
707
+ }
708
+
709
+ // \u2500\u2500 OAuth popup \u2500\u2500
710
+
711
+ var lastRedirectUrl = "";
712
+ var oauthPopup = null;
713
+ function openAuthPopup(url) {
714
+ if (oauthPopup && !oauthPopup.closed) oauthPopup.close();
715
+ oauthPopup = window.open(url, "stoma-oauth", "width=600,height=700");
716
+ }
717
+
718
+ window.addEventListener("message", function(event) {
719
+ if (event.origin !== window.location.origin) return;
720
+ if (!event.data || event.data.type !== "stoma-oauth-callback") return;
721
+ var params = event.data.params;
722
+ if (!params) return;
723
+
724
+ var callbackPath = "/auth/callback";
725
+ for (var i = 0; i < registry.routes.length; i++) {
726
+ if (registry.routes[i].path.toLowerCase().indexOf("callback") !== -1) {
727
+ callbackPath = registry.routes[i].path;
728
+ break;
729
+ }
730
+ }
731
+
732
+ var qs = new URLSearchParams(params).toString();
733
+ document.getElementById("method").value = "GET";
734
+ document.getElementById("path").value = callbackPath + "?" + qs;
735
+
736
+ var banner = document.getElementById("callback-banner");
737
+ banner.innerHTML = 'OAuth parameters received. Click <strong>Send</strong> to complete authorization.';
738
+ banner.style.display = "";
739
+
740
+ document.getElementById("response").innerHTML =
741
+ '<div class="res-empty">OAuth callback parameters prefilled. Click Send to exchange for a token.</div>';
742
+ });
743
+ </script>
744
+ </body>
745
+ </html>`;
746
+ }
747
+ function esc(s) {
748
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
749
+ }
750
+
751
+ // src/playground/oauth-relay.ts
752
+ function oauthRelayHtml(url) {
753
+ const paramsJson = JSON.stringify(
754
+ Object.fromEntries(url.searchParams.entries())
755
+ ).replace(/</g, "\\u003c");
756
+ return `<!DOCTYPE html>
757
+ <html lang="en">
758
+ <head><meta charset="utf-8" /><title>Authorization Complete</title></head>
759
+ <body style="font-family:system-ui;padding:2rem;text-align:center;background:#1e1e1e;color:#d4d4d4">
760
+ <h3>Authorization Complete</h3>
761
+ <p id="msg">Sending parameters to the playground\u2026</p>
762
+ <script>
763
+ (function() {
764
+ var params = ${paramsJson};
765
+ if (window.opener) {
766
+ window.opener.postMessage({ type: "stoma-oauth-callback", params: params }, location.origin);
767
+ document.getElementById("msg").textContent = "Parameters sent. You can close this window.";
768
+ } else {
769
+ document.getElementById("msg").textContent = "No opener window found. Please copy the URL and paste it into the playground manually.";
770
+ }
771
+ })();
772
+ </script>
773
+ </body>
774
+ </html>`;
775
+ }
776
+
777
+ // src/playground/wrap.ts
778
+ function wrapWithPlayground(gatewayFetch, registry) {
779
+ const html = playgroundHtml(registry);
780
+ const callbackPaths = registry.routes.map((r) => r.path).filter((p) => p.toLowerCase().includes("callback"));
781
+ return async (request) => {
782
+ const url = new URL(request.url);
783
+ if (url.pathname === "/__playground") {
784
+ return new Response(html, {
785
+ headers: { "content-type": "text/html; charset=utf-8" }
786
+ });
787
+ }
788
+ if (url.pathname === "/__playground/registry") {
789
+ return Response.json(registry);
790
+ }
791
+ if (url.pathname === "/__playground/send" && request.method === "POST") {
792
+ return handlePlaygroundSend(request, gatewayFetch, url.origin);
793
+ }
794
+ const isNavigation = request.headers.get("accept")?.includes("text/html");
795
+ if (isNavigation && url.search) {
796
+ const isCallbackRoute = callbackPaths.some(
797
+ (p) => url.pathname === p || url.pathname.startsWith(p.replace(/\*$/, ""))
798
+ );
799
+ if (isCallbackRoute) {
800
+ return new Response(oauthRelayHtml(url), {
801
+ headers: { "content-type": "text/html; charset=utf-8" }
802
+ });
803
+ }
804
+ }
805
+ return gatewayFetch(request);
806
+ };
807
+ }
808
+ async function handlePlaygroundSend(request, gatewayFetch, origin) {
809
+ try {
810
+ const payload = await request.json();
811
+ const targetUrl = new URL(payload.path, origin).href;
812
+ const init = {
813
+ method: payload.method,
814
+ headers: payload.headers ?? {}
815
+ };
816
+ if (payload.body && payload.method !== "GET" && payload.method !== "HEAD") {
817
+ init.body = payload.body;
818
+ }
819
+ const gatewayRequest = new Request(targetUrl, init);
820
+ const start = performance.now();
821
+ const res = await gatewayFetch(gatewayRequest);
822
+ const elapsed = performance.now() - start;
823
+ const body = await res.text();
824
+ const headers = {};
825
+ res.headers.forEach((v, k) => {
826
+ headers[k] = v;
827
+ });
828
+ return Response.json({
829
+ status: res.status,
830
+ statusText: res.statusText,
831
+ headers,
832
+ body,
833
+ elapsed
834
+ });
835
+ } catch (err) {
836
+ return Response.json(
837
+ {
838
+ error: err instanceof Error ? err.message : String(err)
839
+ },
840
+ { status: 500 }
841
+ );
842
+ }
843
+ }
844
+
845
+ // src/server/serve.ts
846
+ import { serve } from "@hono/node-server";
847
+ function startServer(options) {
848
+ const { fetch: fetch2, port, hostname } = options;
849
+ return new Promise((resolve2, reject) => {
850
+ const server = serve(
851
+ { fetch: fetch2, port, hostname },
852
+ () => resolve2(server)
853
+ );
854
+ server.on?.(
855
+ "error",
856
+ (err) => {
857
+ if (err.code === "EADDRINUSE") {
858
+ reject(
859
+ new Error(
860
+ `Port ${port} is already in use. Try a different port with --port <number>`
861
+ )
862
+ );
863
+ } else {
864
+ reject(err);
865
+ }
866
+ }
867
+ );
868
+ });
869
+ }
870
+
871
+ // src/utils/logger.ts
872
+ function createLogger(verbose) {
873
+ return {
874
+ info: (message) => console.log(message),
875
+ verbose: verbose ? (message) => console.log(message) : () => {
876
+ },
877
+ error: (message) => console.error(message)
878
+ };
879
+ }
880
+
881
+ // src/commands/run.ts
882
+ var RunCommand = class extends Command {
883
+ static paths = [["run"]];
884
+ static usage = Command.Usage({
885
+ description: "Start a local HTTP server for a Stoma gateway file",
886
+ details: `
887
+ Loads a compiled gateway JS file (or TypeScript with tsx) and serves it
888
+ on a local Node.js HTTP server.
889
+
890
+ The file must export a gateway instance, a factory function, or a
891
+ \`createPlaygroundGateway\` named export.
892
+ `,
893
+ examples: [
894
+ ["Run a compiled gateway file", "stoma run ./my-gateway.js"],
895
+ ["Run on a custom port", "stoma run ./my-gateway.js --port 3000"],
896
+ ["Run with debug logging", "stoma run ./my-gateway.js --debug"],
897
+ [
898
+ "Run a remote gateway file",
899
+ "stoma run https://example.com/gateway.ts --trust-remote"
900
+ ]
901
+ ]
902
+ });
903
+ file = Option.String({ required: true });
904
+ port = Option.String("--port,-p", "8787", {
905
+ description: "Port to listen on (default: 8787)"
906
+ });
907
+ host = Option.String("--host,-H", "localhost", {
908
+ description: "Hostname to bind to (default: localhost)"
909
+ });
910
+ debug = Option.Boolean("--debug,-d", false, {
911
+ description: "Enable gateway debug logging"
912
+ });
913
+ verbose = Option.Boolean("--verbose,-v", false, {
914
+ description: "Verbose CLI output"
915
+ });
916
+ playground = Option.Boolean("--playground", false, {
917
+ description: "Serve interactive playground UI at /__playground"
918
+ });
919
+ trustRemote = Option.Boolean("--trust-remote", false, {
920
+ description: "Allow loading gateway files from remote URLs (downloads and executes code \u2014 use only with trusted sources)"
921
+ });
922
+ async execute() {
923
+ const log = createLogger(this.verbose);
924
+ const isRemote = this.file.startsWith("http://") || this.file.startsWith("https://");
925
+ const filePath = isRemote ? this.file : path2.resolve(this.file);
926
+ const portNum = parseInt(this.port, 10);
927
+ if (Number.isNaN(portNum) || portNum < 0 || portNum > 65535) {
928
+ log.error(`Invalid port: ${this.port}`);
929
+ return 1;
930
+ }
931
+ log.info(`Loading gateway from ${filePath}`);
932
+ let gateway;
933
+ try {
934
+ gateway = await resolveGateway(filePath, {
935
+ debug: this.debug,
936
+ trustRemote: this.trustRemote
937
+ });
938
+ } catch (err) {
939
+ log.error(
940
+ `Failed to load gateway: ${err instanceof Error ? err.message : String(err)}`
941
+ );
942
+ return 1;
943
+ }
944
+ log.info(
945
+ `Gateway "${gateway.name}" loaded (${gateway.routeCount} route${gateway.routeCount === 1 ? "" : "s"})`
946
+ );
947
+ if (this.verbose && gateway._registry) {
948
+ for (const route of gateway._registry.routes) {
949
+ const methods = Array.isArray(route.methods) ? route.methods.join(",") : "ALL";
950
+ log.verbose(` ${methods.padEnd(8)} ${route.path}`);
951
+ }
952
+ }
953
+ try {
954
+ const fetch2 = this.playground && gateway._registry ? wrapWithPlayground(gateway.app.fetch, gateway._registry) : gateway.app.fetch;
955
+ const server = await startServer({
956
+ fetch: fetch2,
957
+ port: portNum,
958
+ hostname: this.host
959
+ });
960
+ log.info(
961
+ `Gateway "${gateway.name}" listening on http://${this.host}:${portNum}`
962
+ );
963
+ if (this.playground) {
964
+ log.info(`Playground: http://${this.host}:${portNum}/__playground`);
965
+ }
966
+ if (gateway._registry) {
967
+ for (const route of gateway._registry.routes) {
968
+ const methods = Array.isArray(route.methods) ? route.methods.join(",") : "ALL";
969
+ log.info(` ${methods.padEnd(8)} ${route.path}`);
970
+ }
971
+ }
972
+ await new Promise((resolve2) => {
973
+ let shuttingDown = false;
974
+ const shutdown = () => {
975
+ if (shuttingDown) {
976
+ log.info("\nForce exit");
977
+ process.exit(1);
978
+ }
979
+ shuttingDown = true;
980
+ log.info("\nShutting down...");
981
+ const forceExit = setTimeout(() => {
982
+ log.error("Graceful shutdown timed out, forcing exit");
983
+ process.exit(1);
984
+ }, 3e3);
985
+ forceExit.unref();
986
+ server.closeAllConnections();
987
+ server.close(() => {
988
+ clearTimeout(forceExit);
989
+ resolve2();
990
+ });
991
+ };
992
+ process.on("SIGINT", shutdown);
993
+ process.on("SIGTERM", shutdown);
994
+ });
995
+ return process.exit(0);
996
+ } catch (err) {
997
+ log.error(
998
+ `Server error: ${err instanceof Error ? err.message : String(err)}`
999
+ );
1000
+ return 1;
1001
+ }
1002
+ }
1003
+ };
1004
+
1005
+ // src/utils/version.ts
1006
+ import { existsSync as existsSync2, readFileSync } from "fs";
1007
+ import { dirname, resolve } from "path";
1008
+ import { fileURLToPath } from "url";
1009
+ var cachedVersion;
1010
+ function getVersion() {
1011
+ if (cachedVersion) return cachedVersion;
1012
+ try {
1013
+ const dir = dirname(fileURLToPath(import.meta.url));
1014
+ for (const rel of ["..", "../.."]) {
1015
+ const candidate = resolve(dir, rel, "package.json");
1016
+ if (existsSync2(candidate)) {
1017
+ const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
1018
+ if (pkg.name === "@vivero/stoma-cli") {
1019
+ cachedVersion = pkg.version ?? "0.0.0";
1020
+ return cachedVersion;
1021
+ }
1022
+ }
1023
+ }
1024
+ cachedVersion = "0.0.0";
1025
+ } catch {
1026
+ cachedVersion = "0.0.0";
1027
+ }
1028
+ return cachedVersion;
1029
+ }
1030
+
1031
+ // src/cli.ts
1032
+ function createCli() {
1033
+ const cli = new Cli({
1034
+ binaryLabel: "stoma",
1035
+ binaryName: "stoma",
1036
+ binaryVersion: getVersion()
1037
+ });
1038
+ cli.register(Builtins.HelpCommand);
1039
+ cli.register(Builtins.VersionCommand);
1040
+ cli.register(RunCommand);
1041
+ return cli;
1042
+ }
1043
+ async function runCli(argv) {
1044
+ const cli = createCli();
1045
+ await cli.runExit(argv);
1046
+ }
1047
+
1048
+ // src/bin.ts
1049
+ runCli(process.argv.slice(2));
1050
+ //# sourceMappingURL=bin.js.map