railcode 0.1.1 → 0.1.5
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 +10 -8
- package/dist/index.js +493 -97
- package/package.json +1 -1
- package/railcode-templates/railcode-react/dist/assets/index-6MYvol4W.css +2 -0
- package/railcode-templates/railcode-react/dist/assets/index-CfaOTQhI.js +9 -0
- package/railcode-templates/railcode-react/dist/index.html +13 -0
- package/railcode-templates/railcode-react/package-lock.json +18 -0
- package/railcode-templates/railcode-react/package.json +1 -0
- package/railcode-templates/railcode-react/src/lib/railcode.ts +2 -0
- package/static/sdk.js +10 -1
- package/railcode-templates/railcode-react/node_modules/.bin/tsc +0 -2
- package/railcode-templates/railcode-react/node_modules/.bin/vite +0 -79
package/dist/index.js
CHANGED
|
@@ -8,14 +8,15 @@ import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
|
8
8
|
import process from "node:process";
|
|
9
9
|
import { createInterface } from "node:readline/promises";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
const VERSION = "0.1.1";
|
|
12
11
|
const CLI_PACKAGE_NAME = "railcode";
|
|
13
12
|
const SKILL_NAME = "create-railcode-app";
|
|
14
13
|
const RAILCODE_HOME = process.env.RAILCODE_HOME || join(homedir(), ".railcode");
|
|
15
14
|
const CONFIG_PATH = join(RAILCODE_HOME, "config.json");
|
|
16
15
|
const CLI_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
-
const
|
|
18
|
-
const
|
|
16
|
+
const VERSION = readCliPackageVersion();
|
|
17
|
+
const DEFAULT_DEV_PORT = 7331;
|
|
18
|
+
const DEFAULT_ASSET_DEV_PORT = 5173;
|
|
19
|
+
const PORT_SEARCH_LIMIT = 100;
|
|
19
20
|
const CLI_AUTH_CALLBACK_PATH = "/railcode-cli/callback";
|
|
20
21
|
const CLI_AUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
21
22
|
const HELP = `railcode ${VERSION}
|
|
@@ -23,8 +24,9 @@ const HELP = `railcode ${VERSION}
|
|
|
23
24
|
Usage:
|
|
24
25
|
railcode dev [--verbose]
|
|
25
26
|
railcode login [--api-url <url>]
|
|
26
|
-
railcode deploy
|
|
27
|
-
railcode
|
|
27
|
+
railcode deploy [--private | --public]
|
|
28
|
+
railcode access [public|private|restricted] [--users <a,b>]
|
|
29
|
+
railcode get design-system
|
|
28
30
|
railcode init <app>
|
|
29
31
|
railcode upgrade
|
|
30
32
|
railcode skill stamp [path]
|
|
@@ -54,6 +56,12 @@ async function main() {
|
|
|
54
56
|
case "deploy":
|
|
55
57
|
await commandDeploy(args);
|
|
56
58
|
return;
|
|
59
|
+
case "access":
|
|
60
|
+
await commandAccess(args);
|
|
61
|
+
return;
|
|
62
|
+
case "get":
|
|
63
|
+
await commandGet(args);
|
|
64
|
+
return;
|
|
57
65
|
case "design-system":
|
|
58
66
|
await commandDesignSystem(args);
|
|
59
67
|
return;
|
|
@@ -110,6 +118,14 @@ function parseArgs(argv) {
|
|
|
110
118
|
}
|
|
111
119
|
return { command, rest, options };
|
|
112
120
|
}
|
|
121
|
+
function readCliPackageVersion() {
|
|
122
|
+
const packagePath = join(CLI_PACKAGE_ROOT, "package.json");
|
|
123
|
+
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
124
|
+
if (typeof pkg.version !== "string" || !pkg.version) {
|
|
125
|
+
throw new Error(`Missing CLI package version in ${packagePath}`);
|
|
126
|
+
}
|
|
127
|
+
return pkg.version;
|
|
128
|
+
}
|
|
113
129
|
function toCamel(input) {
|
|
114
130
|
return input.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
115
131
|
}
|
|
@@ -125,17 +141,9 @@ async function commandDevProxy(args) {
|
|
|
125
141
|
rmSync(localDevDir(ctx.app), { recursive: true, force: true });
|
|
126
142
|
}
|
|
127
143
|
mkdirSync(localDevDir(ctx.app), { recursive: true });
|
|
128
|
-
if (ctx.
|
|
144
|
+
if (ctx.asset.kind !== "none") {
|
|
129
145
|
await ensureAppDependencies(ctx.root);
|
|
130
146
|
}
|
|
131
|
-
const assetChild = ctx.assetCommand
|
|
132
|
-
? spawn(ctx.assetCommand, {
|
|
133
|
-
cwd: ctx.root,
|
|
134
|
-
env: process.env,
|
|
135
|
-
shell: true,
|
|
136
|
-
stdio: "inherit",
|
|
137
|
-
})
|
|
138
|
-
: null;
|
|
139
147
|
const server = createServer((request, response) => {
|
|
140
148
|
handleDevRequest(ctx, request, response).catch((error) => {
|
|
141
149
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -148,11 +156,26 @@ async function commandDevProxy(args) {
|
|
|
148
156
|
server.on("upgrade", (request, socket, head) => {
|
|
149
157
|
proxyWebSocket(ctx, request, socket, head);
|
|
150
158
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
let assetProcess = null;
|
|
160
|
+
try {
|
|
161
|
+
ctx.port = await listenOnAvailablePort(server, ctx.portStart, "Railcode dev");
|
|
162
|
+
assetProcess = await startAssetDevServer(ctx);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (server.listening)
|
|
166
|
+
await closeServer(server);
|
|
167
|
+
assetProcess?.reservation?.release();
|
|
168
|
+
if (assetProcess?.child && !assetProcess.child.killed)
|
|
169
|
+
assetProcess.child.kill("SIGTERM");
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
155
172
|
const localUrl = `http://127.0.0.1:${ctx.port}`;
|
|
173
|
+
if (ctx.port !== ctx.portStart) {
|
|
174
|
+
console.log(`Port ${ctx.portStart} was busy; using ${ctx.port}.`);
|
|
175
|
+
}
|
|
176
|
+
if (ctx.assetPort && ctx.assetPortStart && ctx.assetPort !== ctx.assetPortStart) {
|
|
177
|
+
console.log(`Asset port ${ctx.assetPortStart} was busy; using ${ctx.assetPort}.`);
|
|
178
|
+
}
|
|
156
179
|
printUrlBox(ctx.app, localUrl);
|
|
157
180
|
console.log("");
|
|
158
181
|
// Identity, KV, and files are served locally; LLM and SQL forward to the real
|
|
@@ -173,8 +196,11 @@ async function commandDevProxy(args) {
|
|
|
173
196
|
console.log(`App root: ${ctx.root}`);
|
|
174
197
|
console.log(`KV/files: ${localDevDir(ctx.app)}`);
|
|
175
198
|
console.log(`Prod APIs: ${ctx.apiBase ? `${ctx.apiBase}/v1` : "unconfigured"}`);
|
|
199
|
+
console.log(`Dev port: ${ctx.port}`);
|
|
176
200
|
if (ctx.assetCommand)
|
|
177
201
|
console.log(`App command: ${ctx.assetCommand}`);
|
|
202
|
+
if (ctx.assetTarget)
|
|
203
|
+
console.log(`Asset target: ${ctx.assetTarget}`);
|
|
178
204
|
}
|
|
179
205
|
console.log("");
|
|
180
206
|
await new Promise((resolveRun) => {
|
|
@@ -186,13 +212,20 @@ async function commandDevProxy(args) {
|
|
|
186
212
|
if (code !== 0)
|
|
187
213
|
process.exitCode = code;
|
|
188
214
|
server.close();
|
|
189
|
-
|
|
190
|
-
|
|
215
|
+
assetProcess?.reservation?.release();
|
|
216
|
+
if (assetProcess?.child && !assetProcess.child.killed)
|
|
217
|
+
assetProcess.child.kill("SIGTERM");
|
|
191
218
|
resolveRun();
|
|
192
219
|
};
|
|
193
220
|
process.once("SIGINT", () => shutdown());
|
|
194
221
|
process.once("SIGTERM", () => shutdown());
|
|
195
|
-
|
|
222
|
+
assetProcess?.child.once("error", (error) => {
|
|
223
|
+
console.error(`App command failed to start: ${error.message}`);
|
|
224
|
+
shutdown(1);
|
|
225
|
+
});
|
|
226
|
+
assetProcess?.child.once("exit", (code) => {
|
|
227
|
+
if (closed)
|
|
228
|
+
return;
|
|
196
229
|
if (code && code !== 0)
|
|
197
230
|
console.error(`App command exited with status ${code}`);
|
|
198
231
|
shutdown(code || 0);
|
|
@@ -220,20 +253,21 @@ async function resolveDevContext(args) {
|
|
|
220
253
|
}
|
|
221
254
|
assertAppName(app);
|
|
222
255
|
const root = resolveAppRoot(cwd, app, manifestInfo);
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
256
|
+
const portStart = parsePort(stringOption(args, "port") ?? DEFAULT_DEV_PORT, "--port");
|
|
257
|
+
const assetPortStart = parsePort(stringOption(args, "assetPort") ?? manifest?.dev?.port ?? DEFAULT_ASSET_DEV_PORT, "--asset-port");
|
|
258
|
+
const explicitAssetCommand = stringOption(args, "command") || manifest?.dev?.command;
|
|
259
|
+
const asset = resolveAssetDevConfig(root, explicitAssetCommand, assetPortStart);
|
|
227
260
|
const config = loadConfig();
|
|
228
261
|
const apiBase = apiOrigin(config, args);
|
|
229
262
|
return {
|
|
230
263
|
app,
|
|
231
264
|
root,
|
|
232
|
-
port,
|
|
265
|
+
port: portStart,
|
|
266
|
+
portStart,
|
|
233
267
|
apiBase,
|
|
234
268
|
token: process.env.RAILCODE_API_TOKEN || config.apiToken,
|
|
235
|
-
|
|
236
|
-
|
|
269
|
+
asset,
|
|
270
|
+
assetPortStart: asset.kind === "none" ? undefined : assetPortStart,
|
|
237
271
|
};
|
|
238
272
|
}
|
|
239
273
|
function readRailcodeManifest(start) {
|
|
@@ -328,18 +362,51 @@ function resolveSdkPath() {
|
|
|
328
362
|
return sourceTree;
|
|
329
363
|
throw new CliError(`Railcode SDK file not found. Expected ${bundled}`, 1);
|
|
330
364
|
}
|
|
331
|
-
function
|
|
365
|
+
function resolveAssetDevConfig(root, explicitCommand, targetPort) {
|
|
366
|
+
if (explicitCommand)
|
|
367
|
+
return { kind: "custom", command: explicitCommand, targetPort };
|
|
332
368
|
const packagePath = join(root, "package.json");
|
|
333
369
|
if (!existsSync(packagePath))
|
|
334
|
-
return
|
|
370
|
+
return { kind: "none" };
|
|
335
371
|
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
336
372
|
if (!pkg.scripts?.dev)
|
|
337
|
-
return
|
|
373
|
+
return { kind: "none" };
|
|
338
374
|
const pm = packageManagerFor(root);
|
|
339
|
-
if (
|
|
340
|
-
return
|
|
375
|
+
if (isViteProject(root))
|
|
376
|
+
return { kind: "vite", packageManager: pm, startPort: targetPort };
|
|
377
|
+
return { kind: "script", command: runScriptCommand(pm, "dev"), targetPort };
|
|
378
|
+
}
|
|
379
|
+
function isViteProject(root) {
|
|
380
|
+
return existsSync(join(root, "vite.config.ts")) || existsSync(join(root, "vite.config.js"));
|
|
381
|
+
}
|
|
382
|
+
async function startAssetDevServer(ctx) {
|
|
383
|
+
if (ctx.asset.kind === "none")
|
|
384
|
+
return null;
|
|
385
|
+
let reservation;
|
|
386
|
+
if (ctx.asset.kind === "vite") {
|
|
387
|
+
reservation = await reserveAvailablePort(ctx.asset.startPort, "asset dev server");
|
|
388
|
+
ctx.assetPort = reservation.port;
|
|
389
|
+
ctx.assetCommand = runScriptCommand(ctx.asset.packageManager, "dev", `--host 127.0.0.1 --port ${reservation.port} --strictPort --logLevel warn`);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
ctx.assetPort = ctx.asset.targetPort;
|
|
393
|
+
ctx.assetCommand = ctx.asset.command;
|
|
341
394
|
}
|
|
342
|
-
|
|
395
|
+
ctx.assetTarget = `http://127.0.0.1:${ctx.assetPort}`;
|
|
396
|
+
return {
|
|
397
|
+
child: spawn(ctx.assetCommand, {
|
|
398
|
+
cwd: ctx.root,
|
|
399
|
+
env: {
|
|
400
|
+
...process.env,
|
|
401
|
+
PORT: String(ctx.assetPort),
|
|
402
|
+
RAILCODE_DEV_PORT: String(ctx.port),
|
|
403
|
+
RAILCODE_ASSET_PORT: String(ctx.assetPort),
|
|
404
|
+
},
|
|
405
|
+
shell: true,
|
|
406
|
+
stdio: "inherit",
|
|
407
|
+
}),
|
|
408
|
+
reservation,
|
|
409
|
+
};
|
|
343
410
|
}
|
|
344
411
|
async function ensureAppDependencies(root) {
|
|
345
412
|
if (!existsSync(join(root, "package.json")))
|
|
@@ -355,7 +422,7 @@ function dependenciesReady(root) {
|
|
|
355
422
|
const nodeModules = join(root, "node_modules");
|
|
356
423
|
if (!existsSync(nodeModules) || !statSync(nodeModules).isDirectory())
|
|
357
424
|
return false;
|
|
358
|
-
if (
|
|
425
|
+
if (isViteProject(root)) {
|
|
359
426
|
return existsSync(join(nodeModules, ".bin", process.platform === "win32" ? "vite.cmd" : "vite"));
|
|
360
427
|
}
|
|
361
428
|
return true;
|
|
@@ -380,6 +447,140 @@ function runScriptCommand(pm, script, passthrough = "") {
|
|
|
380
447
|
return `yarn ${script}${suffix}`;
|
|
381
448
|
return `${pm} run ${script}${passthrough ? ` -- ${passthrough}` : ""}`;
|
|
382
449
|
}
|
|
450
|
+
function parsePort(value, label) {
|
|
451
|
+
const port = typeof value === "number" ? value : Number(value);
|
|
452
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
453
|
+
throw new CliError(`${label} must be a TCP port from 1 to 65535.`, 2);
|
|
454
|
+
}
|
|
455
|
+
return port;
|
|
456
|
+
}
|
|
457
|
+
async function listenOnAvailablePort(server, startPort, label) {
|
|
458
|
+
for (let port = startPort, checked = 0; port <= 65535 && checked < PORT_SEARCH_LIMIT; port += 1, checked += 1) {
|
|
459
|
+
try {
|
|
460
|
+
await listenOnPort(server, port);
|
|
461
|
+
return port;
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
if (errorCode(error) !== "EADDRINUSE")
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
throw new CliError(`${label} could not find an available port starting at ${startPort}.`, 1);
|
|
469
|
+
}
|
|
470
|
+
async function reserveAvailablePort(startPort, label) {
|
|
471
|
+
for (let port = startPort, checked = 0; port <= 65535 && checked < PORT_SEARCH_LIMIT; port += 1, checked += 1) {
|
|
472
|
+
const reservation = reserveRailcodePort(port);
|
|
473
|
+
if (!reservation)
|
|
474
|
+
continue;
|
|
475
|
+
try {
|
|
476
|
+
const available = await canListenOnPort(port);
|
|
477
|
+
if (available)
|
|
478
|
+
return reservation;
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
reservation.release();
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
reservation.release();
|
|
485
|
+
}
|
|
486
|
+
throw new CliError(`${label} could not find an available port starting at ${startPort}.`, 1);
|
|
487
|
+
}
|
|
488
|
+
async function canListenOnPort(port) {
|
|
489
|
+
const server = createServer();
|
|
490
|
+
try {
|
|
491
|
+
await listenOnPort(server, port);
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
if (errorCode(error) === "EADDRINUSE")
|
|
496
|
+
return false;
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
finally {
|
|
500
|
+
if (server.listening)
|
|
501
|
+
await closeServer(server);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function listenOnPort(server, port) {
|
|
505
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
506
|
+
const cleanup = () => {
|
|
507
|
+
server.off("error", onError);
|
|
508
|
+
server.off("listening", onListening);
|
|
509
|
+
};
|
|
510
|
+
const onError = (error) => {
|
|
511
|
+
cleanup();
|
|
512
|
+
rejectListen(error);
|
|
513
|
+
};
|
|
514
|
+
const onListening = () => {
|
|
515
|
+
cleanup();
|
|
516
|
+
resolveListen();
|
|
517
|
+
};
|
|
518
|
+
server.once("error", onError);
|
|
519
|
+
server.once("listening", onListening);
|
|
520
|
+
server.listen(port, "127.0.0.1");
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
function reserveRailcodePort(port) {
|
|
524
|
+
const dir = localPortLockDir(port);
|
|
525
|
+
mkdirSync(dirname(dir), { recursive: true });
|
|
526
|
+
try {
|
|
527
|
+
mkdirSync(dir);
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
if (errorCode(error) !== "EEXIST")
|
|
531
|
+
throw error;
|
|
532
|
+
if (!removeStalePortLock(dir))
|
|
533
|
+
return null;
|
|
534
|
+
try {
|
|
535
|
+
mkdirSync(dir);
|
|
536
|
+
}
|
|
537
|
+
catch (retryError) {
|
|
538
|
+
if (errorCode(retryError) === "EEXIST")
|
|
539
|
+
return null;
|
|
540
|
+
throw retryError;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
writeJsonFile(join(dir, "owner.json"), { pid: process.pid, started_at: nowIso() });
|
|
544
|
+
let released = false;
|
|
545
|
+
return {
|
|
546
|
+
port,
|
|
547
|
+
release: () => {
|
|
548
|
+
if (released)
|
|
549
|
+
return;
|
|
550
|
+
released = true;
|
|
551
|
+
const owner = readJsonFile(join(dir, "owner.json"), {});
|
|
552
|
+
if (owner.pid === process.pid)
|
|
553
|
+
rmSync(dir, { recursive: true, force: true });
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function removeStalePortLock(dir) {
|
|
558
|
+
const owner = readJsonFile(join(dir, "owner.json"), {});
|
|
559
|
+
if (typeof owner.pid === "number" && processIsRunning(owner.pid))
|
|
560
|
+
return false;
|
|
561
|
+
rmSync(dir, { recursive: true, force: true });
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
function localPortLockDir(port) {
|
|
565
|
+
return join(RAILCODE_HOME, "dev", ".ports", String(port));
|
|
566
|
+
}
|
|
567
|
+
function processIsRunning(pid) {
|
|
568
|
+
try {
|
|
569
|
+
process.kill(pid, 0);
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
return errorCode(error) === "EPERM";
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function errorCode(error) {
|
|
577
|
+
return typeof error === "object" &&
|
|
578
|
+
error !== null &&
|
|
579
|
+
"code" in error &&
|
|
580
|
+
typeof error.code === "string"
|
|
581
|
+
? error.code
|
|
582
|
+
: undefined;
|
|
583
|
+
}
|
|
383
584
|
async function handleDevRequest(ctx, request, response) {
|
|
384
585
|
const url = new URL(request.url || "/", "http://127.0.0.1");
|
|
385
586
|
if (url.pathname.startsWith("/_api/")) {
|
|
@@ -400,7 +601,11 @@ async function handleLocalApi(ctx, request, response, url) {
|
|
|
400
601
|
return;
|
|
401
602
|
}
|
|
402
603
|
if (path === "/me") {
|
|
403
|
-
sendJson(response, 200, {
|
|
604
|
+
sendJson(response, 200, {
|
|
605
|
+
user: "local-dev",
|
|
606
|
+
display_name: "Local Dev",
|
|
607
|
+
app: ctx.app,
|
|
608
|
+
});
|
|
404
609
|
return;
|
|
405
610
|
}
|
|
406
611
|
if (path === "/app-users") {
|
|
@@ -1000,13 +1205,22 @@ function escapeHtml(value) {
|
|
|
1000
1205
|
.replaceAll('"', """)
|
|
1001
1206
|
.replaceAll("'", "'");
|
|
1002
1207
|
}
|
|
1208
|
+
const DEPLOY_HELP = `Usage:
|
|
1209
|
+
railcode deploy [--private | --public]
|
|
1210
|
+
|
|
1211
|
+
Run from a Railcode app repo. The first deploy creates the app's access policy:
|
|
1212
|
+
public (anyone signed in) by default, or owner-only with --private. You can also
|
|
1213
|
+
set it declaratively in railcode.json: { "deploy": { "access": "private" } }.
|
|
1214
|
+
Redeploys keep the existing policy; change it later with railcode access.
|
|
1215
|
+
`;
|
|
1003
1216
|
async function commandDeploy(args) {
|
|
1004
1217
|
if (booleanOption(args, "help")) {
|
|
1005
|
-
console.log(
|
|
1218
|
+
console.log(DEPLOY_HELP);
|
|
1006
1219
|
return;
|
|
1007
1220
|
}
|
|
1008
1221
|
rejectDeployArgs(args);
|
|
1009
1222
|
const ctx = resolveDeployContext();
|
|
1223
|
+
const access = resolveDeployAccess(args, ctx);
|
|
1010
1224
|
await buildApp(ctx.appRoot);
|
|
1011
1225
|
const source = resolveDeploySource(ctx);
|
|
1012
1226
|
const files = collectDeployFiles(source);
|
|
@@ -1015,7 +1229,7 @@ async function commandDeploy(args) {
|
|
|
1015
1229
|
const apiUrl = canonicalApiOrigin(authUrl);
|
|
1016
1230
|
let authedConfig = await ensureApiToken({ ...config, apiUrl: authUrl }, authUrl, "Deploy");
|
|
1017
1231
|
console.log(`Uploading ${ctx.app} to ${apiUrl}...`);
|
|
1018
|
-
const response = await postDeployBundle(apiUrl, authedConfig, ctx.app, files);
|
|
1232
|
+
const response = await postDeployBundle(apiUrl, authedConfig, ctx.app, files, access);
|
|
1019
1233
|
if (response.status === 401 && !process.env.RAILCODE_API_TOKEN && process.stdin.isTTY) {
|
|
1020
1234
|
console.log("Saved login expired. Log in again to continue.");
|
|
1021
1235
|
const retryConfig = { ...authedConfig, apiUrl: authUrl };
|
|
@@ -1024,19 +1238,46 @@ async function commandDeploy(args) {
|
|
|
1024
1238
|
delete retryConfig.cookies;
|
|
1025
1239
|
saveConfig(retryConfig);
|
|
1026
1240
|
const freshConfig = await ensureApiToken(retryConfig, authUrl, "Deploy");
|
|
1027
|
-
|
|
1241
|
+
const retry = await postDeployBundle(apiUrl, freshConfig, ctx.app, files, access);
|
|
1242
|
+
await handleDeployResponse(retry, ctx.app, apiUrl, access);
|
|
1028
1243
|
return;
|
|
1029
1244
|
}
|
|
1030
|
-
await handleDeployResponse(response, ctx.app, apiUrl);
|
|
1245
|
+
await handleDeployResponse(response, ctx.app, apiUrl, access);
|
|
1031
1246
|
}
|
|
1032
1247
|
function rejectDeployArgs(args) {
|
|
1033
1248
|
if (args.rest.length > 0) {
|
|
1034
1249
|
throw new CliError("railcode deploy does not take an app argument. Run it from the Railcode app directory.", 2);
|
|
1035
1250
|
}
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1251
|
+
const allowed = new Set(["private", "public", "help"]);
|
|
1252
|
+
const unknown = Object.keys(args.options).filter((option) => !allowed.has(option));
|
|
1253
|
+
if (unknown.length > 0) {
|
|
1254
|
+
throw new CliError(`Unknown option for deploy: --${unknown[0]}. Configure the Railcode URL in railcode.json or RAILCODE_API_URL.`, 2);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
// First-deploy access: --private/--public win over railcode.json deploy.access.
|
|
1258
|
+
// Returns the canonical policy mode, or undefined to let the server default.
|
|
1259
|
+
function resolveDeployAccess(args, ctx) {
|
|
1260
|
+
const wantsPrivate = booleanOption(args, "private");
|
|
1261
|
+
const wantsPublic = booleanOption(args, "public");
|
|
1262
|
+
if (wantsPrivate && wantsPublic) {
|
|
1263
|
+
throw new CliError("Use either --private or --public, not both.", 2);
|
|
1264
|
+
}
|
|
1265
|
+
if (wantsPrivate)
|
|
1266
|
+
return "private";
|
|
1267
|
+
if (wantsPublic)
|
|
1268
|
+
return "workspace";
|
|
1269
|
+
return ctx.access ? normalizeDeployAccess(ctx.access) : undefined;
|
|
1270
|
+
}
|
|
1271
|
+
function normalizeDeployAccess(value) {
|
|
1272
|
+
const mode = value.trim().toLowerCase();
|
|
1273
|
+
if (mode === "public" || mode === "workspace")
|
|
1274
|
+
return "workspace";
|
|
1275
|
+
if (mode === "private")
|
|
1276
|
+
return "private";
|
|
1277
|
+
if (mode === "restricted") {
|
|
1278
|
+
throw new CliError("deploy.access 'restricted' is not supported at deploy. Deploy, then run railcode access restricted --users <...>.", 2);
|
|
1279
|
+
}
|
|
1280
|
+
throw new CliError(`Invalid deploy.access '${value}'. Use 'public' or 'private'.`, 2);
|
|
1040
1281
|
}
|
|
1041
1282
|
function resolveDeployContext() {
|
|
1042
1283
|
const cwd = process.cwd();
|
|
@@ -1054,6 +1295,7 @@ function resolveDeployContext() {
|
|
|
1054
1295
|
appRoot,
|
|
1055
1296
|
workspaceRoot,
|
|
1056
1297
|
apiUrl: manifest?.deploy?.apiUrl,
|
|
1298
|
+
access: manifest?.deploy?.access,
|
|
1057
1299
|
};
|
|
1058
1300
|
}
|
|
1059
1301
|
async function buildApp(appRoot) {
|
|
@@ -1145,7 +1387,7 @@ function collectDeployFiles(root) {
|
|
|
1145
1387
|
}
|
|
1146
1388
|
return files;
|
|
1147
1389
|
}
|
|
1148
|
-
async function postDeployBundle(apiUrl, config, app, files) {
|
|
1390
|
+
async function postDeployBundle(apiUrl, config, app, files, access) {
|
|
1149
1391
|
try {
|
|
1150
1392
|
return await fetch(`${apiUrl}/v1/apps/${app}/deploy`, {
|
|
1151
1393
|
method: "POST",
|
|
@@ -1153,7 +1395,7 @@ async function postDeployBundle(apiUrl, config, app, files) {
|
|
|
1153
1395
|
Authorization: `Bearer ${config.apiToken}`,
|
|
1154
1396
|
"Content-Type": "application/json",
|
|
1155
1397
|
},
|
|
1156
|
-
body: JSON.stringify({ files }),
|
|
1398
|
+
body: JSON.stringify(access ? { files, access } : { files }),
|
|
1157
1399
|
redirect: "manual",
|
|
1158
1400
|
});
|
|
1159
1401
|
}
|
|
@@ -1161,14 +1403,19 @@ async function postDeployBundle(apiUrl, config, app, files) {
|
|
|
1161
1403
|
throw new CliError(`Could not reach ${apiUrl}. Configure the Railcode URL in railcode.json deploy.apiUrl or RAILCODE_API_URL. ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
1162
1404
|
}
|
|
1163
1405
|
}
|
|
1164
|
-
async function handleDeployResponse(response, app, apiUrl) {
|
|
1406
|
+
async function handleDeployResponse(response, app, apiUrl, requestedAccess) {
|
|
1165
1407
|
if (response.ok) {
|
|
1166
1408
|
const body = await response.json();
|
|
1167
1409
|
console.log(`Deployed ${app}${body.files ? ` (${body.files} files)` : ""}`);
|
|
1168
1410
|
console.log(body.url || appUrlFromOrigin(app, apiUrl));
|
|
1169
1411
|
const accessLabel = deployAccessLabel(body.access);
|
|
1170
|
-
if (accessLabel)
|
|
1412
|
+
if (accessLabel) {
|
|
1171
1413
|
console.log(`Access: ${accessLabel}`);
|
|
1414
|
+
}
|
|
1415
|
+
else if (requestedAccess && body.access === "existing_policy") {
|
|
1416
|
+
const word = requestedAccess === "private" ? "private" : "public";
|
|
1417
|
+
console.log(`Access unchanged (${app} already exists). Run 'railcode access ${word}' to change it.`);
|
|
1418
|
+
}
|
|
1172
1419
|
return;
|
|
1173
1420
|
}
|
|
1174
1421
|
const text = await response.text();
|
|
@@ -1183,8 +1430,156 @@ async function handleDeployResponse(response, app, apiUrl) {
|
|
|
1183
1430
|
function deployAccessLabel(access) {
|
|
1184
1431
|
if (access === "workspace_created")
|
|
1185
1432
|
return "public to signed-in users";
|
|
1433
|
+
if (access === "private_created")
|
|
1434
|
+
return "private (owner only)";
|
|
1186
1435
|
return undefined;
|
|
1187
1436
|
}
|
|
1437
|
+
const ACCESS_HELP = `Usage:
|
|
1438
|
+
railcode access Show the app's current access
|
|
1439
|
+
railcode access public Anyone signed in
|
|
1440
|
+
railcode access private Just you (the owner)
|
|
1441
|
+
railcode access restricted --users a@b.com,c@d.com Named users only
|
|
1442
|
+
|
|
1443
|
+
Run from a Railcode app repo. Deploy the app at least once before changing access.
|
|
1444
|
+
Only the app owner or an admin can change it. 'public' is an alias for 'workspace'.
|
|
1445
|
+
`;
|
|
1446
|
+
// Friendly CLI words mapped to the server's canonical policy modes.
|
|
1447
|
+
const ACCESS_MODES = {
|
|
1448
|
+
public: "workspace",
|
|
1449
|
+
workspace: "workspace",
|
|
1450
|
+
private: "private",
|
|
1451
|
+
restricted: "restricted",
|
|
1452
|
+
};
|
|
1453
|
+
async function commandAccess(args) {
|
|
1454
|
+
if (booleanOption(args, "help") || args.rest[0] === "help") {
|
|
1455
|
+
console.log(ACCESS_HELP);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const { method, payload } = parseAccessArgs(args);
|
|
1459
|
+
const ctx = resolveDeployContext();
|
|
1460
|
+
const config = loadConfig();
|
|
1461
|
+
const authUrl = await resolveDeployAuthOrigin(config, ctx);
|
|
1462
|
+
const apiUrl = canonicalApiOrigin(authUrl);
|
|
1463
|
+
let authedConfig = await ensureApiToken({ ...config, apiUrl: authUrl }, authUrl, "Access");
|
|
1464
|
+
let response = await sendAccessRequest(apiUrl, authedConfig, ctx.app, method, payload);
|
|
1465
|
+
if (response.status === 401 && !process.env.RAILCODE_API_TOKEN && process.stdin.isTTY) {
|
|
1466
|
+
console.log("Saved login expired. Log in again to continue.");
|
|
1467
|
+
const retryConfig = { ...authedConfig, apiUrl: authUrl };
|
|
1468
|
+
delete retryConfig.apiToken;
|
|
1469
|
+
delete retryConfig.apiTokenPrefix;
|
|
1470
|
+
delete retryConfig.cookies;
|
|
1471
|
+
saveConfig(retryConfig);
|
|
1472
|
+
authedConfig = await ensureApiToken(retryConfig, authUrl, "Access");
|
|
1473
|
+
response = await sendAccessRequest(apiUrl, authedConfig, ctx.app, method, payload);
|
|
1474
|
+
}
|
|
1475
|
+
await handleAccessResponse(response, ctx.app, method);
|
|
1476
|
+
}
|
|
1477
|
+
function parseAccessArgs(args) {
|
|
1478
|
+
const requested = args.rest[0];
|
|
1479
|
+
if (!requested) {
|
|
1480
|
+
rejectAccessOptions(args, false);
|
|
1481
|
+
return { method: "GET" };
|
|
1482
|
+
}
|
|
1483
|
+
if (args.rest.length > 1) {
|
|
1484
|
+
throw new CliError("railcode access takes a single mode argument.", 2);
|
|
1485
|
+
}
|
|
1486
|
+
const mode = ACCESS_MODES[requested.toLowerCase()];
|
|
1487
|
+
if (!mode) {
|
|
1488
|
+
throw new CliError(`Unknown access mode '${requested}'. Use public, private, or restricted.`, 2);
|
|
1489
|
+
}
|
|
1490
|
+
rejectAccessOptions(args, mode === "restricted");
|
|
1491
|
+
const payload = { mode };
|
|
1492
|
+
if (mode === "restricted") {
|
|
1493
|
+
payload.members = parseMembers(args);
|
|
1494
|
+
if (payload.members.length === 0) {
|
|
1495
|
+
throw new CliError("restricted access needs --users <a@b.com,c@d.com> of existing Railcode users.", 2);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return { method: "PUT", payload };
|
|
1499
|
+
}
|
|
1500
|
+
function rejectAccessOptions(args, allowUsers) {
|
|
1501
|
+
const allowed = new Set(["help"]);
|
|
1502
|
+
if (allowUsers)
|
|
1503
|
+
allowed.add("users");
|
|
1504
|
+
const unknown = Object.keys(args.options).filter((option) => !allowed.has(option));
|
|
1505
|
+
if (unknown.length > 0) {
|
|
1506
|
+
const hint = unknown[0] === "users" ? " --users only applies to restricted access." : "";
|
|
1507
|
+
throw new CliError(`Unknown option for access: --${unknown[0]}.${hint}`, 2);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function parseMembers(args) {
|
|
1511
|
+
const raw = stringOption(args, "users");
|
|
1512
|
+
if (!raw)
|
|
1513
|
+
return [];
|
|
1514
|
+
return raw
|
|
1515
|
+
.split(",")
|
|
1516
|
+
.map((member) => member.trim())
|
|
1517
|
+
.filter(Boolean);
|
|
1518
|
+
}
|
|
1519
|
+
async function sendAccessRequest(apiUrl, config, app, method, payload) {
|
|
1520
|
+
if (!config.apiToken) {
|
|
1521
|
+
throw new CliError("Missing Railcode API token. Run from a terminal once, or set RAILCODE_API_TOKEN.", 1);
|
|
1522
|
+
}
|
|
1523
|
+
const headers = { Authorization: `Bearer ${config.apiToken}` };
|
|
1524
|
+
if (method === "PUT")
|
|
1525
|
+
headers["Content-Type"] = "application/json";
|
|
1526
|
+
try {
|
|
1527
|
+
return await fetch(`${apiUrl}/v1/apps/${app}/access`, {
|
|
1528
|
+
method,
|
|
1529
|
+
headers,
|
|
1530
|
+
body: method === "PUT" ? JSON.stringify(payload) : undefined,
|
|
1531
|
+
redirect: "manual",
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
catch (error) {
|
|
1535
|
+
throw new CliError(`Could not reach ${apiUrl}. Configure the Railcode URL in railcode.json deploy.apiUrl or RAILCODE_API_URL. ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
async function handleAccessResponse(response, app, method) {
|
|
1539
|
+
if (response.ok) {
|
|
1540
|
+
printAccess((await response.json()), method === "PUT");
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const detail = errorDetail(await response.text());
|
|
1544
|
+
if (response.status === 401) {
|
|
1545
|
+
throw new CliError("Access login was rejected. Run the command again to log in.", 1);
|
|
1546
|
+
}
|
|
1547
|
+
if (response.status === 403) {
|
|
1548
|
+
throw new CliError(`Access change denied for ${app}: ${detail}. Only the app owner or an admin can change access.`, 1);
|
|
1549
|
+
}
|
|
1550
|
+
if (response.status === 404 || response.status === 409) {
|
|
1551
|
+
throw new CliError(detail || `App ${app} not found.`, 1);
|
|
1552
|
+
}
|
|
1553
|
+
throw new CliError(`Access ${method === "PUT" ? "update" : "lookup"} failed (${response.status}): ${detail}`, 1);
|
|
1554
|
+
}
|
|
1555
|
+
function printAccess(body, changed) {
|
|
1556
|
+
console.log(`${changed ? "Access set:" : "Access:"} ${accessModeLabel(body.mode)}`);
|
|
1557
|
+
if (body.mode === "restricted" && body.members && body.members.length > 0) {
|
|
1558
|
+
console.log(`Allowed: ${body.members.join(", ")}`);
|
|
1559
|
+
}
|
|
1560
|
+
if (body.url)
|
|
1561
|
+
console.log(body.url);
|
|
1562
|
+
}
|
|
1563
|
+
function accessModeLabel(mode) {
|
|
1564
|
+
if (mode === "workspace")
|
|
1565
|
+
return "public (anyone signed in)";
|
|
1566
|
+
if (mode === "private")
|
|
1567
|
+
return "private (owner only)";
|
|
1568
|
+
if (mode === "restricted")
|
|
1569
|
+
return "restricted (named users only)";
|
|
1570
|
+
return mode || "unknown";
|
|
1571
|
+
}
|
|
1572
|
+
function errorDetail(text) {
|
|
1573
|
+
try {
|
|
1574
|
+
const parsed = JSON.parse(text);
|
|
1575
|
+
if (typeof parsed.detail === "string")
|
|
1576
|
+
return parsed.detail;
|
|
1577
|
+
}
|
|
1578
|
+
catch {
|
|
1579
|
+
// Non-JSON error body; fall through to the raw text.
|
|
1580
|
+
}
|
|
1581
|
+
return text;
|
|
1582
|
+
}
|
|
1188
1583
|
function appUrlFromOrigin(app, origin) {
|
|
1189
1584
|
const url = new URL(origin);
|
|
1190
1585
|
const [first, ...rest] = url.hostname.split(".");
|
|
@@ -1200,17 +1595,31 @@ function appUrlFromOrigin(app, origin) {
|
|
|
1200
1595
|
return url.toString();
|
|
1201
1596
|
}
|
|
1202
1597
|
const DESIGN_SYSTEM_HELP = `Usage:
|
|
1203
|
-
railcode design-system
|
|
1204
|
-
railcode pull design-system [output.md] [--output output.md]
|
|
1598
|
+
railcode get design-system
|
|
1205
1599
|
|
|
1206
1600
|
Options:
|
|
1207
1601
|
--api-url <url> Railcode auth/API URL. Defaults to saved config, or prompts if missing.
|
|
1208
|
-
|
|
1602
|
+
|
|
1603
|
+
Compatibility aliases:
|
|
1604
|
+
railcode design-system pull
|
|
1605
|
+
railcode pull design-system
|
|
1209
1606
|
`;
|
|
1607
|
+
async function commandGet(args) {
|
|
1608
|
+
const target = args.rest[0];
|
|
1609
|
+
if (target === "design-system") {
|
|
1610
|
+
await commandDesignSystemGet({ ...args, rest: args.rest.slice(1) });
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (!target || target === "help" || booleanOption(args, "help")) {
|
|
1614
|
+
console.log(DESIGN_SYSTEM_HELP);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
throw new CliError(`Unknown get target: ${target}\n\n${DESIGN_SYSTEM_HELP}`, 2);
|
|
1618
|
+
}
|
|
1210
1619
|
async function commandPull(args) {
|
|
1211
1620
|
const target = args.rest[0];
|
|
1212
1621
|
if (target === "design-system") {
|
|
1213
|
-
await
|
|
1622
|
+
await commandDesignSystemGet({ ...args, rest: args.rest.slice(1) });
|
|
1214
1623
|
return;
|
|
1215
1624
|
}
|
|
1216
1625
|
if (!target || target === "help" || booleanOption(args, "help")) {
|
|
@@ -1222,7 +1631,11 @@ async function commandPull(args) {
|
|
|
1222
1631
|
async function commandDesignSystem(args) {
|
|
1223
1632
|
const subcommand = args.rest[0];
|
|
1224
1633
|
if (subcommand === "pull") {
|
|
1225
|
-
await
|
|
1634
|
+
await commandDesignSystemGet({ ...args, rest: args.rest.slice(1) });
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
if (subcommand === "get") {
|
|
1638
|
+
await commandDesignSystemGet({ ...args, rest: args.rest.slice(1) });
|
|
1226
1639
|
return;
|
|
1227
1640
|
}
|
|
1228
1641
|
if (!subcommand || subcommand === "help" || booleanOption(args, "help")) {
|
|
@@ -1231,16 +1644,16 @@ async function commandDesignSystem(args) {
|
|
|
1231
1644
|
}
|
|
1232
1645
|
throw new CliError(`Unknown design-system command: ${subcommand}\n\n${DESIGN_SYSTEM_HELP}`, 2);
|
|
1233
1646
|
}
|
|
1234
|
-
async function
|
|
1647
|
+
async function commandDesignSystemGet(args) {
|
|
1235
1648
|
if (args.rest[0] === "help" || booleanOption(args, "help")) {
|
|
1236
1649
|
console.log(DESIGN_SYSTEM_HELP);
|
|
1237
1650
|
return;
|
|
1238
1651
|
}
|
|
1239
|
-
|
|
1652
|
+
rejectDesignSystemGetArgs(args);
|
|
1240
1653
|
const config = loadConfig();
|
|
1241
1654
|
const authUrl = await resolveDesignSystemAuthOrigin(config, args);
|
|
1242
1655
|
const apiUrl = canonicalApiOrigin(authUrl);
|
|
1243
|
-
let authedConfig = await ensureApiToken({ ...config, apiUrl: authUrl }, authUrl, "Design system
|
|
1656
|
+
let authedConfig = await ensureApiToken({ ...config, apiUrl: authUrl }, authUrl, "Design system get");
|
|
1244
1657
|
let response = await getDesignSystem(apiUrl, authedConfig);
|
|
1245
1658
|
if (response.status === 401 && !process.env.RAILCODE_API_TOKEN && process.stdin.isTTY) {
|
|
1246
1659
|
console.log("Saved login expired. Log in again to continue.");
|
|
@@ -1249,39 +1662,27 @@ async function commandDesignSystemPull(args) {
|
|
|
1249
1662
|
delete retryConfig.apiTokenPrefix;
|
|
1250
1663
|
delete retryConfig.cookies;
|
|
1251
1664
|
saveConfig(retryConfig);
|
|
1252
|
-
authedConfig = await ensureApiToken(retryConfig, authUrl, "Design system
|
|
1665
|
+
authedConfig = await ensureApiToken(retryConfig, authUrl, "Design system get");
|
|
1253
1666
|
response = await getDesignSystem(apiUrl, authedConfig);
|
|
1254
1667
|
}
|
|
1255
|
-
|
|
1256
|
-
writeDesignSystemMarkdown(designSystem.markdown, designSystemOutput(args));
|
|
1668
|
+
writeDesignSystemMarkdown(await handleDesignSystemResponse(response));
|
|
1257
1669
|
}
|
|
1258
|
-
function
|
|
1259
|
-
const allowedOptions = new Set(["apiUrl", "help"
|
|
1670
|
+
function rejectDesignSystemGetArgs(args) {
|
|
1671
|
+
const allowedOptions = new Set(["apiUrl", "help"]);
|
|
1260
1672
|
const unknown = Object.keys(args.options).filter((option) => !allowedOptions.has(option));
|
|
1261
1673
|
if (unknown.length > 0) {
|
|
1262
|
-
throw new CliError(`Unknown option for design-system
|
|
1263
|
-
}
|
|
1264
|
-
if (args.rest.length > 1) {
|
|
1265
|
-
throw new CliError("design-system pull accepts at most one output path.", 2);
|
|
1674
|
+
throw new CliError(`Unknown option for design-system get: --${unknown[0]}`, 2);
|
|
1266
1675
|
}
|
|
1267
|
-
if (args.rest.length
|
|
1268
|
-
throw new CliError("
|
|
1269
|
-
}
|
|
1270
|
-
if (args.options.output === true) {
|
|
1271
|
-
throw new CliError("--output requires a file path.", 2);
|
|
1676
|
+
if (args.rest.length > 0) {
|
|
1677
|
+
throw new CliError("railcode get design-system does not accept positional arguments.", 2);
|
|
1272
1678
|
}
|
|
1273
1679
|
}
|
|
1274
|
-
function designSystemOutput(args) {
|
|
1275
|
-
if (typeof args.options.output === "string")
|
|
1276
|
-
return args.options.output;
|
|
1277
|
-
return args.rest[0];
|
|
1278
|
-
}
|
|
1279
1680
|
async function resolveDesignSystemAuthOrigin(config, args) {
|
|
1280
1681
|
const manifestApiUrl = readRailcodeManifest(process.cwd())?.manifest.deploy?.apiUrl;
|
|
1281
1682
|
return resolveRequiredAuthOrigin({
|
|
1282
1683
|
config,
|
|
1283
1684
|
args,
|
|
1284
|
-
action: "Design system
|
|
1685
|
+
action: "Design system get",
|
|
1285
1686
|
fallbackApiUrl: manifestApiUrl,
|
|
1286
1687
|
missingUrlHint: "Pass --api-url <url>, set deploy.apiUrl in railcode.json, or set RAILCODE_API_URL.",
|
|
1287
1688
|
});
|
|
@@ -1302,35 +1703,30 @@ async function getDesignSystem(apiUrl, config) {
|
|
|
1302
1703
|
}
|
|
1303
1704
|
async function handleDesignSystemResponse(response) {
|
|
1304
1705
|
if (response.ok) {
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1706
|
+
const text = await response.text();
|
|
1707
|
+
const contentType = response.headers.get("content-type") || "";
|
|
1708
|
+
if (contentType.includes("application/json")) {
|
|
1709
|
+
const body = JSON.parse(text);
|
|
1710
|
+
if (typeof body.markdown !== "string") {
|
|
1711
|
+
throw new CliError("Design system response did not include markdown.", 1);
|
|
1712
|
+
}
|
|
1713
|
+
return body.markdown;
|
|
1308
1714
|
}
|
|
1309
|
-
return
|
|
1310
|
-
markdown: body.markdown,
|
|
1311
|
-
updated_at: body.updated_at ?? null,
|
|
1312
|
-
};
|
|
1715
|
+
return text;
|
|
1313
1716
|
}
|
|
1314
1717
|
const text = await response.text();
|
|
1315
1718
|
if (response.status === 401) {
|
|
1316
|
-
throw new CliError("Design system
|
|
1719
|
+
throw new CliError("Design system get login was rejected. Run the command again to log in.", 1);
|
|
1317
1720
|
}
|
|
1318
1721
|
if (response.status === 403) {
|
|
1319
|
-
throw new CliError(`Design system
|
|
1722
|
+
throw new CliError(`Design system get denied: ${text}`, 1);
|
|
1320
1723
|
}
|
|
1321
|
-
throw new CliError(`Design system
|
|
1724
|
+
throw new CliError(`Design system get failed (${response.status}): ${text}`, 1);
|
|
1322
1725
|
}
|
|
1323
|
-
function writeDesignSystemMarkdown(markdown
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
process.stdout.write("\n");
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
const path = resolve(output);
|
|
1331
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
1332
|
-
writeFileSync(path, markdown);
|
|
1333
|
-
console.log(`Wrote ${relativeLabel(process.cwd(), path)}`);
|
|
1726
|
+
function writeDesignSystemMarkdown(markdown) {
|
|
1727
|
+
process.stdout.write(markdown);
|
|
1728
|
+
if (markdown && !markdown.endsWith("\n"))
|
|
1729
|
+
process.stdout.write("\n");
|
|
1334
1730
|
}
|
|
1335
1731
|
async function commandInit(args) {
|
|
1336
1732
|
const app = args.rest[0];
|