railcode 0.1.2 → 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
CHANGED
|
@@ -46,7 +46,7 @@ mkdir my-app
|
|
|
46
46
|
cd my-app
|
|
47
47
|
railcode init my-app
|
|
48
48
|
railcode dev
|
|
49
|
-
railcode
|
|
49
|
+
railcode get design-system
|
|
50
50
|
railcode deploy
|
|
51
51
|
```
|
|
52
52
|
|
|
@@ -56,20 +56,22 @@ at the root, and deployable build output goes in `dist/`. Direct dependencies
|
|
|
56
56
|
in the starter are exact version pins.
|
|
57
57
|
|
|
58
58
|
`railcode dev` installs missing app dependencies on first run, then runs the app
|
|
59
|
-
from the current Railcode directory with no login step.
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
from the current Railcode directory with no login step. It starts on
|
|
60
|
+
`127.0.0.1:7331` and automatically uses the next available port when another dev
|
|
61
|
+
server is already running; use the printed URL. For inferred Vite apps, the
|
|
62
|
+
asset dev server likewise starts at `5173` and moves upward when needed.
|
|
63
|
+
Identity, access metadata, KV, and files run locally; backend-backed APIs such
|
|
64
|
+
as SQL and LLM are forwarded to the configured Railcode URL when that backend
|
|
65
|
+
access is available.
|
|
62
66
|
|
|
63
67
|
`railcode login` opens a browser authorization flow. Pass `--api-url` or set
|
|
64
68
|
`RAILCODE_API_URL` to choose the Railcode auth/API origin; if neither is set and
|
|
65
69
|
no saved URL exists, the CLI prompts for the domain and assumes `https://` when
|
|
66
70
|
you enter a bare domain.
|
|
67
71
|
|
|
68
|
-
`railcode design-system
|
|
72
|
+
`railcode get design-system` fetches the platform design-system markdown from
|
|
69
73
|
`/v1/config/design-system` using the saved browser-authorized API token or
|
|
70
|
-
`RAILCODE_API_TOKEN
|
|
71
|
-
prints markdown to stdout by default, or writes to a path passed positionally or
|
|
72
|
-
with `--output`.
|
|
74
|
+
`RAILCODE_API_TOKEN` and prints it to stdout.
|
|
73
75
|
|
|
74
76
|
`railcode deploy` runs from the current Railcode app directory, builds the app,
|
|
75
77
|
and publishes the inferred app output over HTTP to the canonical API host. It
|
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/")) {
|
|
@@ -1004,13 +1205,22 @@ function escapeHtml(value) {
|
|
|
1004
1205
|
.replaceAll('"', """)
|
|
1005
1206
|
.replaceAll("'", "'");
|
|
1006
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
|
+
`;
|
|
1007
1216
|
async function commandDeploy(args) {
|
|
1008
1217
|
if (booleanOption(args, "help")) {
|
|
1009
|
-
console.log(
|
|
1218
|
+
console.log(DEPLOY_HELP);
|
|
1010
1219
|
return;
|
|
1011
1220
|
}
|
|
1012
1221
|
rejectDeployArgs(args);
|
|
1013
1222
|
const ctx = resolveDeployContext();
|
|
1223
|
+
const access = resolveDeployAccess(args, ctx);
|
|
1014
1224
|
await buildApp(ctx.appRoot);
|
|
1015
1225
|
const source = resolveDeploySource(ctx);
|
|
1016
1226
|
const files = collectDeployFiles(source);
|
|
@@ -1019,7 +1229,7 @@ async function commandDeploy(args) {
|
|
|
1019
1229
|
const apiUrl = canonicalApiOrigin(authUrl);
|
|
1020
1230
|
let authedConfig = await ensureApiToken({ ...config, apiUrl: authUrl }, authUrl, "Deploy");
|
|
1021
1231
|
console.log(`Uploading ${ctx.app} to ${apiUrl}...`);
|
|
1022
|
-
const response = await postDeployBundle(apiUrl, authedConfig, ctx.app, files);
|
|
1232
|
+
const response = await postDeployBundle(apiUrl, authedConfig, ctx.app, files, access);
|
|
1023
1233
|
if (response.status === 401 && !process.env.RAILCODE_API_TOKEN && process.stdin.isTTY) {
|
|
1024
1234
|
console.log("Saved login expired. Log in again to continue.");
|
|
1025
1235
|
const retryConfig = { ...authedConfig, apiUrl: authUrl };
|
|
@@ -1028,19 +1238,46 @@ async function commandDeploy(args) {
|
|
|
1028
1238
|
delete retryConfig.cookies;
|
|
1029
1239
|
saveConfig(retryConfig);
|
|
1030
1240
|
const freshConfig = await ensureApiToken(retryConfig, authUrl, "Deploy");
|
|
1031
|
-
|
|
1241
|
+
const retry = await postDeployBundle(apiUrl, freshConfig, ctx.app, files, access);
|
|
1242
|
+
await handleDeployResponse(retry, ctx.app, apiUrl, access);
|
|
1032
1243
|
return;
|
|
1033
1244
|
}
|
|
1034
|
-
await handleDeployResponse(response, ctx.app, apiUrl);
|
|
1245
|
+
await handleDeployResponse(response, ctx.app, apiUrl, access);
|
|
1035
1246
|
}
|
|
1036
1247
|
function rejectDeployArgs(args) {
|
|
1037
1248
|
if (args.rest.length > 0) {
|
|
1038
1249
|
throw new CliError("railcode deploy does not take an app argument. Run it from the Railcode app directory.", 2);
|
|
1039
1250
|
}
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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);
|
|
1044
1281
|
}
|
|
1045
1282
|
function resolveDeployContext() {
|
|
1046
1283
|
const cwd = process.cwd();
|
|
@@ -1058,6 +1295,7 @@ function resolveDeployContext() {
|
|
|
1058
1295
|
appRoot,
|
|
1059
1296
|
workspaceRoot,
|
|
1060
1297
|
apiUrl: manifest?.deploy?.apiUrl,
|
|
1298
|
+
access: manifest?.deploy?.access,
|
|
1061
1299
|
};
|
|
1062
1300
|
}
|
|
1063
1301
|
async function buildApp(appRoot) {
|
|
@@ -1149,7 +1387,7 @@ function collectDeployFiles(root) {
|
|
|
1149
1387
|
}
|
|
1150
1388
|
return files;
|
|
1151
1389
|
}
|
|
1152
|
-
async function postDeployBundle(apiUrl, config, app, files) {
|
|
1390
|
+
async function postDeployBundle(apiUrl, config, app, files, access) {
|
|
1153
1391
|
try {
|
|
1154
1392
|
return await fetch(`${apiUrl}/v1/apps/${app}/deploy`, {
|
|
1155
1393
|
method: "POST",
|
|
@@ -1157,7 +1395,7 @@ async function postDeployBundle(apiUrl, config, app, files) {
|
|
|
1157
1395
|
Authorization: `Bearer ${config.apiToken}`,
|
|
1158
1396
|
"Content-Type": "application/json",
|
|
1159
1397
|
},
|
|
1160
|
-
body: JSON.stringify({ files }),
|
|
1398
|
+
body: JSON.stringify(access ? { files, access } : { files }),
|
|
1161
1399
|
redirect: "manual",
|
|
1162
1400
|
});
|
|
1163
1401
|
}
|
|
@@ -1165,14 +1403,19 @@ async function postDeployBundle(apiUrl, config, app, files) {
|
|
|
1165
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);
|
|
1166
1404
|
}
|
|
1167
1405
|
}
|
|
1168
|
-
async function handleDeployResponse(response, app, apiUrl) {
|
|
1406
|
+
async function handleDeployResponse(response, app, apiUrl, requestedAccess) {
|
|
1169
1407
|
if (response.ok) {
|
|
1170
1408
|
const body = await response.json();
|
|
1171
1409
|
console.log(`Deployed ${app}${body.files ? ` (${body.files} files)` : ""}`);
|
|
1172
1410
|
console.log(body.url || appUrlFromOrigin(app, apiUrl));
|
|
1173
1411
|
const accessLabel = deployAccessLabel(body.access);
|
|
1174
|
-
if (accessLabel)
|
|
1412
|
+
if (accessLabel) {
|
|
1175
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
|
+
}
|
|
1176
1419
|
return;
|
|
1177
1420
|
}
|
|
1178
1421
|
const text = await response.text();
|
|
@@ -1187,8 +1430,156 @@ async function handleDeployResponse(response, app, apiUrl) {
|
|
|
1187
1430
|
function deployAccessLabel(access) {
|
|
1188
1431
|
if (access === "workspace_created")
|
|
1189
1432
|
return "public to signed-in users";
|
|
1433
|
+
if (access === "private_created")
|
|
1434
|
+
return "private (owner only)";
|
|
1190
1435
|
return undefined;
|
|
1191
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
|
+
}
|
|
1192
1583
|
function appUrlFromOrigin(app, origin) {
|
|
1193
1584
|
const url = new URL(origin);
|
|
1194
1585
|
const [first, ...rest] = url.hostname.split(".");
|
|
@@ -1204,17 +1595,31 @@ function appUrlFromOrigin(app, origin) {
|
|
|
1204
1595
|
return url.toString();
|
|
1205
1596
|
}
|
|
1206
1597
|
const DESIGN_SYSTEM_HELP = `Usage:
|
|
1207
|
-
railcode design-system
|
|
1208
|
-
railcode pull design-system [output.md] [--output output.md]
|
|
1598
|
+
railcode get design-system
|
|
1209
1599
|
|
|
1210
1600
|
Options:
|
|
1211
1601
|
--api-url <url> Railcode auth/API URL. Defaults to saved config, or prompts if missing.
|
|
1212
|
-
|
|
1602
|
+
|
|
1603
|
+
Compatibility aliases:
|
|
1604
|
+
railcode design-system pull
|
|
1605
|
+
railcode pull design-system
|
|
1213
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
|
+
}
|
|
1214
1619
|
async function commandPull(args) {
|
|
1215
1620
|
const target = args.rest[0];
|
|
1216
1621
|
if (target === "design-system") {
|
|
1217
|
-
await
|
|
1622
|
+
await commandDesignSystemGet({ ...args, rest: args.rest.slice(1) });
|
|
1218
1623
|
return;
|
|
1219
1624
|
}
|
|
1220
1625
|
if (!target || target === "help" || booleanOption(args, "help")) {
|
|
@@ -1226,7 +1631,11 @@ async function commandPull(args) {
|
|
|
1226
1631
|
async function commandDesignSystem(args) {
|
|
1227
1632
|
const subcommand = args.rest[0];
|
|
1228
1633
|
if (subcommand === "pull") {
|
|
1229
|
-
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) });
|
|
1230
1639
|
return;
|
|
1231
1640
|
}
|
|
1232
1641
|
if (!subcommand || subcommand === "help" || booleanOption(args, "help")) {
|
|
@@ -1235,16 +1644,16 @@ async function commandDesignSystem(args) {
|
|
|
1235
1644
|
}
|
|
1236
1645
|
throw new CliError(`Unknown design-system command: ${subcommand}\n\n${DESIGN_SYSTEM_HELP}`, 2);
|
|
1237
1646
|
}
|
|
1238
|
-
async function
|
|
1647
|
+
async function commandDesignSystemGet(args) {
|
|
1239
1648
|
if (args.rest[0] === "help" || booleanOption(args, "help")) {
|
|
1240
1649
|
console.log(DESIGN_SYSTEM_HELP);
|
|
1241
1650
|
return;
|
|
1242
1651
|
}
|
|
1243
|
-
|
|
1652
|
+
rejectDesignSystemGetArgs(args);
|
|
1244
1653
|
const config = loadConfig();
|
|
1245
1654
|
const authUrl = await resolveDesignSystemAuthOrigin(config, args);
|
|
1246
1655
|
const apiUrl = canonicalApiOrigin(authUrl);
|
|
1247
|
-
let authedConfig = await ensureApiToken({ ...config, apiUrl: authUrl }, authUrl, "Design system
|
|
1656
|
+
let authedConfig = await ensureApiToken({ ...config, apiUrl: authUrl }, authUrl, "Design system get");
|
|
1248
1657
|
let response = await getDesignSystem(apiUrl, authedConfig);
|
|
1249
1658
|
if (response.status === 401 && !process.env.RAILCODE_API_TOKEN && process.stdin.isTTY) {
|
|
1250
1659
|
console.log("Saved login expired. Log in again to continue.");
|
|
@@ -1253,39 +1662,27 @@ async function commandDesignSystemPull(args) {
|
|
|
1253
1662
|
delete retryConfig.apiTokenPrefix;
|
|
1254
1663
|
delete retryConfig.cookies;
|
|
1255
1664
|
saveConfig(retryConfig);
|
|
1256
|
-
authedConfig = await ensureApiToken(retryConfig, authUrl, "Design system
|
|
1665
|
+
authedConfig = await ensureApiToken(retryConfig, authUrl, "Design system get");
|
|
1257
1666
|
response = await getDesignSystem(apiUrl, authedConfig);
|
|
1258
1667
|
}
|
|
1259
|
-
|
|
1260
|
-
writeDesignSystemMarkdown(designSystem.markdown, designSystemOutput(args));
|
|
1668
|
+
writeDesignSystemMarkdown(await handleDesignSystemResponse(response));
|
|
1261
1669
|
}
|
|
1262
|
-
function
|
|
1263
|
-
const allowedOptions = new Set(["apiUrl", "help"
|
|
1670
|
+
function rejectDesignSystemGetArgs(args) {
|
|
1671
|
+
const allowedOptions = new Set(["apiUrl", "help"]);
|
|
1264
1672
|
const unknown = Object.keys(args.options).filter((option) => !allowedOptions.has(option));
|
|
1265
1673
|
if (unknown.length > 0) {
|
|
1266
|
-
throw new CliError(`Unknown option for design-system
|
|
1267
|
-
}
|
|
1268
|
-
if (args.rest.length > 1) {
|
|
1269
|
-
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);
|
|
1270
1675
|
}
|
|
1271
|
-
if (args.rest.length
|
|
1272
|
-
throw new CliError("
|
|
1273
|
-
}
|
|
1274
|
-
if (args.options.output === true) {
|
|
1275
|
-
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);
|
|
1276
1678
|
}
|
|
1277
1679
|
}
|
|
1278
|
-
function designSystemOutput(args) {
|
|
1279
|
-
if (typeof args.options.output === "string")
|
|
1280
|
-
return args.options.output;
|
|
1281
|
-
return args.rest[0];
|
|
1282
|
-
}
|
|
1283
1680
|
async function resolveDesignSystemAuthOrigin(config, args) {
|
|
1284
1681
|
const manifestApiUrl = readRailcodeManifest(process.cwd())?.manifest.deploy?.apiUrl;
|
|
1285
1682
|
return resolveRequiredAuthOrigin({
|
|
1286
1683
|
config,
|
|
1287
1684
|
args,
|
|
1288
|
-
action: "Design system
|
|
1685
|
+
action: "Design system get",
|
|
1289
1686
|
fallbackApiUrl: manifestApiUrl,
|
|
1290
1687
|
missingUrlHint: "Pass --api-url <url>, set deploy.apiUrl in railcode.json, or set RAILCODE_API_URL.",
|
|
1291
1688
|
});
|
|
@@ -1306,35 +1703,30 @@ async function getDesignSystem(apiUrl, config) {
|
|
|
1306
1703
|
}
|
|
1307
1704
|
async function handleDesignSystemResponse(response) {
|
|
1308
1705
|
if (response.ok) {
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
|
|
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;
|
|
1312
1714
|
}
|
|
1313
|
-
return
|
|
1314
|
-
markdown: body.markdown,
|
|
1315
|
-
updated_at: body.updated_at ?? null,
|
|
1316
|
-
};
|
|
1715
|
+
return text;
|
|
1317
1716
|
}
|
|
1318
1717
|
const text = await response.text();
|
|
1319
1718
|
if (response.status === 401) {
|
|
1320
|
-
throw new CliError("Design system
|
|
1719
|
+
throw new CliError("Design system get login was rejected. Run the command again to log in.", 1);
|
|
1321
1720
|
}
|
|
1322
1721
|
if (response.status === 403) {
|
|
1323
|
-
throw new CliError(`Design system
|
|
1722
|
+
throw new CliError(`Design system get denied: ${text}`, 1);
|
|
1324
1723
|
}
|
|
1325
|
-
throw new CliError(`Design system
|
|
1724
|
+
throw new CliError(`Design system get failed (${response.status}): ${text}`, 1);
|
|
1326
1725
|
}
|
|
1327
|
-
function writeDesignSystemMarkdown(markdown
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
process.stdout.write("\n");
|
|
1332
|
-
return;
|
|
1333
|
-
}
|
|
1334
|
-
const path = resolve(output);
|
|
1335
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
1336
|
-
writeFileSync(path, markdown);
|
|
1337
|
-
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");
|
|
1338
1730
|
}
|
|
1339
1731
|
async function commandInit(args) {
|
|
1340
1732
|
const app = args.rest[0];
|
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@tailwindcss/vite": "4.3.1",
|
|
18
|
+
"@types/node": "26.0.0",
|
|
18
19
|
"@types/react": "19.2.17",
|
|
19
20
|
"@types/react-dom": "19.2.3",
|
|
20
21
|
"@vitejs/plugin-react": "6.0.2",
|
|
@@ -779,6 +780,16 @@
|
|
|
779
780
|
"tslib": "^2.4.0"
|
|
780
781
|
}
|
|
781
782
|
},
|
|
783
|
+
"node_modules/@types/node": {
|
|
784
|
+
"version": "26.0.0",
|
|
785
|
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
|
|
786
|
+
"integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
|
|
787
|
+
"dev": true,
|
|
788
|
+
"license": "MIT",
|
|
789
|
+
"dependencies": {
|
|
790
|
+
"undici-types": "~8.3.0"
|
|
791
|
+
}
|
|
792
|
+
},
|
|
782
793
|
"node_modules/@types/react": {
|
|
783
794
|
"version": "19.2.17",
|
|
784
795
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
|
@@ -1397,6 +1408,13 @@
|
|
|
1397
1408
|
"node": ">=14.17"
|
|
1398
1409
|
}
|
|
1399
1410
|
},
|
|
1411
|
+
"node_modules/undici-types": {
|
|
1412
|
+
"version": "8.3.0",
|
|
1413
|
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
|
1414
|
+
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
|
1415
|
+
"dev": true,
|
|
1416
|
+
"license": "MIT"
|
|
1417
|
+
},
|
|
1400
1418
|
"node_modules/vite": {
|
|
1401
1419
|
"version": "8.0.16",
|
|
1402
1420
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
package/static/sdk.js
CHANGED
|
@@ -372,7 +372,16 @@
|
|
|
372
372
|
var databaseConnectors = dataConnectors;
|
|
373
373
|
|
|
374
374
|
// src/design-system.ts
|
|
375
|
-
var designSystem = () => track("design-system", "designSystem()", () =>
|
|
375
|
+
var designSystem = () => track("design-system", "designSystem()", async () => {
|
|
376
|
+
const response = await call("GET", "/config/design-system");
|
|
377
|
+
if (typeof response === "string") return response;
|
|
378
|
+
if (response && typeof response === "object") {
|
|
379
|
+
const body = response;
|
|
380
|
+
if (typeof body.markdown === "string") return body.markdown;
|
|
381
|
+
if (typeof body.text === "function") return body.text();
|
|
382
|
+
}
|
|
383
|
+
throw new Error("Design system response did not include markdown.");
|
|
384
|
+
});
|
|
376
385
|
|
|
377
386
|
// src/files.ts
|
|
378
387
|
var files = {
|