create-asaje-go-vue 0.3.8 → 0.3.9

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.
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import crypto from "node:crypto";
4
3
  import os from "node:os";
5
4
  import path from "node:path";
6
5
  import process from "node:process";
@@ -8,63 +7,100 @@ import {
8
7
  cancel,
9
8
  confirm,
10
9
  intro,
11
- isCancel,
12
10
  outro,
13
- password,
14
11
  select,
15
12
  text,
16
13
  } from "@clack/prompts";
17
- import degit from "degit";
18
14
  import { execa } from "execa";
19
15
  import fs from "fs-extra";
20
16
  import pc from "picocolors";
17
+ import {
18
+ parseCreateArgs,
19
+ parseDeployRailwayArgs,
20
+ parseDestroyRailwayArgs,
21
+ parseDirectoryArgs,
22
+ parseSetupRailwayArgs,
23
+ parseStartArgs,
24
+ parseUpdateArgs,
25
+ } from "../src/cli/args.js";
26
+ import { toEnvContent, tryReadEnvFile } from "../src/cli/env.js";
27
+ import { splitCommaSeparatedPaths, uniquePaths } from "../src/cli/paths.js";
28
+ import { prompt } from "../src/cli/prompts.js";
29
+ import { checkCommand, createManagedProcess, runCommand } from "../src/cli/process.js";
30
+ import { runCliCommand } from "../src/cli/runner.js";
31
+ import {
32
+ randomSecret,
33
+ resolveProjectSlug,
34
+ shellEscape,
35
+ } from "../src/cli/strings.js";
36
+ import { fetchRailwayProjectServices } from "../src/railway/client.js";
37
+ import {
38
+ readRailwayManifest as readRailwayManifestFile,
39
+ writeRailwayManifest as writeRailwayManifestFile,
40
+ } from "../src/railway/manifest.js";
41
+ import {
42
+ DEFAULT_RAILWAY_APP_SERVICE_SPECS,
43
+ buildCreateRailwayServices,
44
+ buildSyncedRailwayManifest,
45
+ findCreatedRailwayService,
46
+ findRailwayService,
47
+ findRailwayServiceByKey,
48
+ normalizeRailwayServiceName,
49
+ normalizeRailwayServices,
50
+ resolveRailwayAppServiceSpecs,
51
+ resolveRailwayServiceName,
52
+ updateRailwayManifestAppServices,
53
+ } from "../src/railway/services.js";
54
+ import {
55
+ buildGithubWorkflowContent,
56
+ buildProjectReadmeContent,
57
+ buildReadmeAnswersFromProjectConfig,
58
+ getGithubActionsDeployConfig,
59
+ } from "../src/project/docs.js";
60
+ import { buildProjectMakefileContent } from "../src/project/makefile.js";
61
+ import {
62
+ cleanupTemplateFiles,
63
+ cloneTemplate,
64
+ ensureDestinationIsAvailable,
65
+ ensureProjectStructure,
66
+ ensureScaffoldedSurfaces,
67
+ loadProjectConfig,
68
+ } from "../src/project/files.js";
69
+ import {
70
+ buildSyncedProjectConfig,
71
+ getRailwayConfig,
72
+ scanProjectForManagedRailwayServices,
73
+ } from "../src/project/sync.js";
74
+ import { collectCreateAnswers } from "../src/create/questions.js";
75
+ import { collectStartAnswers } from "../src/start/answers.js";
76
+ import {
77
+ ensureEnvFiles,
78
+ installProjectDependencies,
79
+ loadRuntimeConfig,
80
+ startInfrastructure,
81
+ } from "../src/start/runtime.js";
82
+ import {
83
+ buildRailwayConfigExportFilename,
84
+ buildRailwayConfigSnapshotDiff,
85
+ readRailwayConfigSnapshot,
86
+ sanitizeRailwayConfigSnapshotDiff,
87
+ assertRailwaySnapshotImportable,
88
+ } from "../src/railway/snapshot.js";
89
+ import {
90
+ assignManagedRailwayServiceVariables,
91
+ buildRailwayBrowserOrigins,
92
+ buildRailwayVariableDiff,
93
+ formatRailwayVariableValue,
94
+ sanitizeVariablesForOutput,
95
+ } from "../src/railway/variables.js";
96
+ import { resolveInvocation } from "../src/cli/invocation.js";
21
97
 
22
98
  const DEFAULT_TEMPLATE = process.env.ASAJE_TEMPLATE_REPO || "asaje379/boilerplate-go-vue";
23
99
  const DEFAULT_BRANCH = process.env.ASAJE_TEMPLATE_BRANCH || "main";
24
- const EXCLUDED_TEMPLATE_PATHS = ["cli"];
25
- const RAILWAY_GRAPHQL_ENDPOINT = "https://backboard.railway.com/graphql/v2";
26
100
  const RAILWAY_MANIFEST_FILENAME = "asaje.railway.json";
27
101
  const DEFAULT_RAILWAY_BUCKET = "boilerplate-files";
28
102
  const RAILWAY_SERVICE_DISCOVERY_RETRY_DELAY_MS = 2000;
29
103
  const RAILWAY_SERVICE_DISCOVERY_RETRY_COUNT = 5;
30
- const DEFAULT_RAILWAY_APP_SERVICE_SPECS = [
31
- {
32
- aliases: ["api", "backend", "server"],
33
- baseName: "api",
34
- directory: "api",
35
- key: "api",
36
- },
37
- {
38
- aliases: ["worker", "api-worker"],
39
- baseName: "worker",
40
- directory: "api",
41
- key: "worker",
42
- },
43
- {
44
- aliases: ["admin", "frontend", "web"],
45
- baseName: "admin",
46
- directory: "admin",
47
- key: "admin",
48
- },
49
- {
50
- aliases: ["realtime-gateway", "realtime"],
51
- baseName: "realtime-gateway",
52
- directory: "realtime-gateway",
53
- key: "realtime",
54
- },
55
- {
56
- aliases: ["landing", "marketing"],
57
- baseName: "landing",
58
- directory: "landing",
59
- key: "landing",
60
- },
61
- {
62
- aliases: ["pwa", "mobile-web"],
63
- baseName: "pwa",
64
- directory: "pwa",
65
- key: "pwa",
66
- },
67
- ];
68
104
  const SAFE_UPDATE_PATHS = [
69
105
  ".github/workflows/deploy-railway.yml",
70
106
  "docker-compose.yml",
@@ -87,13 +123,6 @@ const SAFE_UPDATE_PATHS = [
87
123
  "pwa/nginx.conf",
88
124
  "pwa/public",
89
125
  ];
90
- const ENV_FILE_SPECS = [
91
- { envPath: "admin/.env", examplePath: "admin/.env.example" },
92
- { envPath: "api/.env", examplePath: "api/.env.example" },
93
- { envPath: "realtime-gateway/.env", examplePath: "realtime-gateway/.env.example" },
94
- { envPath: "landing/.env", examplePath: "landing/.env.example" },
95
- { envPath: "pwa/.env", examplePath: "pwa/.env.example" },
96
- ];
97
126
 
98
127
  await main();
99
128
 
@@ -102,110 +131,27 @@ async function main() {
102
131
  intro(pc.cyan(invocation.title));
103
132
 
104
133
  try {
105
- if (invocation.command === "help") {
106
- printHelp();
107
- outro(pc.green("Ready."));
108
- return;
109
- }
110
-
111
- if (invocation.command === "start") {
112
- await runStart(invocation.argv);
113
- outro(pc.green("Services stopped."));
114
- return;
115
- }
116
-
117
- if (invocation.command === "doctor") {
118
- await runDoctor(invocation.argv);
119
- outro(pc.green("Doctor finished."));
120
- return;
121
- }
122
-
123
- if (invocation.command === "publish") {
124
- await runPublish(invocation.argv);
125
- outro(pc.green("Publish checklist complete."));
126
- return;
127
- }
128
-
129
- if (invocation.command === "update") {
130
- await runUpdate(invocation.argv);
131
- outro(pc.green("Project update complete."));
132
- return;
133
- }
134
-
135
- if (invocation.command === "sync-project-config") {
136
- await runSyncProjectConfig(invocation.argv);
137
- outro(pc.green("Project config sync complete."));
138
- return;
139
- }
140
-
141
- if (invocation.command === "sync-readme") {
142
- await runSyncReadme(invocation.argv);
143
- outro(pc.green("Project README sync complete."));
144
- return;
145
- }
146
-
147
- if (invocation.command === "sync-github-workflow") {
148
- await runSyncGithubWorkflow(invocation.argv);
149
- outro(pc.green("GitHub workflow sync complete."));
150
- return;
151
- }
152
-
153
- if (invocation.command === "setup-railway") {
154
- await runSetupRailway(invocation.argv);
155
- outro(pc.green("Railway setup complete."));
156
- return;
157
- }
158
-
159
- if (invocation.command === "update-railway") {
160
- await runUpdateRailway(invocation.argv);
161
- outro(pc.green("Railway update complete."));
162
- return;
163
- }
164
-
165
- if (invocation.command === "sync-railway-env") {
166
- await runSyncRailwayEnv(invocation.argv);
167
- outro(pc.green("Railway environment sync complete."));
168
- return;
169
- }
170
-
171
- if (invocation.command === "print-railway-config") {
172
- await runPrintRailwayConfig(invocation.argv);
173
- outro(pc.green("Railway config printed."));
174
- return;
175
- }
176
-
177
- if (invocation.command === "export-railway-config") {
178
- await runExportRailwayConfig(invocation.argv);
179
- outro(pc.green("Railway config exported."));
180
- return;
181
- }
182
-
183
- if (invocation.command === "import-railway-config") {
184
- await runImportRailwayConfig(invocation.argv);
185
- outro(pc.green("Railway config imported."));
186
- return;
187
- }
188
-
189
- if (invocation.command === "diff-railway-config") {
190
- await runDiffRailwayConfig(invocation.argv);
191
- outro(pc.green("Railway config diff complete."));
192
- return;
193
- }
194
-
195
- if (invocation.command === "deploy-railway") {
196
- await runDeployRailway(invocation.argv);
197
- outro(pc.green("Railway deployment complete."));
198
- return;
199
- }
200
-
201
- if (invocation.command === "destroy-railway") {
202
- await runDestroyRailway(invocation.argv);
203
- outro(pc.green("Railway teardown complete."));
204
- return;
205
- }
206
-
207
- await runCreate(invocation.argv);
208
- outro(pc.green("Project ready."));
134
+ const completionMessage = await runCliCommand(invocation, {
135
+ printHelp,
136
+ runCreate,
137
+ runDeployRailway,
138
+ runDestroyRailway,
139
+ runDiffRailwayConfig,
140
+ runDoctor,
141
+ runExportRailwayConfig,
142
+ runImportRailwayConfig,
143
+ runPrintRailwayConfig,
144
+ runPublish,
145
+ runSetupRailway,
146
+ runStart,
147
+ runSyncGithubWorkflow,
148
+ runSyncProjectConfig,
149
+ runSyncRailwayEnv,
150
+ runSyncReadme,
151
+ runUpdate,
152
+ runUpdateRailway,
153
+ });
154
+ outro(pc.green(completionMessage));
209
155
  } catch (error) {
210
156
  if (error instanceof Error) {
211
157
  cancel(error.message);
@@ -217,159 +163,6 @@ async function main() {
217
163
  }
218
164
  }
219
165
 
220
- function resolveInvocation(argv) {
221
- const binName = path.basename(argv[1] || "create-asaje-go-vue");
222
- const normalizedBinName = binName.replace(/\.js$/, "");
223
- const rawArgs = argv.slice(2);
224
- const firstArg = rawArgs[0];
225
-
226
- if (["help", "--help", "-h"].includes(firstArg || "")) {
227
- return { argv: rawArgs.slice(1), command: "help", title: normalizedBinName };
228
- }
229
-
230
- if (normalizedBinName === "asaje") {
231
- if (!firstArg) {
232
- return { argv: [], command: "help", title: "asaje" };
233
- }
234
-
235
- if (firstArg === "start") {
236
- return { argv: rawArgs.slice(1), command: "start", title: "asaje start" };
237
- }
238
-
239
- if (firstArg === "doctor") {
240
- return { argv: rawArgs.slice(1), command: "doctor", title: "asaje doctor" };
241
- }
242
-
243
- if (firstArg === "publish") {
244
- return { argv: rawArgs.slice(1), command: "publish", title: "asaje publish" };
245
- }
246
-
247
- if (firstArg === "update") {
248
- return { argv: rawArgs.slice(1), command: "update", title: "asaje update" };
249
- }
250
-
251
- if (firstArg === "sync-project-config") {
252
- return { argv: rawArgs.slice(1), command: "sync-project-config", title: "asaje sync-project-config" };
253
- }
254
-
255
- if (firstArg === "sync-readme") {
256
- return { argv: rawArgs.slice(1), command: "sync-readme", title: "asaje sync-readme" };
257
- }
258
-
259
- if (firstArg === "sync-github-workflow") {
260
- return { argv: rawArgs.slice(1), command: "sync-github-workflow", title: "asaje sync-github-workflow" };
261
- }
262
-
263
- if (firstArg === "setup-railway") {
264
- return { argv: rawArgs.slice(1), command: "setup-railway", title: "asaje setup-railway" };
265
- }
266
-
267
- if (firstArg === "update-railway") {
268
- return { argv: rawArgs.slice(1), command: "update-railway", title: "asaje update-railway" };
269
- }
270
-
271
- if (firstArg === "sync-railway-env") {
272
- return { argv: rawArgs.slice(1), command: "sync-railway-env", title: "asaje sync-railway-env" };
273
- }
274
-
275
- if (firstArg === "print-railway-config") {
276
- return { argv: rawArgs.slice(1), command: "print-railway-config", title: "asaje print-railway-config" };
277
- }
278
-
279
- if (firstArg === "export-railway-config") {
280
- return { argv: rawArgs.slice(1), command: "export-railway-config", title: "asaje export-railway-config" };
281
- }
282
-
283
- if (firstArg === "import-railway-config") {
284
- return { argv: rawArgs.slice(1), command: "import-railway-config", title: "asaje import-railway-config" };
285
- }
286
-
287
- if (firstArg === "diff-railway-config") {
288
- return { argv: rawArgs.slice(1), command: "diff-railway-config", title: "asaje diff-railway-config" };
289
- }
290
-
291
- if (firstArg === "deploy-railway") {
292
- return { argv: rawArgs.slice(1), command: "deploy-railway", title: "asaje deploy-railway" };
293
- }
294
-
295
- if (firstArg === "destroy-railway") {
296
- return { argv: rawArgs.slice(1), command: "destroy-railway", title: "asaje destroy-railway" };
297
- }
298
-
299
- if (firstArg === "create") {
300
- return { argv: rawArgs.slice(1), command: "create", title: "asaje create" };
301
- }
302
-
303
- return { argv: [], command: "help", title: "asaje" };
304
- }
305
-
306
- if (firstArg === "start") {
307
- return { argv: rawArgs.slice(1), command: "start", title: "create-asaje-go-vue" };
308
- }
309
-
310
- if (firstArg === "doctor") {
311
- return { argv: rawArgs.slice(1), command: "doctor", title: "create-asaje-go-vue" };
312
- }
313
-
314
- if (firstArg === "publish") {
315
- return { argv: rawArgs.slice(1), command: "publish", title: "create-asaje-go-vue" };
316
- }
317
-
318
- if (firstArg === "update") {
319
- return { argv: rawArgs.slice(1), command: "update", title: "create-asaje-go-vue" };
320
- }
321
-
322
- if (firstArg === "sync-project-config") {
323
- return { argv: rawArgs.slice(1), command: "sync-project-config", title: "create-asaje-go-vue" };
324
- }
325
-
326
- if (firstArg === "sync-readme") {
327
- return { argv: rawArgs.slice(1), command: "sync-readme", title: "create-asaje-go-vue" };
328
- }
329
-
330
- if (firstArg === "sync-github-workflow") {
331
- return { argv: rawArgs.slice(1), command: "sync-github-workflow", title: "create-asaje-go-vue" };
332
- }
333
-
334
- if (firstArg === "setup-railway") {
335
- return { argv: rawArgs.slice(1), command: "setup-railway", title: "create-asaje-go-vue" };
336
- }
337
-
338
- if (firstArg === "update-railway") {
339
- return { argv: rawArgs.slice(1), command: "update-railway", title: "create-asaje-go-vue" };
340
- }
341
-
342
- if (firstArg === "sync-railway-env") {
343
- return { argv: rawArgs.slice(1), command: "sync-railway-env", title: "create-asaje-go-vue" };
344
- }
345
-
346
- if (firstArg === "print-railway-config") {
347
- return { argv: rawArgs.slice(1), command: "print-railway-config", title: "create-asaje-go-vue" };
348
- }
349
-
350
- if (firstArg === "export-railway-config") {
351
- return { argv: rawArgs.slice(1), command: "export-railway-config", title: "create-asaje-go-vue" };
352
- }
353
-
354
- if (firstArg === "import-railway-config") {
355
- return { argv: rawArgs.slice(1), command: "import-railway-config", title: "create-asaje-go-vue" };
356
- }
357
-
358
- if (firstArg === "diff-railway-config") {
359
- return { argv: rawArgs.slice(1), command: "diff-railway-config", title: "create-asaje-go-vue" };
360
- }
361
-
362
- if (firstArg === "deploy-railway") {
363
- return { argv: rawArgs.slice(1), command: "deploy-railway", title: "create-asaje-go-vue" };
364
- }
365
-
366
- if (firstArg === "destroy-railway") {
367
- return { argv: rawArgs.slice(1), command: "destroy-railway", title: "create-asaje-go-vue" };
368
- }
369
-
370
- return { argv: rawArgs, command: "create", title: "create-asaje-go-vue" };
371
- }
372
-
373
166
  function printHelp() {
374
167
  console.log(pc.bold("\nCommands"));
375
168
  console.log(`- ${pc.bold("create-asaje-go-vue <directory>")} scaffold a new project`);
@@ -412,575 +205,33 @@ function printHelp() {
412
205
  }
413
206
 
414
207
  async function runCreate(argv) {
415
- const args = parseCreateArgs(argv);
416
- const answers = await collectCreateAnswers(args);
417
- const destinationDir = path.resolve(process.cwd(), answers.directory);
418
-
419
- await ensureDestinationIsAvailable(destinationDir);
420
-
421
- console.log(pc.dim("\nScaffolding project from GitHub template..."));
422
- await cloneTemplate(answers.template, answers.branch, destinationDir);
423
- await cleanupTemplateFiles(destinationDir);
424
- await ensureScaffoldedSurfaces(destinationDir, answers);
425
- await writeProjectConfig(destinationDir, answers);
426
- await writeEnvFiles(destinationDir, answers);
427
- await writeProjectReadme(destinationDir, answers);
428
- await writeGithubWorkflow(destinationDir, answers);
429
-
430
- if (answers.installDependencies) {
431
- console.log(pc.dim("\nInstalling frontend and Go dependencies..."));
432
- await installProjectDependencies(destinationDir);
433
- }
434
-
435
- if (answers.startInfra) {
436
- console.log(pc.dim("\nStarting local infrastructure with Docker Compose..."));
437
- await startInfrastructure(destinationDir);
438
- }
439
-
440
- printCreateSummary(destinationDir, answers);
441
- }
442
-
443
- function parseCreateArgs(argv) {
444
- const options = {
445
- branch: DEFAULT_BRANCH,
446
- installDependencies: undefined,
447
- startInfra: undefined,
448
- template: DEFAULT_TEMPLATE,
449
- yes: false,
450
- };
451
- const positionals = [];
452
-
453
- for (let index = 0; index < argv.length; index += 1) {
454
- const arg = argv[index];
455
-
456
- if (arg === "--yes" || arg === "-y") {
457
- options.yes = true;
458
- continue;
459
- }
460
-
461
- if (arg === "--template") {
462
- options.template = argv[index + 1] || options.template;
463
- index += 1;
464
- continue;
465
- }
466
-
467
- if (arg.startsWith("--template=")) {
468
- options.template = arg.split("=")[1] || options.template;
469
- continue;
470
- }
471
-
472
- if (arg === "--branch") {
473
- options.branch = argv[index + 1] || options.branch;
474
- index += 1;
475
- continue;
476
- }
477
-
478
- if (arg.startsWith("--branch=")) {
479
- options.branch = arg.split("=")[1] || options.branch;
480
- continue;
481
- }
482
-
483
- if (arg === "--install") {
484
- options.installDependencies = true;
485
- continue;
486
- }
487
-
488
- if (arg === "--skip-install") {
489
- options.installDependencies = false;
490
- continue;
491
- }
492
-
493
- if (arg === "--start-infra") {
494
- options.startInfra = true;
495
- continue;
496
- }
497
-
498
- if (arg === "--skip-infra") {
499
- options.startInfra = false;
500
- continue;
501
- }
502
-
503
- positionals.push(arg);
504
- }
505
-
506
- return { ...options, directory: positionals[0] };
507
- }
508
-
509
- function parseUpdateArgs(argv) {
510
- const options = {
511
- branch: undefined,
512
- directory: ".",
513
- dryRun: false,
514
- include: [],
515
- template: undefined,
516
- yes: false,
517
- };
518
- const positionals = [];
519
-
520
- for (let index = 0; index < argv.length; index += 1) {
521
- const arg = argv[index];
522
-
523
- if (arg === "--yes" || arg === "-y") {
524
- options.yes = true;
525
- continue;
526
- }
527
-
528
- if (arg === "--dry-run") {
529
- options.dryRun = true;
530
- continue;
531
- }
532
-
533
- if (arg === "--template") {
534
- options.template = argv[index + 1] || options.template;
535
- index += 1;
536
- continue;
537
- }
538
-
539
- if (arg.startsWith("--template=")) {
540
- options.template = arg.split("=")[1] || options.template;
541
- continue;
542
- }
543
-
544
- if (arg === "--branch") {
545
- options.branch = argv[index + 1] || options.branch;
546
- index += 1;
547
- continue;
548
- }
549
-
550
- if (arg.startsWith("--branch=")) {
551
- options.branch = arg.split("=")[1] || options.branch;
552
- continue;
553
- }
554
-
555
- if (arg === "--include") {
556
- options.include.push(...splitCommaSeparatedPaths(argv[index + 1] || ""));
557
- index += 1;
558
- continue;
559
- }
560
-
561
- if (arg.startsWith("--include=")) {
562
- options.include.push(...splitCommaSeparatedPaths(arg.split("=")[1] || ""));
563
- continue;
564
- }
565
-
566
- positionals.push(arg);
567
- }
568
-
569
- options.directory = positionals[0] || options.directory;
570
- options.include = uniquePaths(options.include);
571
- return options;
572
- }
573
-
574
- async function collectCreateAnswers(args) {
575
- const defaultDirectory = args.directory || "my-asaje-app";
576
- const defaultSlug = slugify(path.basename(defaultDirectory));
577
- const defaultAppName = titleize(defaultSlug || "asaje app");
578
-
579
- if (args.yes) {
580
- return buildCreateAnswers({
581
- adminPort: 5173,
582
- apiPort: 8080,
583
- appName: defaultAppName,
584
- branch: args.branch,
585
- bucketBasePath: defaultSlug,
586
- corsAllowedOrigins: "",
587
- defaultLocale: "fr",
588
- directory: defaultDirectory,
589
- includeLanding: true,
590
- includePwa: true,
591
- installDependencies: args.installDependencies ?? true,
592
- mailFromEmail: `no-reply@${defaultSlug || "asaje-app"}.local`,
593
- mailFromName: defaultAppName,
594
- mailProvider: "mailchimp",
595
- mailchimpApiKey: "dev-placeholder-key",
596
- realtimePort: 8090,
597
- seedAdmin: false,
598
- seedUser: false,
599
- startInfra: args.startInfra ?? true,
600
- storageProvider: "minio",
601
- swaggerUsername: "swagger",
602
- template: args.template,
603
- });
604
- }
605
-
606
- const directory = await prompt(
607
- text({
608
- defaultValue: defaultDirectory,
609
- message: "Project directory?",
610
- placeholder: "my-asaje-app",
611
- validate(value) {
612
- return value.trim().length === 0 ? "Directory is required" : undefined;
613
- },
614
- }),
615
- );
616
-
617
- const appName = await prompt(
618
- text({
619
- defaultValue: defaultAppName,
620
- message: "App name?",
621
- placeholder: "My Asaje App",
622
- validate(value) {
623
- return value.trim().length === 0 ? "App name is required" : undefined;
624
- },
625
- }),
626
- );
627
-
628
- const defaultLocale = await prompt(
629
- select({
630
- initialValue: "fr",
631
- message: "Default locale?",
632
- options: [
633
- { label: "French", value: "fr" },
634
- { label: "English", value: "en" },
635
- ],
636
- }),
637
- );
638
-
639
- const adminPort = await promptNumber("Admin dev server port?", 5173);
640
- const apiPort = await promptNumber("API port?", 8080);
641
- const realtimePort = await promptNumber("Realtime gateway port?", 8090);
642
- const swaggerUsername = await prompt(
643
- text({
644
- defaultValue: "swagger",
645
- message: "Swagger username?",
646
- validate(value) {
647
- return value.trim().length === 0 ? "Swagger username is required" : undefined;
648
- },
649
- }),
650
- );
651
-
652
- const seedAdmin = await prompt(
653
- confirm({
654
- initialValue: true,
655
- message: "Seed an admin account now?",
656
- }),
657
- );
658
-
659
- let adminName = "Admin";
660
- let adminEmail = "admin@example.com";
661
- let adminPassword = "admin12345";
662
-
663
- if (seedAdmin) {
664
- adminName = await prompt(
665
- text({
666
- defaultValue: adminName,
667
- message: "Admin name?",
668
- validate(value) {
669
- return value.trim().length >= 2 ? undefined : "Admin name must be at least 2 characters";
670
- },
671
- }),
672
- );
673
-
674
- adminEmail = await promptEmail("Admin email?", adminEmail);
675
- adminPassword = await promptSecret("Admin password?", 8);
676
- }
677
-
678
- const seedUser = await prompt(
679
- confirm({
680
- initialValue: false,
681
- message: "Seed a standard user too?",
682
- }),
683
- );
684
-
685
- let seedUserName = "User";
686
- let seedUserEmail = "user@example.com";
687
- let seedUserPassword = "user12345";
688
-
689
- if (seedUser) {
690
- seedUserName = await prompt(
691
- text({
692
- defaultValue: seedUserName,
693
- message: "User name?",
694
- validate(value) {
695
- return value.trim().length >= 2 ? undefined : "User name must be at least 2 characters";
696
- },
697
- }),
698
- );
699
-
700
- seedUserEmail = await promptEmail("User email?", seedUserEmail);
701
- seedUserPassword = await promptSecret("User password?", 8);
702
- }
703
-
704
- const storageProvider = await prompt(
705
- select({
706
- initialValue: "minio",
707
- message: "Object storage provider?",
708
- options: [
709
- { label: "Local MinIO", value: "minio" },
710
- { label: "AWS S3 / compatible", value: "aws" },
711
- ],
712
- }),
713
- );
714
-
715
- const slug = slugify(path.basename(directory));
716
- const bucketBasePath = await prompt(
717
- text({
718
- defaultValue: slug,
719
- message: "Bucket base path?",
720
- placeholder: slug,
721
- }),
722
- );
723
-
724
- let minioEndpoint = "localhost";
725
- let minioPort = 9000;
726
- let minioUseSSL = false;
727
- let minioAccessKey = "minioadmin";
728
- let minioSecretKey = "minioadmin";
729
- let minioBucket = "boilerplate-files";
730
- let minioPublicURL = "http://localhost:9000";
731
- let awsEndpointURL = "";
732
- let awsAccessKeyId = "";
733
- let awsSecretAccessKey = "";
734
- let awsBucket = "";
735
- let awsRegion = "us-east-1";
736
-
737
- if (storageProvider === "minio") {
738
- minioEndpoint = await prompt(
739
- text({
740
- defaultValue: minioEndpoint,
741
- message: "MinIO host?",
742
- validate(value) {
743
- return value.trim().length === 0 ? "MinIO host is required" : undefined;
744
- },
745
- }),
746
- );
747
- minioPort = await promptNumber("MinIO port?", minioPort);
748
- minioUseSSL = await prompt(
749
- confirm({
750
- initialValue: false,
751
- message: "Use SSL for MinIO?",
752
- }),
753
- );
754
- minioAccessKey = await prompt(
755
- text({
756
- defaultValue: minioAccessKey,
757
- message: "MinIO access key?",
758
- validate(value) {
759
- return value.trim().length === 0 ? "MinIO access key is required" : undefined;
760
- },
761
- }),
762
- );
763
- minioSecretKey = await promptSecret("MinIO secret key?", 3, minioSecretKey);
764
- minioBucket = await prompt(
765
- text({
766
- defaultValue: minioBucket,
767
- message: "MinIO bucket name?",
768
- validate(value) {
769
- return value.trim().length === 0 ? "MinIO bucket name is required" : undefined;
770
- },
771
- }),
772
- );
773
- minioPublicURL = await prompt(
774
- text({
775
- defaultValue: minioPublicURL,
776
- message: "MinIO public URL?",
777
- validate(value) {
778
- return isHttpUrl(value) ? undefined : "Enter a valid URL";
779
- },
780
- }),
781
- );
782
- } else {
783
- awsEndpointURL = await prompt(
784
- text({
785
- defaultValue: awsEndpointURL,
786
- message: "AWS endpoint URL? (leave empty for AWS)",
787
- }),
788
- );
789
- awsAccessKeyId = await prompt(
790
- text({
791
- message: "AWS access key id?",
792
- validate(value) {
793
- return value.trim().length === 0 ? "AWS access key id is required" : undefined;
794
- },
795
- }),
796
- );
797
- awsSecretAccessKey = await promptSecret("AWS secret access key?", 3);
798
- awsBucket = await prompt(
799
- text({
800
- message: "AWS bucket name?",
801
- validate(value) {
802
- return value.trim().length === 0 ? "AWS bucket name is required" : undefined;
803
- },
804
- }),
805
- );
806
- awsRegion = await prompt(
807
- text({
808
- defaultValue: awsRegion,
809
- message: "AWS region?",
810
- validate(value) {
811
- return value.trim().length === 0 ? "AWS region is required" : undefined;
812
- },
813
- }),
814
- );
815
- }
816
-
817
- const mailProvider = await prompt(
818
- select({
819
- initialValue: "mailchimp",
820
- message: "Transactional email mode?",
821
- options: [
822
- { label: "Mailchimp Transactional", value: "mailchimp" },
823
- { label: "Brevo", value: "brevo" },
824
- { label: "SMTP", value: "smtp" },
825
- ],
826
- }),
827
- );
828
-
829
- const mailFromEmail = await promptEmail(
830
- "Transactional sender email?",
831
- `no-reply@${slug || "asaje-app"}.local`,
832
- );
833
- const mailFromName = await prompt(
834
- text({
835
- defaultValue: appName,
836
- message: "Transactional sender name?",
837
- validate(value) {
838
- return value.trim().length === 0 ? "Sender name is required" : undefined;
839
- },
840
- }),
841
- );
842
- const mailchimpApiKey = mailProvider === "mailchimp" ? await promptSecret("Mailchimp Transactional API key?", 3) : "";
843
- const brevoApiKey = mailProvider === "brevo" ? await promptSecret("Brevo API key?", 3) : "";
844
- const smtpHost =
845
- mailProvider === "smtp"
846
- ? await prompt(
847
- text({
848
- defaultValue: "smtp.gmail.com",
849
- message: "SMTP host?",
850
- validate(value) {
851
- return value.trim().length === 0 ? "SMTP host is required" : undefined;
852
- },
853
- }),
854
- )
855
- : "";
856
- const smtpPort = mailProvider === "smtp" ? await promptNumber("SMTP port?", 587) : 587;
857
- const smtpUsername = mailProvider === "smtp" ? await prompt(text({ defaultValue: "", message: "SMTP username?" })) : "";
858
- const smtpPassword = mailProvider === "smtp" ? await promptSecret("SMTP password?", 0, "") : "";
859
- const smtpUseSSL =
860
- mailProvider === "smtp"
861
- ? await prompt(
862
- confirm({
863
- initialValue: false,
864
- message: "Use SSL for SMTP?",
865
- }),
866
- )
867
- : false;
868
-
869
- const includeLanding = await prompt(
870
- confirm({
871
- initialValue: true,
872
- message: "Enable the optional landing surface?",
873
- }),
874
- );
875
- const includePwa = await prompt(
876
- confirm({
877
- initialValue: true,
878
- message: "Enable the optional PWA surface?",
879
- }),
880
- );
208
+ const args = parseCreateArgs(argv, { defaultBranch: DEFAULT_BRANCH, defaultTemplate: DEFAULT_TEMPLATE });
209
+ const answers = await collectCreateAnswers(args);
210
+ const destinationDir = path.resolve(process.cwd(), answers.directory);
881
211
 
882
- const enableGithubWorkflow = await prompt(
883
- confirm({
884
- initialValue: true,
885
- message: "Generate a GitHub Actions Railway deploy workflow?",
886
- }),
887
- );
888
- const productionBranch = enableGithubWorkflow
889
- ? await prompt(
890
- text({
891
- defaultValue: "main",
892
- message: "Production branch?",
893
- validate(value) {
894
- return value.trim().length === 0 ? "Production branch is required" : undefined;
895
- },
896
- }),
897
- )
898
- : "main";
899
- const stagingBranch = enableGithubWorkflow
900
- ? await prompt(
901
- text({
902
- defaultValue: "develop",
903
- message: "Staging branch?",
904
- validate(value) {
905
- return value.trim().length === 0 ? "Staging branch is required" : undefined;
906
- },
907
- }),
908
- )
909
- : "develop";
212
+ await ensureDestinationIsAvailable(destinationDir);
910
213
 
911
- const corsAllowedOrigins = await prompt(
912
- text({
913
- defaultValue: "",
914
- message: "Extra CORS origins? (comma-separated, optional)",
915
- placeholder: "https://app.example.com,https://admin.example.com",
916
- }),
917
- );
214
+ console.log(pc.dim("\nScaffolding project from GitHub template..."));
215
+ await cloneTemplate(answers.template, answers.branch, destinationDir);
216
+ await cleanupTemplateFiles(destinationDir);
217
+ await ensureScaffoldedSurfaces(destinationDir, answers);
218
+ await writeProjectConfig(destinationDir, answers);
219
+ await writeEnvFiles(destinationDir, answers);
220
+ await writeProjectMakefile(destinationDir, answers);
221
+ await writeProjectReadme(destinationDir, answers);
222
+ await writeGithubWorkflow(destinationDir, answers);
918
223
 
919
- const installDependencies = await prompt(
920
- confirm({
921
- initialValue: args.installDependencies ?? true,
922
- message: "Install dependencies now?",
923
- }),
924
- );
224
+ if (answers.installDependencies) {
225
+ console.log(pc.dim("\nInstalling frontend and Go dependencies..."));
226
+ await installProjectDependencies(destinationDir);
227
+ }
925
228
 
926
- const startInfra = await prompt(
927
- confirm({
928
- initialValue: args.startInfra ?? true,
929
- message: "Start local Docker services now?",
930
- }),
931
- );
229
+ if (answers.startInfra) {
230
+ console.log(pc.dim("\nStarting local infrastructure with Docker Compose..."));
231
+ await startInfrastructure(destinationDir);
232
+ }
932
233
 
933
- return buildCreateAnswers({
934
- adminEmail,
935
- adminName,
936
- adminPassword,
937
- adminPort,
938
- apiPort,
939
- appName,
940
- awsAccessKeyId,
941
- awsBucket,
942
- awsEndpointURL,
943
- awsRegion,
944
- awsSecretAccessKey,
945
- branch: args.branch,
946
- bucketBasePath,
947
- corsAllowedOrigins,
948
- defaultLocale,
949
- directory,
950
- enableGithubWorkflow,
951
- includeLanding,
952
- includePwa,
953
- installDependencies,
954
- brevoApiKey,
955
- mailFromEmail,
956
- mailFromName,
957
- mailProvider,
958
- mailchimpApiKey,
959
- minioAccessKey,
960
- minioBucket,
961
- minioEndpoint,
962
- minioPort,
963
- minioPublicURL,
964
- minioSecretKey,
965
- minioUseSSL,
966
- realtimePort,
967
- seedAdmin,
968
- seedUser,
969
- seedUserEmail,
970
- seedUserName,
971
- seedUserPassword,
972
- startInfra,
973
- storageProvider,
974
- swaggerUsername,
975
- smtpHost,
976
- smtpPassword,
977
- smtpPort,
978
- smtpUseSSL,
979
- smtpUsername,
980
- stagingBranch,
981
- template: args.template,
982
- productionBranch,
983
- });
234
+ printCreateSummary(destinationDir, answers);
984
235
  }
985
236
 
986
237
  async function collectUpdateAnswers(args) {
@@ -1021,115 +272,6 @@ async function collectUpdateAnswers(args) {
1021
272
  };
1022
273
  }
1023
274
 
1024
- function buildCreateAnswers(input) {
1025
- const directory = input.directory.trim();
1026
- const slug = slugify(path.basename(directory));
1027
- const appName = input.appName.trim();
1028
- const databaseName = slug.replace(/-/g, "_") || "asaje_app";
1029
- const swaggerPassword = randomSecret(20);
1030
- const jwtSecret = randomSecret(32);
1031
- const corsAllowedOrigins = [
1032
- `http://localhost:${input.adminPort}`,
1033
- ...splitCsv(input.corsAllowedOrigins || ""),
1034
- ];
1035
-
1036
- return {
1037
- admin: input.seedAdmin
1038
- ? {
1039
- email: input.adminEmail.trim(),
1040
- name: input.adminName.trim(),
1041
- password: input.adminPassword,
1042
- }
1043
- : null,
1044
- adminPort: input.adminPort,
1045
- apiPort: input.apiPort,
1046
- appName,
1047
- aws: {
1048
- accessKeyId: (input.awsAccessKeyId || "").trim(),
1049
- bucket: (input.awsBucket || "").trim(),
1050
- endpointUrl: (input.awsEndpointURL || "").trim(),
1051
- region: (input.awsRegion || "us-east-1").trim(),
1052
- secretAccessKey: input.awsSecretAccessKey || "",
1053
- },
1054
- branch: input.branch,
1055
- brevoApiKey: input.brevoApiKey || "",
1056
- bucketBasePath: (input.bucketBasePath || slug).trim(),
1057
- corsAllowedOrigins,
1058
- databaseName,
1059
- defaultLocale: input.defaultLocale,
1060
- directory,
1061
- github: {
1062
- enabled: Boolean(input.enableGithubWorkflow),
1063
- branchEnvironments: Boolean(input.enableGithubWorkflow)
1064
- ? [
1065
- { branch: (input.productionBranch || "main").trim(), environment: "production" },
1066
- { branch: (input.stagingBranch || "develop").trim(), environment: "staging" },
1067
- ].filter((entry, index, items) => entry.branch && items.findIndex((candidate) => candidate.branch === entry.branch) === index)
1068
- : [],
1069
- },
1070
- includeLanding: Boolean(input.includeLanding),
1071
- includePwa: Boolean(input.includePwa),
1072
- installDependencies: input.installDependencies,
1073
- jwtSecret,
1074
- mailFromEmail: input.mailFromEmail.trim(),
1075
- mailFromName: input.mailFromName.trim(),
1076
- mailProvider: input.mailProvider,
1077
- mailchimpApiKey: input.mailchimpApiKey,
1078
- minio: {
1079
- accessKey: input.minioAccessKey || "minioadmin",
1080
- bucket: input.minioBucket || "boilerplate-files",
1081
- endpoint: input.minioEndpoint || "localhost",
1082
- port: input.minioPort || 9000,
1083
- publicUrl:
1084
- input.minioPublicURL || `${input.minioUseSSL ? "https" : "http"}://${input.minioEndpoint}:${input.minioPort}`,
1085
- secretKey: input.minioSecretKey || "minioadmin",
1086
- useSSL: Boolean(input.minioUseSSL),
1087
- },
1088
- realtimePort: input.realtimePort,
1089
- seedAdmin: input.seedAdmin,
1090
- seedUser: input.seedUser,
1091
- slug,
1092
- smtp: {
1093
- host: (input.smtpHost || "").trim(),
1094
- password: input.smtpPassword || "",
1095
- port: input.smtpPort || 587,
1096
- useSSL: Boolean(input.smtpUseSSL),
1097
- username: (input.smtpUsername || "").trim(),
1098
- },
1099
- standardUser: input.seedUser
1100
- ? {
1101
- email: input.seedUserEmail.trim(),
1102
- name: input.seedUserName.trim(),
1103
- password: input.seedUserPassword,
1104
- }
1105
- : null,
1106
- startInfra: input.startInfra,
1107
- storageProvider: input.storageProvider,
1108
- swaggerPassword,
1109
- swaggerUsername: input.swaggerUsername.trim(),
1110
- template: input.template,
1111
- };
1112
- }
1113
-
1114
- function buildCreateRailwayServices(answers) {
1115
- return DEFAULT_RAILWAY_APP_SERVICE_SPECS.filter((spec) => {
1116
- if (spec.key === "landing") {
1117
- return answers.includeLanding;
1118
- }
1119
- if (spec.key === "pwa") {
1120
- return answers.includePwa;
1121
- }
1122
- return true;
1123
- }).map((spec) => ({
1124
- aliases: [...spec.aliases],
1125
- baseName: spec.baseName,
1126
- directory: spec.directory,
1127
- dockerfile: spec.key === "worker" ? "api/Dockerfile" : `${spec.directory}/Dockerfile`,
1128
- key: spec.key,
1129
- seedImage: spec.key === "admin" || spec.key === "landing" || spec.key === "pwa" ? "nginx:1.29-alpine" : "alpine:3.22",
1130
- }));
1131
- }
1132
-
1133
275
  async function writeProjectConfig(destinationDir, answers) {
1134
276
  const config = {
1135
277
  projectName: answers.appName,
@@ -1189,7 +331,7 @@ async function writeEnvFiles(destinationDir, answers) {
1189
331
  VITE_API_BASE_URL: `http://localhost:${answers.apiPort}/api/v1`,
1190
332
  VITE_API_TIMEOUT_MS: "15000",
1191
333
  VITE_REALTIME_BASE_URL: `http://localhost:${answers.realtimePort}`,
1192
- VITE_REALTIME_DEFAULT_TRANSPORT: "sse",
334
+ VITE_REALTIME_DEFAULT_TRANSPORT: "ws",
1193
335
  VITE_REALTIME_RECONNECT_DELAY_MS: "3000",
1194
336
  }),
1195
337
  );
@@ -1307,111 +449,8 @@ async function writeProjectReadme(destinationDir, answers) {
1307
449
  await fs.writeFile(path.join(destinationDir, "README.md"), buildProjectReadmeContent(answers));
1308
450
  }
1309
451
 
1310
- function buildProjectReadmeContent(answers) {
1311
- const surfaces = [
1312
- "- `admin/`: Vue 3 admin SPA for back-office and internal tooling",
1313
- "- `api/`: Go HTTP API with clean architecture and PostgreSQL",
1314
- "- `realtime-gateway/`: Go realtime transport service for SSE/WebSocket",
1315
- answers.includeLanding ? "- `landing/`: optional public marketing surface" : null,
1316
- answers.includePwa ? "- `pwa/`: optional installable end-user PWA surface" : null,
1317
- ].filter(Boolean).join("\n");
1318
-
1319
- const ciMappings = (answers.github?.branchEnvironments || [])
1320
- .map((entry) => `- \`${entry.branch}\` -> \`${entry.environment}\``)
1321
- .join("\n");
1322
-
1323
- return `# ${answers.appName}
1324
-
1325
- Generated with \`create-asaje-go-vue\` / \`asaje\`.
1326
-
1327
- ## Stack
1328
-
1329
- - Frontend admin: Vue 3, Vite, Pinia, Vue Router, vue-i18n, shadcn-vue
1330
- - API: Go, Gin, GORM, PostgreSQL, JWT
1331
- - Async and realtime: RabbitMQ + realtime gateway (SSE/WebSocket)
1332
- - File storage: ${answers.storageProvider === "aws" ? "AWS S3" : "MinIO / S3-compatible"}
1333
- - Optional mail providers: Mailchimp Transactional, Brevo, SMTP
1334
- - Deployment tooling: Railway + GitHub Actions via \`asaje\`
1335
-
1336
- ## Project Surfaces
1337
-
1338
- ${surfaces}
1339
-
1340
- ## How Things Are Linked
1341
-
1342
- - The admin and PWA call the API through domain API modules, not raw fetches.
1343
- - The API owns business logic and persistence.
1344
- - The API publishes async tasks and realtime events to RabbitMQ.
1345
- - The realtime gateway consumes realtime events and pushes them to browsers.
1346
- - File uploads go through the API and object storage, with stable media URLs exposed by the API.
1347
-
1348
- ## Local Development
1349
-
1350
- Install and start the project:
1351
-
1352
- \`\`\`bash
1353
- docker compose up -d
1354
- npx -p create-asaje-go-vue asaje start .
1355
- \`\`\`
1356
-
1357
- Useful local URLs:
1358
-
1359
- - Admin: http://localhost:${answers.adminPort}
1360
- ${answers.includeLanding ? `- Landing: http://localhost:8088
1361
- ` : ""}${answers.includePwa ? `- PWA: http://localhost:4174
1362
- ` : ""}- API: http://localhost:${answers.apiPort}/api/v1
1363
- - Swagger: http://localhost:${answers.apiPort}/swagger/index.html
1364
- - Realtime gateway: http://localhost:${answers.realtimePort}
1365
-
1366
- ## Asaje Commands
1367
-
1368
- - \`asaje start .\`: run local services
1369
- - \`asaje doctor .\`: check tooling and project readiness
1370
- - \`asaje update .\`: update managed boilerplate files from the template
1371
- - \`asaje sync-project-config .\`: rescan the project and rewrite config manifests
1372
- - \`asaje setup-railway .\`: provision Railway resources and first deploy
1373
- - \`asaje update-railway .\`: reconcile Railway resources, services, and variables
1374
- - \`asaje sync-railway-env .\`: sync only Railway environment variables
1375
- - \`asaje deploy-railway .\`: deploy the current source tree to Railway
1376
- - \`asaje sync-github-workflow .\`: regenerate the GitHub Actions Railway workflow from config
1377
-
1378
- ## Railway And GitHub Actions
1379
-
1380
- - Railway variable mode defaults to \`preserve-remote\`, so existing Railway values are kept unless you explicitly override them in \`asaje.config.json\`.
1381
- ${answers.github?.enabled ? `- GitHub Actions deploy workflow is generated in \`.github/workflows/deploy-railway.yml\`.
1382
- - Branch to environment mapping:
1383
- ${ciMappings}
1384
- - Add \`RAILWAY_TOKEN\` to your GitHub repository secrets before enabling automatic deploys.
1385
- ` : "- No GitHub Actions Railway workflow was generated during bootstrap. You can enable it later in \`asaje.config.json\` and run \`asaje sync-github-workflow .\`.\n"}
1386
- ## Important Files
1387
-
1388
- - \`asaje.config.json\`: project config, Railway config, CI/CD metadata
1389
- - \`asaje.railway.json\`: local manifest of discovered Railway services/resources
1390
- - \`api/notifications.yaml\`: generic notification event/channel templates
1391
- - \`api/crons.yaml\`: worker cron configuration
1392
-
1393
- ## Notes
1394
-
1395
- - This project is designed to stay modular: keep generic infrastructure in the boilerplate, and move product-specific business logic into your app domain.
1396
- - When you add new app surfaces or Dockerfiles, rerun \`asaje sync-project-config .\`.
1397
- `;
1398
- }
1399
-
1400
- function buildReadmeAnswersFromProjectConfig(projectDir, projectConfig) {
1401
- const ports = projectConfig?.ports || {};
1402
- const appServiceSpecs = resolveRailwayAppServiceSpecs(projectConfig);
1403
- const githubConfig = getGithubActionsDeployConfig(projectConfig);
1404
-
1405
- return {
1406
- adminPort: Number(ports.admin || 5173),
1407
- apiPort: Number(ports.api || 8080),
1408
- appName: String(projectConfig?.projectName || path.basename(projectDir)),
1409
- github: githubConfig,
1410
- includeLanding: appServiceSpecs.some((spec) => spec.key === "landing"),
1411
- includePwa: appServiceSpecs.some((spec) => spec.key === "pwa"),
1412
- realtimePort: Number(ports.realtime || 8090),
1413
- storageProvider: String(projectConfig?.services?.storageProvider || "minio"),
1414
- };
452
+ async function writeProjectMakefile(destinationDir, answers) {
453
+ await fs.writeFile(path.join(destinationDir, "Makefile"), buildProjectMakefileContent(answers));
1415
454
  }
1416
455
 
1417
456
  async function writeGithubWorkflowFromProjectConfig(projectDir, projectConfig) {
@@ -1430,147 +469,6 @@ async function writeGithubWorkflowFromProjectConfig(projectDir, projectConfig) {
1430
469
  await fs.writeFile(workflowPath, buildGithubWorkflowContent(answers));
1431
470
  }
1432
471
 
1433
- function getGithubActionsDeployConfig(projectConfig) {
1434
- const deployRailway = projectConfig?.ci?.githubActions?.deployRailway;
1435
- const rawMappings = Array.isArray(deployRailway?.branchEnvironments) ? deployRailway.branchEnvironments : [];
1436
- const branchEnvironments = rawMappings
1437
- .map((entry) => ({
1438
- branch: String(entry?.branch || "").trim(),
1439
- environment: String(entry?.environment || "").trim(),
1440
- }))
1441
- .filter((entry, index, items) => entry.branch && entry.environment && items.findIndex((candidate) => candidate.branch === entry.branch) === index);
1442
-
1443
- return {
1444
- branchEnvironments,
1445
- enabled: Boolean(deployRailway?.enabled) && branchEnvironments.length > 0,
1446
- };
1447
- }
1448
-
1449
- function buildGithubWorkflowContent(answers) {
1450
- const branchEnvironments = answers.github.branchEnvironments;
1451
- const branchList = branchEnvironments.map((entry) => ` - ${entry.branch}`).join("\n");
1452
-
1453
- const branchCase = branchEnvironments
1454
- .map((entry) => ` ${entry.branch}) echo "environment=${entry.environment}" >> "$GITHUB_OUTPUT" ;;
1455
- `)
1456
- .join("");
1457
-
1458
- const managedServices = buildCreateRailwayServices(answers).map((spec) => spec.key);
1459
- const hasLanding = managedServices.includes("landing");
1460
- const hasPwa = managedServices.includes("pwa");
1461
-
1462
- return `name: Deploy Railway
1463
-
1464
- on:
1465
- push:
1466
- branches:
1467
- ${branchList}
1468
- workflow_dispatch:
1469
-
1470
- jobs:
1471
- detect-changes:
1472
- runs-on: ubuntu-latest
1473
- outputs:
1474
- admin: \${{ steps.filter.outputs.admin }}
1475
- api: \${{ steps.filter.outputs.api }}
1476
- config: \${{ steps.filter.outputs.config }}
1477
- landing: \${{ steps.filter.outputs.landing }}
1478
- pwa: \${{ steps.filter.outputs.pwa }}
1479
- realtime: \${{ steps.filter.outputs.realtime }}
1480
- steps:
1481
- - uses: actions/checkout@v4
1482
- with:
1483
- fetch-depth: 0
1484
-
1485
- - uses: dorny/paths-filter@v3
1486
- id: filter
1487
- with:
1488
- filters: |
1489
- api:
1490
- - 'api/**'
1491
- realtime:
1492
- - 'realtime-gateway/**'
1493
- admin:
1494
- - 'admin/**'
1495
- landing:
1496
- - 'landing/**'
1497
- pwa:
1498
- - 'pwa/**'
1499
- config:
1500
- - 'asaje.config.json'
1501
- - 'asaje.railway.json'
1502
- - 'docker-compose.yml'
1503
-
1504
- deploy:
1505
- needs: detect-changes
1506
- runs-on: ubuntu-latest
1507
- steps:
1508
- - uses: actions/checkout@v4
1509
-
1510
- - uses: pnpm/action-setup@v4
1511
- with:
1512
- version: 9
1513
-
1514
- - uses: actions/setup-node@v4
1515
- with:
1516
- node-version: 22
1517
-
1518
- - name: Install Railway CLI
1519
- run: npm install -g @railway/cli
1520
-
1521
- - name: Resolve target environment
1522
- id: target
1523
- shell: bash
1524
- run: |
1525
- branch="\${GITHUB_REF_NAME}"
1526
- case "$branch" in
1527
- ${branchCase} *) echo "Unsupported branch: $branch" >&2; exit 1 ;;
1528
- esac
1529
-
1530
- - name: Sync Railway environment
1531
- if: needs.detect-changes.outputs.config == 'true'
1532
- env:
1533
- RAILWAY_TOKEN: \${{ secrets.RAILWAY_TOKEN }}
1534
- run: npx -p create-asaje-go-vue@latest asaje sync-railway-env . --yes --environment \${{ steps.target.outputs.environment }}
1535
-
1536
- - name: Deploy api
1537
- if: needs.detect-changes.outputs.api == 'true' || needs.detect-changes.outputs.config == 'true'
1538
- env:
1539
- RAILWAY_TOKEN: \${{ secrets.RAILWAY_TOKEN }}
1540
- run: npx -p create-asaje-go-vue@latest asaje deploy-railway . --service api --environment \${{ steps.target.outputs.environment }}
1541
-
1542
- - name: Deploy worker
1543
- if: needs.detect-changes.outputs.api == 'true' || needs.detect-changes.outputs.config == 'true'
1544
- env:
1545
- RAILWAY_TOKEN: \${{ secrets.RAILWAY_TOKEN }}
1546
- run: npx -p create-asaje-go-vue@latest asaje deploy-railway . --service worker --environment \${{ steps.target.outputs.environment }}
1547
-
1548
- - name: Deploy realtime
1549
- if: needs.detect-changes.outputs.realtime == 'true' || needs.detect-changes.outputs.config == 'true'
1550
- env:
1551
- RAILWAY_TOKEN: \${{ secrets.RAILWAY_TOKEN }}
1552
- run: npx -p create-asaje-go-vue@latest asaje deploy-railway . --service realtime --environment \${{ steps.target.outputs.environment }}
1553
-
1554
- - name: Deploy admin
1555
- if: needs.detect-changes.outputs.admin == 'true' || needs.detect-changes.outputs.config == 'true'
1556
- env:
1557
- RAILWAY_TOKEN: \${{ secrets.RAILWAY_TOKEN }}
1558
- run: npx -p create-asaje-go-vue@latest asaje deploy-railway . --service admin --environment \${{ steps.target.outputs.environment }}
1559
- ${hasLanding ? `
1560
- - name: Deploy landing
1561
- if: needs.detect-changes.outputs.landing == 'true' || needs.detect-changes.outputs.config == 'true'
1562
- env:
1563
- RAILWAY_TOKEN: \${{ secrets.RAILWAY_TOKEN }}
1564
- run: npx -p create-asaje-go-vue@latest asaje deploy-railway . --service landing --environment \${{ steps.target.outputs.environment }}
1565
- ` : ""}${hasPwa ? `
1566
- - name: Deploy pwa
1567
- if: needs.detect-changes.outputs.pwa == 'true' || needs.detect-changes.outputs.config == 'true'
1568
- env:
1569
- RAILWAY_TOKEN: \${{ secrets.RAILWAY_TOKEN }}
1570
- run: npx -p create-asaje-go-vue@latest asaje deploy-railway . --service pwa --environment \${{ steps.target.outputs.environment }}
1571
- ` : ""}`;
1572
- }
1573
-
1574
472
  function printCreateSummary(destinationDir, answers) {
1575
473
  console.log(pc.green("\nSetup complete."));
1576
474
  console.log(`- Project: ${pc.bold(answers.appName)}`);
@@ -1609,7 +507,11 @@ async function runStart(argv) {
1609
507
  const projectDir = path.resolve(process.cwd(), answers.directory);
1610
508
 
1611
509
  await ensureProjectStructure(projectDir);
1612
- await ensureEnvFiles(projectDir);
510
+ await ensureEnvFiles(projectDir, {
511
+ onCreated(spec) {
512
+ console.log(pc.yellow(`- Created ${spec.envPath} from ${spec.examplePath}`));
513
+ },
514
+ });
1613
515
 
1614
516
  if (answers.installDependencies) {
1615
517
  console.log(pc.dim("\nInstalling frontend and Go dependencies..."));
@@ -1836,7 +738,7 @@ async function runSyncGithubWorkflow(argv) {
1836
738
  }
1837
739
 
1838
740
  async function runSetupRailway(argv) {
1839
- const args = parseSetupRailwayArgs(argv);
741
+ const args = parseSetupRailwayArgs(argv, { defaultBucket: DEFAULT_RAILWAY_BUCKET });
1840
742
  const answers = await collectSetupRailwayAnswers(args);
1841
743
  const projectDir = path.resolve(process.cwd(), answers.directory);
1842
744
  const projectConfig = await loadProjectConfig(projectDir);
@@ -1992,7 +894,7 @@ async function runUpdateRailway(argv) {
1992
894
  }
1993
895
 
1994
896
  async function runSyncRailwayEnv(argv) {
1995
- const args = parseSetupRailwayArgs(argv);
897
+ const args = parseSetupRailwayArgs(argv, { defaultBucket: DEFAULT_RAILWAY_BUCKET });
1996
898
  const answers = await collectSetupRailwayAnswers(args);
1997
899
  const projectDir = path.resolve(process.cwd(), answers.directory);
1998
900
  const projectConfig = await loadProjectConfig(projectDir);
@@ -2330,95 +1232,6 @@ async function runDestroyRailway(argv) {
2330
1232
  }
2331
1233
  }
2332
1234
 
2333
- function parseDirectoryArgs(argv) {
2334
- return { directory: argv[0] || "." };
2335
- }
2336
-
2337
- function parseSetupRailwayArgs(argv) {
2338
- const options = {
2339
- bucket: DEFAULT_RAILWAY_BUCKET,
2340
- bucketProvided: false,
2341
- directory: ".",
2342
- diff: false,
2343
- dryRun: false,
2344
- environment: undefined,
2345
- services: [],
2346
- yes: false,
2347
- };
2348
- const positionals = [];
2349
-
2350
- for (let index = 0; index < argv.length; index += 1) {
2351
- const arg = argv[index];
2352
-
2353
- if (arg === "--yes" || arg === "-y") {
2354
- options.yes = true;
2355
- continue;
2356
- }
2357
-
2358
- if (arg === "--dry-run") {
2359
- options.dryRun = true;
2360
- continue;
2361
- }
2362
-
2363
- if (arg === "--diff") {
2364
- options.diff = true;
2365
- continue;
2366
- }
2367
-
2368
- if (arg === "--bucket") {
2369
- options.bucket = argv[index + 1] || options.bucket;
2370
- options.bucketProvided = true;
2371
- index += 1;
2372
- continue;
2373
- }
2374
-
2375
- if (arg.startsWith("--bucket=")) {
2376
- options.bucket = arg.split("=")[1] || options.bucket;
2377
- options.bucketProvided = true;
2378
- continue;
2379
- }
2380
-
2381
- if (arg === "--environment" || arg === "-e") {
2382
- options.environment = argv[index + 1] || options.environment;
2383
- index += 1;
2384
- continue;
2385
- }
2386
-
2387
- if (arg.startsWith("--environment=")) {
2388
- options.environment = arg.split("=")[1] || options.environment;
2389
- continue;
2390
- }
2391
-
2392
- if (arg === "--service") {
2393
- options.services.push(argv[index + 1] || "");
2394
- index += 1;
2395
- continue;
2396
- }
2397
-
2398
- if (arg.startsWith("--service=")) {
2399
- options.services.push(arg.split("=")[1] || "");
2400
- continue;
2401
- }
2402
-
2403
- if (arg === "--services") {
2404
- options.services.push(...splitCsv(argv[index + 1] || ""));
2405
- index += 1;
2406
- continue;
2407
- }
2408
-
2409
- if (arg.startsWith("--services=")) {
2410
- options.services.push(...splitCsv(arg.split("=")[1] || ""));
2411
- continue;
2412
- }
2413
-
2414
- positionals.push(arg);
2415
- }
2416
-
2417
- options.directory = positionals[0] || options.directory;
2418
- options.services = [...new Set(options.services.map((service) => service.trim()).filter(Boolean))];
2419
- return options;
2420
- }
2421
-
2422
1235
  function parsePrintRailwayConfigArgs(argv) {
2423
1236
  const options = {
2424
1237
  directory: ".",
@@ -2604,106 +1417,8 @@ function parseSyncGithubWorkflowArgs(argv) {
2604
1417
  for (let index = 0; index < argv.length; index += 1) {
2605
1418
  const arg = argv[index];
2606
1419
 
2607
- if (arg === "--dry-run") {
2608
- options.dryRun = true;
2609
- continue;
2610
- }
2611
-
2612
- positionals.push(arg);
2613
- }
2614
-
2615
- options.directory = positionals[0] || options.directory;
2616
- return options;
2617
- }
2618
-
2619
- function parseSyncReadmeArgs(argv) {
2620
- const options = {
2621
- directory: ".",
2622
- dryRun: false,
2623
- };
2624
- const positionals = [];
2625
-
2626
- for (let index = 0; index < argv.length; index += 1) {
2627
- const arg = argv[index];
2628
-
2629
- if (arg === "--dry-run") {
2630
- options.dryRun = true;
2631
- continue;
2632
- }
2633
-
2634
- positionals.push(arg);
2635
- }
2636
-
2637
- options.directory = positionals[0] || options.directory;
2638
- return options;
2639
- }
2640
-
2641
- function parseDiffRailwayConfigArgs(argv) {
2642
- const options = {
2643
- compareEnvironment: undefined,
2644
- compareFile: undefined,
2645
- directory: ".",
2646
- environment: undefined,
2647
- file: undefined,
2648
- json: false,
2649
- showSecrets: false,
2650
- };
2651
- const positionals = [];
2652
-
2653
- for (let index = 0; index < argv.length; index += 1) {
2654
- const arg = argv[index];
2655
-
2656
- if (arg === "--json") {
2657
- options.json = true;
2658
- continue;
2659
- }
2660
-
2661
- if (arg === "--show-secrets") {
2662
- options.showSecrets = true;
2663
- continue;
2664
- }
2665
-
2666
- if (arg === "--environment" || arg === "-e") {
2667
- options.environment = argv[index + 1] || options.environment;
2668
- index += 1;
2669
- continue;
2670
- }
2671
-
2672
- if (arg.startsWith("--environment=")) {
2673
- options.environment = arg.split("=")[1] || options.environment;
2674
- continue;
2675
- }
2676
-
2677
- if (arg === "--compare-environment") {
2678
- options.compareEnvironment = argv[index + 1] || options.compareEnvironment;
2679
- index += 1;
2680
- continue;
2681
- }
2682
-
2683
- if (arg.startsWith("--compare-environment=")) {
2684
- options.compareEnvironment = arg.split("=")[1] || options.compareEnvironment;
2685
- continue;
2686
- }
2687
-
2688
- if (arg === "--file" || arg === "-f") {
2689
- options.file = argv[index + 1] || options.file;
2690
- index += 1;
2691
- continue;
2692
- }
2693
-
2694
- if (arg.startsWith("--file=")) {
2695
- options.file = arg.split("=")[1] || options.file;
2696
- continue;
2697
- }
2698
-
2699
- if (arg === "--compare-file") {
2700
- options.compareFile = argv[index + 1] || options.compareFile;
2701
- index += 1;
2702
- continue;
2703
- }
2704
-
2705
- if (arg.startsWith("--compare-file=")) {
2706
- options.compareFile = arg.split("=")[1] || options.compareFile;
1420
+ if (arg === "--dry-run") {
1421
+ options.dryRun = true;
2707
1422
  continue;
2708
1423
  }
2709
1424
 
@@ -2711,97 +1426,53 @@ function parseDiffRailwayConfigArgs(argv) {
2711
1426
  }
2712
1427
 
2713
1428
  options.directory = positionals[0] || options.directory;
2714
- if (!options.compareEnvironment && !options.compareFile) {
2715
- throw new Error("diff-railway-config requires --compare-environment <name> or --compare-file <snapshot.json>.");
2716
- }
2717
1429
  return options;
2718
1430
  }
2719
1431
 
2720
- function parseDeployRailwayArgs(argv) {
1432
+ function parseSyncReadmeArgs(argv) {
2721
1433
  const options = {
2722
1434
  directory: ".",
2723
1435
  dryRun: false,
2724
- environment: undefined,
2725
- services: [],
2726
- yes: false,
2727
1436
  };
2728
1437
  const positionals = [];
2729
1438
 
2730
1439
  for (let index = 0; index < argv.length; index += 1) {
2731
1440
  const arg = argv[index];
2732
1441
 
2733
- if (arg === "--yes" || arg === "-y") {
2734
- options.yes = true;
2735
- continue;
2736
- }
2737
-
2738
1442
  if (arg === "--dry-run") {
2739
1443
  options.dryRun = true;
2740
1444
  continue;
2741
1445
  }
2742
1446
 
2743
- if (arg === "--environment" || arg === "-e") {
2744
- options.environment = argv[index + 1] || options.environment;
2745
- index += 1;
2746
- continue;
2747
- }
2748
-
2749
- if (arg.startsWith("--environment=")) {
2750
- options.environment = arg.split("=")[1] || options.environment;
2751
- continue;
2752
- }
2753
-
2754
- if (arg === "--service") {
2755
- options.services.push(argv[index + 1] || "");
2756
- index += 1;
2757
- continue;
2758
- }
2759
-
2760
- if (arg.startsWith("--service=")) {
2761
- options.services.push(arg.split("=")[1] || "");
2762
- continue;
2763
- }
2764
-
2765
- if (arg === "--services") {
2766
- options.services.push(...splitCsv(argv[index + 1] || ""));
2767
- index += 1;
2768
- continue;
2769
- }
2770
-
2771
- if (arg.startsWith("--services=")) {
2772
- options.services.push(...splitCsv(arg.split("=")[1] || ""));
2773
- continue;
2774
- }
2775
-
2776
1447
  positionals.push(arg);
2777
1448
  }
2778
1449
 
2779
1450
  options.directory = positionals[0] || options.directory;
2780
- options.services = [...new Set(options.services.map((service) => service.trim()).filter(Boolean))];
2781
1451
  return options;
2782
1452
  }
2783
1453
 
2784
- function parseDestroyRailwayArgs(argv) {
1454
+ function parseDiffRailwayConfigArgs(argv) {
2785
1455
  const options = {
1456
+ compareEnvironment: undefined,
1457
+ compareFile: undefined,
2786
1458
  directory: ".",
2787
- dryRun: false,
2788
1459
  environment: undefined,
2789
- scope: "environment",
2790
- twoFactorCode: undefined,
2791
- yes: false,
1460
+ file: undefined,
1461
+ json: false,
1462
+ showSecrets: false,
2792
1463
  };
2793
1464
  const positionals = [];
2794
1465
 
2795
1466
  for (let index = 0; index < argv.length; index += 1) {
2796
1467
  const arg = argv[index];
2797
1468
 
2798
- if (arg === "--yes" || arg === "-y") {
2799
- options.yes = true;
1469
+ if (arg === "--json") {
1470
+ options.json = true;
2800
1471
  continue;
2801
1472
  }
2802
1473
 
2803
- if (arg === "--dry-run") {
2804
- options.dryRun = true;
1474
+ if (arg === "--show-secrets") {
1475
+ options.showSecrets = true;
2805
1476
  continue;
2806
1477
  }
2807
1478
 
@@ -2816,25 +1487,36 @@ function parseDestroyRailwayArgs(argv) {
2816
1487
  continue;
2817
1488
  }
2818
1489
 
2819
- if (arg === "--scope") {
2820
- options.scope = argv[index + 1] || options.scope;
1490
+ if (arg === "--compare-environment") {
1491
+ options.compareEnvironment = argv[index + 1] || options.compareEnvironment;
1492
+ index += 1;
1493
+ continue;
1494
+ }
1495
+
1496
+ if (arg.startsWith("--compare-environment=")) {
1497
+ options.compareEnvironment = arg.split("=")[1] || options.compareEnvironment;
1498
+ continue;
1499
+ }
1500
+
1501
+ if (arg === "--file" || arg === "-f") {
1502
+ options.file = argv[index + 1] || options.file;
2821
1503
  index += 1;
2822
1504
  continue;
2823
1505
  }
2824
1506
 
2825
- if (arg.startsWith("--scope=")) {
2826
- options.scope = arg.split("=")[1] || options.scope;
1507
+ if (arg.startsWith("--file=")) {
1508
+ options.file = arg.split("=")[1] || options.file;
2827
1509
  continue;
2828
1510
  }
2829
1511
 
2830
- if (arg === "--2fa-code") {
2831
- options.twoFactorCode = argv[index + 1] || options.twoFactorCode;
1512
+ if (arg === "--compare-file") {
1513
+ options.compareFile = argv[index + 1] || options.compareFile;
2832
1514
  index += 1;
2833
1515
  continue;
2834
1516
  }
2835
1517
 
2836
- if (arg.startsWith("--2fa-code=")) {
2837
- options.twoFactorCode = arg.split("=")[1] || options.twoFactorCode;
1518
+ if (arg.startsWith("--compare-file=")) {
1519
+ options.compareFile = arg.split("=")[1] || options.compareFile;
2838
1520
  continue;
2839
1521
  }
2840
1522
 
@@ -2842,10 +1524,9 @@ function parseDestroyRailwayArgs(argv) {
2842
1524
  }
2843
1525
 
2844
1526
  options.directory = positionals[0] || options.directory;
2845
- if (!["environment", "project"].includes(options.scope)) {
2846
- throw new Error("--scope must be either 'environment' or 'project'");
1527
+ if (!options.compareEnvironment && !options.compareFile) {
1528
+ throw new Error("diff-railway-config requires --compare-environment <name> or --compare-file <snapshot.json>.");
2847
1529
  }
2848
-
2849
1530
  return options;
2850
1531
  }
2851
1532
 
@@ -3107,27 +1788,16 @@ async function ensureRailwayEnvironmentLinked(projectDir, environment) {
3107
1788
  }
3108
1789
 
3109
1790
  async function readRailwayManifest(projectDir) {
3110
- const manifestPath = path.join(projectDir, RAILWAY_MANIFEST_FILENAME);
3111
- if (!(await fs.pathExists(manifestPath))) {
3112
- return {
3113
- appServices: {},
3114
- bucket: DEFAULT_RAILWAY_BUCKET,
3115
- environmentId: null,
3116
- environmentName: null,
3117
- projectSlug: null,
3118
- projectId: null,
3119
- projectName: null,
3120
- resources: {},
3121
- updatedAt: null,
3122
- };
3123
- }
3124
-
3125
- return fs.readJson(manifestPath);
1791
+ return readRailwayManifestFile(projectDir, {
1792
+ defaultBucket: DEFAULT_RAILWAY_BUCKET,
1793
+ filename: RAILWAY_MANIFEST_FILENAME,
1794
+ });
3126
1795
  }
3127
1796
 
3128
1797
  async function writeRailwayManifest(projectDir, manifest) {
3129
- const manifestPath = path.join(projectDir, RAILWAY_MANIFEST_FILENAME);
3130
- await fs.writeJson(manifestPath, manifest, { spaces: 2 });
1798
+ await writeRailwayManifestFile(projectDir, manifest, {
1799
+ filename: RAILWAY_MANIFEST_FILENAME,
1800
+ });
3131
1801
  }
3132
1802
 
3133
1803
  async function loadRailwayContext(projectDir, environment) {
@@ -3177,55 +1847,11 @@ async function discoverRailwayServices(railwayContext, projectDir) {
3177
1847
  return cliServices;
3178
1848
  }
3179
1849
 
3180
- const auth = getRailwayApiAuth();
3181
- if (!auth || !railwayContext.projectId) {
1850
+ if (!railwayContext.projectId) {
3182
1851
  return [];
3183
1852
  }
3184
1853
 
3185
- try {
3186
- const response = await fetch(RAILWAY_GRAPHQL_ENDPOINT, {
3187
- body: JSON.stringify({
3188
- query: `query SetupRailwayServices($projectId: String!) {
3189
- project(id: $projectId) {
3190
- services {
3191
- edges {
3192
- node {
3193
- id
3194
- name
3195
- icon
3196
- }
3197
- }
3198
- }
3199
- }
3200
- }`,
3201
- variables: {
3202
- projectId: railwayContext.projectId,
3203
- },
3204
- }),
3205
- headers: {
3206
- ...auth.headers,
3207
- "Content-Type": "application/json",
3208
- },
3209
- method: "POST",
3210
- });
3211
-
3212
- if (!response.ok) {
3213
- return [];
3214
- }
3215
-
3216
- const payload = await response.json();
3217
- const nodes = payload?.data?.project?.services?.edges || [];
3218
- return nodes
3219
- .map((edge) => edge?.node)
3220
- .filter(Boolean)
3221
- .map((service) => ({
3222
- icon: typeof service.icon === "string" ? service.icon : null,
3223
- id: typeof service.id === "string" ? service.id : null,
3224
- name: typeof service.name === "string" ? service.name : null,
3225
- }));
3226
- } catch {
3227
- return [];
3228
- }
1854
+ return fetchRailwayProjectServices(railwayContext.projectId);
3229
1855
  }
3230
1856
 
3231
1857
  async function discoverRailwayServicesViaCli(railwayContext, projectDir) {
@@ -3254,26 +1880,6 @@ async function discoverRailwayServicesViaCli(railwayContext, projectDir) {
3254
1880
  }
3255
1881
  }
3256
1882
 
3257
- function getRailwayApiAuth() {
3258
- if (process.env.RAILWAY_API_TOKEN) {
3259
- return {
3260
- headers: {
3261
- Authorization: `Bearer ${process.env.RAILWAY_API_TOKEN}`,
3262
- },
3263
- };
3264
- }
3265
-
3266
- if (process.env.RAILWAY_TOKEN) {
3267
- return {
3268
- headers: {
3269
- "Project-Access-Token": process.env.RAILWAY_TOKEN,
3270
- },
3271
- };
3272
- }
3273
-
3274
- return null;
3275
- }
3276
-
3277
1883
  async function ensureRailwayResource(config) {
3278
1884
  const manifestEntry = config.manifest.resources?.[config.key];
3279
1885
  const existingService = findRailwayService(config.existingServices, config.aliases, manifestEntry?.serviceName);
@@ -3464,29 +2070,6 @@ function resolveDeployRailwaySpecs(selectedServices, availableSpecs = DEFAULT_RA
3464
2070
  return specs;
3465
2071
  }
3466
2072
 
3467
- function resolveRailwayAppServiceSpecs(projectConfig) {
3468
- const configuredServices = projectConfig?.railway?.services;
3469
- if (!Array.isArray(configuredServices) || configuredServices.length === 0) {
3470
- return DEFAULT_RAILWAY_APP_SERVICE_SPECS.map((spec) => ({ ...spec, aliases: [...spec.aliases] }));
3471
- }
3472
-
3473
- const specs = configuredServices.map((service, index) => normalizeRailwayAppServiceSpec(service, index));
3474
- const seenKeys = new Set();
3475
- for (const spec of specs) {
3476
- if (seenKeys.has(spec.key)) {
3477
- throw new Error(`Duplicate railway.services key \`${spec.key}\`.`);
3478
- }
3479
- seenKeys.add(spec.key);
3480
- }
3481
-
3482
- return specs;
3483
- }
3484
-
3485
- function getRailwayConfig(projectConfig) {
3486
- const railwayConfig = projectConfig?.railway;
3487
- return railwayConfig && typeof railwayConfig === "object" && !Array.isArray(railwayConfig) ? railwayConfig : {};
3488
- }
3489
-
3490
2073
  function listRailwayEnvironmentEntries(projectConfig) {
3491
2074
  const environments = getRailwayConfig(projectConfig).environments;
3492
2075
  if (!environments || typeof environments !== "object" || Array.isArray(environments)) {
@@ -3551,75 +2134,6 @@ function resolveRailwayEnvironmentSelection(projectConfig, requestedEnvironment,
3551
2134
  };
3552
2135
  }
3553
2136
 
3554
- function normalizeRailwayAppServiceSpec(input, index) {
3555
- if (!input || typeof input !== "object" || Array.isArray(input)) {
3556
- throw new Error(`Invalid railway.services entry at index ${index}.`);
3557
- }
3558
-
3559
- const key = slugify(String(input.key || "").trim());
3560
- const directory = String(input.directory || "").trim().replace(/^\.\//, "").replace(/\/+$/g, "");
3561
- const baseName = slugify(String(input.baseName || input.key || path.basename(directory) || "").trim());
3562
- const aliases = [
3563
- key,
3564
- baseName,
3565
- ...(Array.isArray(input.aliases) ? input.aliases : []),
3566
- ]
3567
- .map((value) => normalizeRailwayServiceName(value))
3568
- .filter(Boolean);
3569
-
3570
- if (!key) {
3571
- throw new Error("Each railway.services entry needs a non-empty `key`.");
3572
- }
3573
- if (!directory) {
3574
- throw new Error(`Railway service \`${key}\` needs a non-empty \`directory\`.`);
3575
- }
3576
- if (!baseName) {
3577
- throw new Error(`Railway service \`${key}\` needs a valid \`baseName\` or \`key\`.`);
3578
- }
3579
-
3580
- return {
3581
- aliases: [...new Set(aliases)],
3582
- baseName,
3583
- directory,
3584
- dockerfile: String(input.dockerfile || "").trim() || null,
3585
- key,
3586
- seedImage: String(input.seedImage || (key === "admin" ? "nginx:1.29-alpine" : "alpine:3.22")).trim(),
3587
- serviceName: String(input.serviceName || "").trim() || null,
3588
- };
3589
- }
3590
-
3591
- function resolveRailwayServiceName(spec, projectSlug) {
3592
- return spec.serviceName || buildRailwayAppServiceName(projectSlug, spec.baseName);
3593
- }
3594
-
3595
- function findRailwayAppServiceSpec(appServiceSpecs, key) {
3596
- const exact = appServiceSpecs.find((candidate) => candidate.key === key);
3597
- if (exact) {
3598
- return exact;
3599
- }
3600
-
3601
- const defaultSpec = DEFAULT_RAILWAY_APP_SERVICE_SPECS.find((candidate) => candidate.key === key);
3602
- if (!defaultSpec) {
3603
- return null;
3604
- }
3605
-
3606
- const defaultNames = [defaultSpec.key, defaultSpec.baseName, ...defaultSpec.aliases].map(normalizeRailwayServiceName);
3607
- return appServiceSpecs.find((candidate) => {
3608
- const names = [candidate.key, candidate.baseName, ...candidate.aliases].map(normalizeRailwayServiceName);
3609
- return names.some((name) => defaultNames.includes(name));
3610
- }) || null;
3611
- }
3612
-
3613
- function findRailwayServiceByKey(services, appServiceSpecs, manifest, key) {
3614
- const spec = findRailwayAppServiceSpec(appServiceSpecs, key);
3615
- if (!spec) {
3616
- return null;
3617
- }
3618
-
3619
- const preferredName = manifest.appServices?.[key]?.serviceName || resolveRailwayServiceName(spec, manifest.projectSlug);
3620
- return findRailwayService(services, spec.aliases, preferredName);
3621
- }
3622
-
3623
2137
  function resolveRailwayVariablesMode(projectConfig) {
3624
2138
  const mode = String(getRailwayConfig(projectConfig).variablesMode || "preserve-remote").trim().toLowerCase();
3625
2139
  if (!mode) {
@@ -3731,33 +2245,6 @@ function mergeRailwayServiceVariables(registryEntry, variables) {
3731
2245
  };
3732
2246
  }
3733
2247
 
3734
- function assignManagedRailwayServiceVariables(registryEntry, variables, variablesMode) {
3735
- if (!registryEntry || !registryEntry.name) {
3736
- return;
3737
- }
3738
-
3739
- const nextVariables = {};
3740
- const existingVariables = registryEntry.existingVariables || {};
3741
-
3742
- for (const [key, value] of Object.entries(variables || {})) {
3743
- if (typeof value !== "string" || value.length === 0) {
3744
- continue;
3745
- }
3746
-
3747
- if (
3748
- variablesMode === "preserve-remote" &&
3749
- Object.prototype.hasOwnProperty.call(existingVariables, key)
3750
- ) {
3751
- nextVariables[key] = existingVariables[key];
3752
- continue;
3753
- }
3754
-
3755
- nextVariables[key] = value;
3756
- }
3757
-
3758
- mergeRailwayServiceVariables(registryEntry, nextVariables);
3759
- }
3760
-
3761
2248
  async function hydrateRailwayServiceVariables(projectDir, environment, serviceRegistry) {
3762
2249
  await Promise.all(
3763
2250
  Object.values(serviceRegistry)
@@ -3829,7 +2316,7 @@ async function resolveRailwayVariablePlan(config) {
3829
2316
  const declaredVariables = resolveDeclaredRailwayVariables(config.projectConfig, config.environmentSelection);
3830
2317
  validateDeclaredRailwayVariableTargets(declaredVariables, serviceRegistry);
3831
2318
 
3832
- if (variablesMode === "preserve-remote") {
2319
+ if (variablesMode === "preserve-remote" || variablesMode === "sync-managed") {
3833
2320
  await hydrateRailwayServiceVariables(config.projectDir, config.railwayContext.environmentRef, serviceRegistry);
3834
2321
  }
3835
2322
 
@@ -3880,8 +2367,9 @@ async function resolveRailwayVariablePlan(config) {
3880
2367
  if (infra.objectStorage?.name) {
3881
2368
  Object.assign(variables, buildObjectStorageVariables(infra.objectStorage.name));
3882
2369
  }
3883
- if (appServices.admin?.name) {
3884
- variables.CORS_ALLOWED_ORIGINS = `https://${railwayReference(appServices.admin.name, "RAILWAY_PUBLIC_DOMAIN")}`;
2370
+ const browserOrigins = buildRailwayBrowserOrigins(appServices);
2371
+ if (browserOrigins) {
2372
+ variables.CORS_ALLOWED_ORIGINS = browserOrigins;
3885
2373
  }
3886
2374
  variables.PUBLIC_API_BASE_URL = `https://${railwayReference(appServices.api.name, "RAILWAY_PUBLIC_DOMAIN")}`;
3887
2375
  assignManagedRailwayServiceVariables(serviceRegistry.api, variables, variablesMode);
@@ -3896,8 +2384,9 @@ async function resolveRailwayVariablePlan(config) {
3896
2384
  if (appServices.api?.name) {
3897
2385
  variables.JWT_SECRET = railwayReference(appServices.api.name, "JWT_SECRET");
3898
2386
  }
3899
- if (appServices.admin?.name) {
3900
- variables.CORS_ALLOWED_ORIGINS = `https://${railwayReference(appServices.admin?.name || "admin", "RAILWAY_PUBLIC_DOMAIN")}`;
2387
+ const browserOrigins = buildRailwayBrowserOrigins(appServices);
2388
+ if (browserOrigins) {
2389
+ variables.CORS_ALLOWED_ORIGINS = browserOrigins;
3901
2390
  }
3902
2391
  assignManagedRailwayServiceVariables(serviceRegistry.realtime, variables, variablesMode);
3903
2392
  }
@@ -4109,7 +2598,7 @@ function buildAdminDefaults(localEnv) {
4109
2598
  return {
4110
2599
  VITE_API_TIMEOUT_MS: localEnv.admin.VITE_API_TIMEOUT_MS || "15000",
4111
2600
  VITE_APP_NAME: localEnv.admin.VITE_APP_NAME || "Admin Blueprint",
4112
- VITE_REALTIME_DEFAULT_TRANSPORT: localEnv.admin.VITE_REALTIME_DEFAULT_TRANSPORT || "sse",
2601
+ VITE_REALTIME_DEFAULT_TRANSPORT: localEnv.admin.VITE_REALTIME_DEFAULT_TRANSPORT || "ws",
4113
2602
  VITE_REALTIME_RECONNECT_DELAY_MS: localEnv.admin.VITE_REALTIME_RECONNECT_DELAY_MS || "3000",
4114
2603
  };
4115
2604
  }
@@ -4137,14 +2626,6 @@ function sanitizeSecret(value, placeholder) {
4137
2626
  return normalized;
4138
2627
  }
4139
2628
 
4140
- async function tryReadEnvFile(filePath) {
4141
- if (!(await fs.pathExists(filePath))) {
4142
- return {};
4143
- }
4144
-
4145
- return readEnvFile(filePath);
4146
- }
4147
-
4148
2629
  function buildObjectStorageVariables(serviceName) {
4149
2630
  return {
4150
2631
  MINIO_ACCESS_KEY: railwayReference(serviceName, "MINIO_ROOT_USER"),
@@ -4209,185 +2690,42 @@ async function applyRailwayVariables(config) {
4209
2690
  });
4210
2691
  }
4211
2692
 
4212
- function buildRailwayVariableDiff(currentVariables, nextVariables, options = {}) {
4213
- const changes = [];
4214
- const includeRemoved = options.includeRemoved ?? true;
4215
- const keys = [...new Set([...Object.keys(nextVariables || {}), ...(includeRemoved ? Object.keys(currentVariables || {}) : [])])].sort();
4216
- for (const key of keys) {
4217
- const rawCurrent = currentVariables?.[key];
4218
- const rawNext = nextVariables?.[key];
4219
- const currentValue = typeof rawCurrent === "string" ? rawCurrent : rawCurrent === undefined || rawCurrent === null ? undefined : String(rawCurrent);
4220
- const nextValue = typeof rawNext === "string" ? rawNext : rawNext === undefined || rawNext === null ? undefined : String(rawNext);
4221
- if (nextValue === undefined && currentValue === undefined) {
4222
- continue;
4223
- }
4224
-
4225
- let status = "unchanged";
4226
- if (currentValue === undefined) {
4227
- status = "added";
4228
- } else if (nextValue === undefined) {
4229
- status = "removed";
4230
- } else if (currentValue !== nextValue) {
4231
- status = "changed";
4232
- }
4233
-
4234
- changes.push({ currentValue, key, nextValue, status });
4235
- }
4236
-
4237
- return {
4238
- added: changes.filter((item) => item.status === "added"),
4239
- changed: changes.filter((item) => item.status === "changed"),
4240
- removed: changes.filter((item) => item.status === "removed"),
4241
- unchanged: changes.filter((item) => item.status === "unchanged"),
4242
- };
4243
- }
4244
-
4245
2693
  function printRailwayVariableDiff(serviceName, diff) {
4246
2694
  const counts = [`${diff.added.length} added`, `${diff.changed.length} changed`, `${diff.unchanged.length} unchanged`].join(", ");
4247
2695
  console.log(`- ${pc.cyan(serviceName)} diff: ${counts}`);
4248
2696
 
4249
2697
  for (const item of [...diff.added, ...diff.changed]) {
4250
- const before = item.currentValue === undefined ? "<unset>" : formatRailwayVariableValue(item.key, item.currentValue);
4251
- const after = formatRailwayVariableValue(item.key, item.nextValue);
4252
- console.log(` ${item.key}: ${before} -> ${after}`);
4253
- }
4254
- }
4255
-
4256
- function sanitizeVariablesForOutput(variables, showSecrets = false) {
4257
- const sanitized = {};
4258
- for (const [key, value] of Object.entries(variables)) {
4259
- sanitized[key] = formatRailwayVariableValue(key, value, showSecrets);
4260
- }
4261
- return sanitized;
4262
- }
4263
-
4264
- function formatRailwayVariableValue(key, value, showSecrets = false) {
4265
- if (value === undefined) {
4266
- return "<unset>";
4267
- }
4268
-
4269
- const normalizedKey = String(key || "").toUpperCase();
4270
- if (!showSecrets && /(SECRET|PASSWORD|TOKEN|API_KEY|ACCESS_KEY|SECRET_KEY|ERLANG_COOKIE)/.test(normalizedKey)) {
4271
- return redactRailwayVariableValue(value);
4272
- }
4273
-
4274
- return String(value);
4275
- }
4276
-
4277
- function redactRailwayVariableValue(value) {
4278
- const textValue = String(value || "");
4279
- if (textValue.length <= 8) {
4280
- return "[redacted]";
4281
- }
4282
- return `${textValue.slice(0, 3)}...[redacted]...${textValue.slice(-2)}`;
4283
- }
4284
-
4285
- function printResolvedRailwayConfig(payload) {
4286
- console.log(pc.bold("\nRailway config"));
4287
- console.log(`- Directory: ${pc.bold(payload.directory)}`);
4288
- console.log(`- Variables mode: ${pc.bold(payload.variablesMode)}`);
4289
- console.log(`- Environment key: ${pc.bold(payload.environment.configKey || "<none>")}`);
4290
- console.log(`- Railway environment: ${pc.bold(payload.environment.railwayEnvironment || "<linked default>")}`);
4291
-
4292
- console.log(pc.bold("\nServices"));
4293
- for (const service of payload.services) {
4294
- console.log(`- ${pc.bold(service.key)} -> ${service.serviceName}`);
4295
- console.log(` directory: ${service.directory}`);
4296
- if (service.dockerfile) {
4297
- console.log(` dockerfile: ${service.dockerfile}`);
4298
- }
4299
- console.log(` aliases: ${service.aliases.join(", ")}`);
4300
- const variableEntries = Object.entries(service.variables || {});
4301
- if (variableEntries.length === 0) {
4302
- console.log(" variables: <none>");
4303
- continue;
4304
- }
4305
- console.log(" variables:");
4306
- for (const [key, value] of variableEntries.sort(([left], [right]) => left.localeCompare(right))) {
4307
- console.log(` ${key}=${value}`);
4308
- }
4309
- }
4310
- }
4311
-
4312
- function buildRailwayConfigExportFilename(payload) {
4313
- const envSuffix = slugify(payload.environment.configKey || payload.environment.railwayEnvironment || "default") || "default";
4314
- return path.join(payload.directory, `.railway-config.${envSuffix}.json`);
4315
- }
4316
-
4317
- async function readRailwayConfigSnapshot(filePath) {
4318
- const absolutePath = path.resolve(process.cwd(), filePath);
4319
- if (!(await fs.pathExists(absolutePath))) {
4320
- throw new Error(`Railway config snapshot not found: ${absolutePath}`);
4321
- }
4322
-
4323
- const payload = await fs.readJson(absolutePath);
4324
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
4325
- throw new Error(`Railway config snapshot is invalid: ${absolutePath}`);
4326
- }
4327
-
4328
- if (!Array.isArray(payload.services)) {
4329
- throw new Error(`Railway config snapshot is missing a services array: ${absolutePath}`);
4330
- }
4331
-
4332
- return payload;
4333
- }
4334
-
4335
- function assertRailwaySnapshotImportable(snapshot) {
4336
- const redactedEntries = [];
4337
- for (const service of snapshot.services || []) {
4338
- for (const [key, value] of Object.entries(service.variables || {})) {
4339
- if (String(value || "").includes("[redacted]")) {
4340
- redactedEntries.push(`${service.key}.${key}`);
4341
- }
4342
- }
4343
- }
4344
-
4345
- if (redactedEntries.length > 0) {
4346
- throw new Error(
4347
- `Snapshot contains redacted values and cannot be imported safely. Re-export with --show-secrets. Problem keys: ${redactedEntries.join(", ")}`,
4348
- );
4349
- }
4350
- }
4351
-
4352
- function buildRailwayConfigSnapshotDiff(left, right) {
4353
- const leftServices = new Map((left.services || []).map((service) => [service.key, service]));
4354
- const rightServices = new Map((right.services || []).map((service) => [service.key, service]));
4355
- const serviceKeys = [...new Set([...leftServices.keys(), ...rightServices.keys()])].sort();
4356
-
4357
- const services = serviceKeys.map((key) => {
4358
- const leftService = leftServices.get(key) || null;
4359
- const rightService = rightServices.get(key) || null;
4360
- return {
4361
- key,
4362
- metadata: {
4363
- directory: buildRailwayFieldDiff(leftService?.directory, rightService?.directory),
4364
- dockerfile: buildRailwayFieldDiff(leftService?.dockerfile, rightService?.dockerfile),
4365
- serviceName: buildRailwayFieldDiff(leftService?.serviceName, rightService?.serviceName),
4366
- },
4367
- status: !leftService ? "added" : !rightService ? "removed" : "present",
4368
- variables: buildRailwayVariableDiff(leftService?.variables || {}, rightService?.variables || {}),
4369
- };
4370
- });
4371
-
4372
- return {
4373
- left: {
4374
- directory: left.directory,
4375
- environment: left.environment,
4376
- },
4377
- right: {
4378
- directory: right.directory,
4379
- environment: right.environment,
4380
- },
4381
- services,
4382
- };
2698
+ const before = item.currentValue === undefined ? "<unset>" : formatRailwayVariableValue(item.key, item.currentValue);
2699
+ const after = formatRailwayVariableValue(item.key, item.nextValue);
2700
+ console.log(` ${item.key}: ${before} -> ${after}`);
2701
+ }
4383
2702
  }
4384
2703
 
4385
- function buildRailwayFieldDiff(leftValue, rightValue) {
4386
- return {
4387
- left: leftValue ?? null,
4388
- right: rightValue ?? null,
4389
- status: leftValue === rightValue ? "unchanged" : leftValue === undefined ? "added" : rightValue === undefined ? "removed" : "changed",
4390
- };
2704
+ function printResolvedRailwayConfig(payload) {
2705
+ console.log(pc.bold("\nRailway config"));
2706
+ console.log(`- Directory: ${pc.bold(payload.directory)}`);
2707
+ console.log(`- Variables mode: ${pc.bold(payload.variablesMode)}`);
2708
+ console.log(`- Environment key: ${pc.bold(payload.environment.configKey || "<none>")}`);
2709
+ console.log(`- Railway environment: ${pc.bold(payload.environment.railwayEnvironment || "<linked default>")}`);
2710
+
2711
+ console.log(pc.bold("\nServices"));
2712
+ for (const service of payload.services) {
2713
+ console.log(`- ${pc.bold(service.key)} -> ${service.serviceName}`);
2714
+ console.log(` directory: ${service.directory}`);
2715
+ if (service.dockerfile) {
2716
+ console.log(` dockerfile: ${service.dockerfile}`);
2717
+ }
2718
+ console.log(` aliases: ${service.aliases.join(", ")}`);
2719
+ const variableEntries = Object.entries(service.variables || {});
2720
+ if (variableEntries.length === 0) {
2721
+ console.log(" variables: <none>");
2722
+ continue;
2723
+ }
2724
+ console.log(" variables:");
2725
+ for (const [key, value] of variableEntries.sort(([left], [right]) => left.localeCompare(right))) {
2726
+ console.log(` ${key}=${value}`);
2727
+ }
2728
+ }
4391
2729
  }
4392
2730
 
4393
2731
  function printRailwayConfigSnapshotDiff(diff) {
@@ -4411,33 +2749,6 @@ function printRailwayConfigSnapshotDiff(diff) {
4411
2749
  }
4412
2750
  }
4413
2751
 
4414
- function sanitizeRailwayConfigSnapshotDiff(diff, showSecrets) {
4415
- if (showSecrets) {
4416
- return diff;
4417
- }
4418
-
4419
- return {
4420
- ...diff,
4421
- services: diff.services.map((service) => ({
4422
- ...service,
4423
- variables: {
4424
- added: service.variables.added.map(sanitizeRailwayDiffEntry),
4425
- changed: service.variables.changed.map(sanitizeRailwayDiffEntry),
4426
- removed: service.variables.removed.map(sanitizeRailwayDiffEntry),
4427
- unchanged: service.variables.unchanged.map(sanitizeRailwayDiffEntry),
4428
- },
4429
- })),
4430
- };
4431
- }
4432
-
4433
- function sanitizeRailwayDiffEntry(entry) {
4434
- return {
4435
- ...entry,
4436
- currentValue: formatRailwayVariableValue(entry.key, entry.currentValue),
4437
- nextValue: formatRailwayVariableValue(entry.key, entry.nextValue),
4438
- };
4439
- }
4440
-
4441
2752
  function formatRailwayDiffSide(side) {
4442
2753
  const env = side.environment?.configKey || side.environment?.railwayEnvironment || "default";
4443
2754
  return `${side.directory || "<snapshot>"} (${env})`;
@@ -4510,80 +2821,6 @@ async function waitForCreatedRailwayService(config) {
4510
2821
  );
4511
2822
  }
4512
2823
 
4513
- function findRailwayService(services, aliases, preferredName) {
4514
- if (preferredName) {
4515
- const exact = services.find(
4516
- (service) => normalizeRailwayServiceName(service.name) === normalizeRailwayServiceName(preferredName),
4517
- );
4518
- if (exact) {
4519
- return exact;
4520
- }
4521
- }
4522
-
4523
- const normalizedAliases = aliases.map(normalizeRailwayServiceName);
4524
- return services.find((service) => {
4525
- const normalizedName = normalizeRailwayServiceName(service.name);
4526
- return normalizedAliases.some((alias) => normalizedName === alias || normalizedName.endsWith(`-${alias}`));
4527
- });
4528
- }
4529
-
4530
- function normalizeRailwayServiceName(value) {
4531
- return String(value || "")
4532
- .trim()
4533
- .toLowerCase()
4534
- .replace(/[^a-z0-9]+/g, "-")
4535
- .replace(/^-+|-+$/g, "");
4536
- }
4537
-
4538
- function normalizeRailwayServices(services) {
4539
- const seen = new Set();
4540
- const normalized = [];
4541
-
4542
- for (const service of services) {
4543
- const name = pickFirstString([service.name, service.serviceName]);
4544
- if (!name) {
4545
- continue;
4546
- }
4547
-
4548
- const id = pickFirstString([service.id, service.serviceId]);
4549
- const key = `${normalizeRailwayServiceName(name)}:${id || ""}`;
4550
- if (seen.has(key)) {
4551
- continue;
4552
- }
4553
-
4554
- seen.add(key);
4555
- normalized.push({ id, name });
4556
- }
4557
-
4558
- return normalized;
4559
- }
4560
-
4561
- function findCreatedRailwayService(config) {
4562
- const beforeServices = normalizeRailwayServices(config.beforeServices);
4563
- const afterServices = normalizeRailwayServices(config.servicesAfter);
4564
- const beforeKeys = new Set(beforeServices.map(createRailwayServiceIdentity));
4565
- const newServices = afterServices.filter((service) => !beforeKeys.has(createRailwayServiceIdentity(service)));
4566
-
4567
- if (newServices.length === 1) {
4568
- return newServices[0];
4569
- }
4570
-
4571
- const aliasMatch = findRailwayService(newServices, config.aliases, config.manifestServiceName);
4572
- if (aliasMatch) {
4573
- return aliasMatch;
4574
- }
4575
-
4576
- return null;
4577
- }
4578
-
4579
- function createRailwayServiceIdentity(service) {
4580
- if (service?.id) {
4581
- return `id:${service.id}`;
4582
- }
4583
-
4584
- return `name:${normalizeRailwayServiceName(service?.name)}`;
4585
- }
4586
-
4587
2824
  function normalizeRailwayVariables(input) {
4588
2825
  const normalized = {};
4589
2826
 
@@ -4604,27 +2841,6 @@ function normalizeRailwayVariables(input) {
4604
2841
  return normalized;
4605
2842
  }
4606
2843
 
4607
- function updateRailwayManifestAppServices(manifest, services, appServiceSpecs, projectSlug) {
4608
- manifest.appServices ||= {};
4609
-
4610
- for (const spec of appServiceSpecs) {
4611
- const key = spec.key;
4612
- const service = findRailwayService(
4613
- services,
4614
- spec.aliases,
4615
- manifest.appServices[key]?.serviceName || resolveRailwayServiceName(spec, projectSlug || manifest.projectSlug),
4616
- );
4617
- if (!service?.name) {
4618
- continue;
4619
- }
4620
-
4621
- manifest.appServices[key] = {
4622
- serviceId: service.id || manifest.appServices[key]?.serviceId || null,
4623
- serviceName: service.name,
4624
- };
4625
- }
4626
- }
4627
-
4628
2844
  function extractRailwayServiceCandidates(input) {
4629
2845
  const candidates = [];
4630
2846
 
@@ -4795,302 +3011,36 @@ function printRailwaySetupSummary(config) {
4795
3011
  }
4796
3012
 
4797
3013
  if (!getRailwayApiAuth()) {
4798
- console.log(pc.yellow("\nNote: Set RAILWAY_API_TOKEN or RAILWAY_TOKEN to let future runs verify remote services before provisioning."));
4799
- }
4800
- }
4801
-
4802
- function printRailwayDeploySummary(config) {
4803
- console.log(pc.bold("\nRailway deploy"));
4804
- console.log(`- Directory: ${pc.bold(config.projectDir)}`);
4805
- if (config.railwayContext.projectName || config.railwayContext.projectId) {
4806
- console.log(`- Project: ${pc.bold(config.railwayContext.projectName || config.railwayContext.projectId)}`);
4807
- }
4808
- if (config.railwayContext.environmentName || config.railwayContext.environmentId) {
4809
- console.log(`- Environment: ${pc.bold(config.railwayContext.environmentName || config.railwayContext.environmentId)}`);
4810
- }
4811
- console.log(`- Default managed services: ${pc.bold("api, worker, realtime-gateway, admin")}`);
4812
- console.log(`- Services: ${pc.bold(config.selectedServices.join(", "))}`);
4813
-
4814
- console.log(pc.bold("\nDeployments"));
4815
- if (config.deploySummary.length === 0) {
4816
- console.log("- No application deployments were triggered");
4817
- } else {
4818
- for (const item of config.deploySummary) {
4819
- console.log(`- ${pc.bold(item.serviceName)}: ${item.status} from ${item.directory}/`);
4820
- }
4821
- }
4822
-
4823
- if (config.dryRun) {
4824
- console.log(`- Dry run only, ${pc.bold(RAILWAY_MANIFEST_FILENAME)} was not written`);
4825
- } else {
4826
- console.log(`- Manifest written to ${pc.bold(RAILWAY_MANIFEST_FILENAME)} for future runs`);
4827
- }
4828
- }
4829
-
4830
- function parseStartArgs(argv) {
4831
- const options = {
4832
- directory: ".",
4833
- installDependencies: undefined,
4834
- profile: undefined,
4835
- startInfra: undefined,
4836
- yes: false,
4837
- };
4838
- const positionals = [];
4839
-
4840
- for (let index = 0; index < argv.length; index += 1) {
4841
- const arg = argv[index];
4842
-
4843
- if (arg === "--yes" || arg === "-y") {
4844
- options.yes = true;
4845
- continue;
4846
- }
4847
-
4848
- if (arg === "--install") {
4849
- options.installDependencies = true;
4850
- continue;
4851
- }
4852
-
4853
- if (arg === "--skip-install") {
4854
- options.installDependencies = false;
4855
- continue;
4856
- }
4857
-
4858
- if (arg === "--start-infra") {
4859
- options.startInfra = true;
4860
- continue;
4861
- }
4862
-
4863
- if (arg === "--skip-infra") {
4864
- options.startInfra = false;
4865
- continue;
4866
- }
4867
-
4868
- if (arg === "--profile") {
4869
- options.profile = argv[index + 1] || options.profile;
4870
- index += 1;
4871
- continue;
4872
- }
4873
-
4874
- if (arg.startsWith("--profile=")) {
4875
- options.profile = arg.split("=")[1] || options.profile;
4876
- continue;
4877
- }
4878
-
4879
- if (arg === "--skip-api") {
4880
- options.api = false;
4881
- continue;
4882
- }
4883
-
4884
- if (arg === "--skip-worker") {
4885
- options.worker = false;
4886
- continue;
4887
- }
4888
-
4889
- if (arg === "--skip-realtime") {
4890
- options.realtime = false;
4891
- continue;
4892
- }
4893
-
4894
- if (arg === "--skip-admin") {
4895
- options.admin = false;
4896
- continue;
4897
- }
4898
-
4899
- if (arg === "--skip-landing") {
4900
- options.landing = false;
4901
- continue;
4902
- }
4903
-
4904
- if (arg === "--skip-pwa") {
4905
- options.pwa = false;
4906
- continue;
4907
- }
4908
-
4909
- positionals.push(arg);
4910
- }
4911
-
4912
- options.directory = positionals[0] || options.directory;
4913
- return options;
4914
- }
4915
-
4916
- async function collectStartAnswers(args) {
4917
- if (args.yes) {
4918
- return {
4919
- directory: args.directory,
4920
- installDependencies: args.installDependencies ?? false,
4921
- profile: args.profile || inferProfileFromArgs(args) || "full",
4922
- selectedServices: buildSelectedServices(args),
4923
- startInfra: args.startInfra ?? true,
4924
- };
4925
- }
4926
-
4927
- const directory = await prompt(
4928
- text({
4929
- defaultValue: args.directory,
4930
- message: "Project directory to start?",
4931
- placeholder: ".",
4932
- validate(value) {
4933
- return value.trim().length === 0 ? "Project directory is required" : undefined;
4934
- },
4935
- }),
4936
- );
4937
-
4938
- const installDependencies = await prompt(
4939
- confirm({
4940
- initialValue: args.installDependencies ?? false,
4941
- message: "Install dependencies before start?",
4942
- }),
4943
- );
4944
-
4945
- const startInfra = await prompt(
4946
- confirm({
4947
- initialValue: args.startInfra ?? true,
4948
- message: "Start local Docker services?",
4949
- }),
4950
- );
4951
-
4952
- const profile = await prompt(
4953
- select({
4954
- initialValue: inferProfileFromArgs(args) || "full",
4955
- message: "Startup profile?",
4956
- options: [
4957
- { label: "Full stack", value: "full" },
4958
- { label: "Backend only", value: "backend-only" },
4959
- { label: "Frontend only", value: "frontend-only" },
4960
- { label: "Custom", value: "custom" },
4961
- ],
4962
- }),
4963
- );
4964
-
4965
- if (profile !== "custom") {
4966
- return {
4967
- directory,
4968
- installDependencies,
4969
- profile,
4970
- selectedServices: profileToServices(profile),
4971
- startInfra,
4972
- };
4973
- }
4974
-
4975
- const api = await prompt(
4976
- confirm({
4977
- initialValue: args.api ?? true,
4978
- message: "Start API server?",
4979
- }),
4980
- );
4981
- const worker = await prompt(
4982
- confirm({
4983
- initialValue: args.worker ?? true,
4984
- message: "Start API worker?",
4985
- }),
4986
- );
4987
- const realtime = await prompt(
4988
- confirm({
4989
- initialValue: args.realtime ?? true,
4990
- message: "Start realtime gateway?",
4991
- }),
4992
- );
4993
- const admin = await prompt(
4994
- confirm({
4995
- initialValue: args.admin ?? true,
4996
- message: "Start admin frontend?",
4997
- }),
4998
- );
4999
- const landing = await prompt(
5000
- confirm({
5001
- initialValue: args.landing ?? true,
5002
- message: "Start landing surface?",
5003
- }),
5004
- );
5005
- const pwa = await prompt(
5006
- confirm({
5007
- initialValue: args.pwa ?? true,
5008
- message: "Start PWA surface?",
5009
- }),
5010
- );
5011
-
5012
- return {
5013
- directory,
5014
- installDependencies,
5015
- profile: "custom",
5016
- selectedServices: [api && "api", worker && "worker", realtime && "realtime", admin && "admin", landing && "landing", pwa && "pwa"].filter(Boolean),
5017
- startInfra,
5018
- };
5019
- }
5020
-
5021
- function buildSelectedServices(args) {
5022
- if (args.profile) {
5023
- return applyServiceOverrides(profileToServices(args.profile), args);
5024
- }
5025
-
5026
- return applyServiceOverrides(profileToServices("full"), args);
5027
- }
5028
-
5029
- function inferProfileFromArgs(args) {
5030
- const selected = [
5031
- args.api !== false && "api",
5032
- args.worker !== false && "worker",
5033
- args.realtime !== false && "realtime",
5034
- args.admin !== false && "admin",
5035
- args.landing !== false && "landing",
5036
- args.pwa !== false && "pwa",
5037
- ].filter(Boolean);
5038
-
5039
- if (selected.length === 6) {
5040
- return "full";
5041
- }
5042
-
5043
- if (arraysEqual(selected, profileToServices("backend-only"))) {
5044
- return "backend-only";
5045
- }
5046
-
5047
- if (arraysEqual(selected, profileToServices("frontend-only"))) {
5048
- return "frontend-only";
5049
- }
5050
-
5051
- return undefined;
5052
- }
5053
-
5054
- function profileToServices(profile) {
5055
- switch (profile) {
5056
- case "backend-only":
5057
- return ["api", "worker", "realtime"];
5058
- case "frontend-only":
5059
- return ["admin", "landing", "pwa"];
5060
- case "custom":
5061
- return [];
5062
- case "full":
5063
- default:
5064
- return ["api", "worker", "realtime", "admin", "landing", "pwa"];
5065
- }
5066
- }
5067
-
5068
- function applyServiceOverrides(services, args) {
5069
- return services.filter((service) => args[service] !== false);
5070
- }
5071
-
5072
- function arraysEqual(left, right) {
5073
- return left.length === right.length && left.every((value, index) => value === right[index]);
3014
+ console.log(pc.yellow("\nNote: Set RAILWAY_API_TOKEN or RAILWAY_TOKEN to let future runs verify remote services before provisioning."));
3015
+ }
5074
3016
  }
5075
3017
 
5076
- async function ensureProjectStructure(projectDir) {
5077
- const requiredPaths = ["admin", "api", "realtime-gateway", "docker-compose.yml"];
3018
+ function printRailwayDeploySummary(config) {
3019
+ console.log(pc.bold("\nRailway deploy"));
3020
+ console.log(`- Directory: ${pc.bold(config.projectDir)}`);
3021
+ if (config.railwayContext.projectName || config.railwayContext.projectId) {
3022
+ console.log(`- Project: ${pc.bold(config.railwayContext.projectName || config.railwayContext.projectId)}`);
3023
+ }
3024
+ if (config.railwayContext.environmentName || config.railwayContext.environmentId) {
3025
+ console.log(`- Environment: ${pc.bold(config.railwayContext.environmentName || config.railwayContext.environmentId)}`);
3026
+ }
3027
+ console.log(`- Default managed services: ${pc.bold("api, worker, realtime-gateway, admin")}`);
3028
+ console.log(`- Services: ${pc.bold(config.selectedServices.join(", "))}`);
5078
3029
 
5079
- for (const relativePath of requiredPaths) {
5080
- const target = path.join(projectDir, relativePath);
5081
- if (!(await fs.pathExists(target))) {
5082
- throw new Error(`Project root not recognized, missing ${relativePath} in ${projectDir}`);
3030
+ console.log(pc.bold("\nDeployments"));
3031
+ if (config.deploySummary.length === 0) {
3032
+ console.log("- No application deployments were triggered");
3033
+ } else {
3034
+ for (const item of config.deploySummary) {
3035
+ console.log(`- ${pc.bold(item.serviceName)}: ${item.status} from ${item.directory}/`);
5083
3036
  }
5084
3037
  }
5085
- }
5086
3038
 
5087
- async function loadProjectConfig(projectDir) {
5088
- const configPath = path.join(projectDir, "asaje.config.json");
5089
- if (!(await fs.pathExists(configPath))) {
5090
- return null;
3039
+ if (config.dryRun) {
3040
+ console.log(`- Dry run only, ${pc.bold(RAILWAY_MANIFEST_FILENAME)} was not written`);
3041
+ } else {
3042
+ console.log(`- Manifest written to ${pc.bold(RAILWAY_MANIFEST_FILENAME)} for future runs`);
5091
3043
  }
5092
-
5093
- return fs.readJson(configPath);
5094
3044
  }
5095
3045
 
5096
3046
  async function ensureRailwayAppServiceTargets(projectDir, appServiceSpecs) {
@@ -5109,191 +3059,6 @@ async function ensureRailwayAppServiceTargets(projectDir, appServiceSpecs) {
5109
3059
  }
5110
3060
  }
5111
3061
 
5112
- async function scanProjectForManagedRailwayServices(projectDir) {
5113
- const dockerfiles = [];
5114
- await collectDockerfiles(projectDir, "", dockerfiles);
5115
-
5116
- const scannedSpecs = dockerfiles
5117
- .map((dockerfilePath) => buildScannedRailwayServiceSpec(projectDir, dockerfilePath))
5118
- .filter(Boolean)
5119
- .sort((left, right) => left.directory.localeCompare(right.directory));
5120
-
5121
- const serviceSpecs = synthesizeDerivedRailwayServices(scannedSpecs);
5122
-
5123
- return {
5124
- dockerfiles,
5125
- serviceSpecs,
5126
- };
5127
- }
5128
-
5129
- function synthesizeDerivedRailwayServices(serviceSpecs) {
5130
- const nextSpecs = [...serviceSpecs];
5131
- const hasAPI = serviceSpecs.some((spec) => spec.key === "api" && spec.directory === "api");
5132
- const hasWorker = serviceSpecs.some((spec) => spec.key === "worker");
5133
- if (hasAPI && !hasWorker) {
5134
- nextSpecs.push({
5135
- aliases: ["worker", "api-worker"],
5136
- baseName: "worker",
5137
- directory: "api",
5138
- dockerfile: "api/Dockerfile",
5139
- key: "worker",
5140
- seedImage: "alpine:3.22",
5141
- serviceName: null,
5142
- });
5143
- }
5144
-
5145
- return nextSpecs.sort((left, right) => `${left.directory}:${left.key}`.localeCompare(`${right.directory}:${right.key}`));
5146
- }
5147
-
5148
- async function collectDockerfiles(projectDir, relativeDir, results) {
5149
- const absoluteDir = path.join(projectDir, relativeDir);
5150
- const entries = await fs.readdir(absoluteDir, { withFileTypes: true });
5151
-
5152
- for (const entry of entries) {
5153
- const nextRelativePath = relativeDir ? path.posix.join(relativeDir, entry.name) : entry.name;
5154
- if (entry.isDirectory()) {
5155
- if (shouldSkipProjectScanDirectory(entry.name, nextRelativePath)) {
5156
- continue;
5157
- }
5158
-
5159
- await collectDockerfiles(projectDir, nextRelativePath, results);
5160
- continue;
5161
- }
5162
-
5163
- if (entry.isFile() && entry.name === "Dockerfile") {
5164
- results.push(nextRelativePath);
5165
- }
5166
- }
5167
- }
5168
-
5169
- function shouldSkipProjectScanDirectory(name, relativePath) {
5170
- const normalized = String(name || "").trim();
5171
- if (!normalized) {
5172
- return false;
5173
- }
5174
-
5175
- if ([".git", ".turbo", ".next", ".nuxt", "node_modules", "dist", "build", "coverage", "tmp", "vendor"].includes(normalized)) {
5176
- return true;
5177
- }
5178
-
5179
- if (relativePath === "cli") {
5180
- return true;
5181
- }
5182
-
5183
- return normalized.startsWith(".") && normalized !== ".well-known";
5184
- }
5185
-
5186
- function buildScannedRailwayServiceSpec(projectDir, dockerfilePath) {
5187
- const directory = path.posix.dirname(dockerfilePath);
5188
- if (!directory || directory === ".") {
5189
- return null;
5190
- }
5191
-
5192
- const inferred = inferRailwayServiceIdentity(directory);
5193
- return {
5194
- aliases: inferred.aliases,
5195
- baseName: inferred.baseName,
5196
- directory,
5197
- dockerfile: dockerfilePath,
5198
- key: inferred.key,
5199
- seedImage: inferred.key === "admin" ? "nginx:1.29-alpine" : "alpine:3.22",
5200
- serviceName: null,
5201
- };
5202
- }
5203
-
5204
- function inferRailwayServiceIdentity(directory) {
5205
- const normalizedDirectory = directory.replace(/\/+$/g, "");
5206
- const directoryName = path.posix.basename(normalizedDirectory);
5207
- const normalizedName = normalizeRailwayServiceName(directoryName);
5208
-
5209
- if (["api", "backend", "server"].includes(normalizedName)) {
5210
- return { aliases: ["api", "backend", "server"], baseName: "api", key: "api" };
5211
- }
5212
- if (["admin", "frontend", "web"].includes(normalizedName)) {
5213
- return { aliases: ["admin", "frontend", "web"], baseName: "admin", key: "admin" };
5214
- }
5215
- if (["realtime", "realtime-gateway"].includes(normalizedName)) {
5216
- return { aliases: ["realtime-gateway", "realtime"], baseName: "realtime-gateway", key: "realtime" };
5217
- }
5218
-
5219
- const slug = slugify(directoryName);
5220
- return { aliases: [slug], baseName: slug, key: slug };
5221
- }
5222
-
5223
- function buildSyncedProjectConfig(projectDir, projectConfig, scannedServiceSpecs) {
5224
- const nextConfig = {
5225
- ...(projectConfig || {}),
5226
- projectName: projectConfig?.projectName || path.basename(projectDir),
5227
- projectSlug: projectConfig?.projectSlug || slugify(path.basename(projectDir)),
5228
- };
5229
-
5230
- const previousServices = resolveRailwayAppServiceSpecs(projectConfig);
5231
- const mergedServices = mergeScannedRailwayServices(previousServices, scannedServiceSpecs);
5232
-
5233
- nextConfig.railway = {
5234
- ...(getRailwayConfig(projectConfig) || {}),
5235
- services: mergedServices.map((service) => ({
5236
- baseName: service.baseName,
5237
- directory: service.directory,
5238
- ...(service.dockerfile ? { dockerfile: service.dockerfile } : {}),
5239
- key: service.key,
5240
- ...(service.aliases?.length > 0 ? { aliases: service.aliases } : {}),
5241
- ...(service.serviceName ? { serviceName: service.serviceName } : {}),
5242
- ...(service.seedImage ? { seedImage: service.seedImage } : {}),
5243
- })),
5244
- };
5245
-
5246
- return nextConfig;
5247
- }
5248
-
5249
- function mergeScannedRailwayServices(previousServices, scannedServiceSpecs) {
5250
- const previousByKey = new Map(previousServices.map((service) => [service.key, service]));
5251
- const previousByDirectory = new Map(previousServices.map((service) => [service.directory, service]));
5252
-
5253
- return scannedServiceSpecs.map((scanned) => {
5254
- const previous = previousByKey.get(scanned.key) || previousByDirectory.get(scanned.directory);
5255
- return {
5256
- aliases: uniqueStrings([...(previous?.aliases || []), ...scanned.aliases]),
5257
- baseName: previous?.baseName || scanned.baseName,
5258
- directory: scanned.directory,
5259
- dockerfile: scanned.dockerfile,
5260
- key: previous?.key || scanned.key,
5261
- seedImage: previous?.seedImage || scanned.seedImage,
5262
- serviceName: previous?.serviceName || null,
5263
- };
5264
- });
5265
- }
5266
-
5267
- function buildSyncedRailwayManifest(manifest, nextProjectConfig, scannedServiceSpecs) {
5268
- const nextManifest = {
5269
- ...(manifest || {}),
5270
- appServices: {},
5271
- projectSlug: nextProjectConfig.projectSlug || manifest?.projectSlug || null,
5272
- updatedAt: new Date().toISOString(),
5273
- };
5274
-
5275
- const previousAppServices = manifest?.appServices || {};
5276
- const existingSpecs = resolveRailwayAppServiceSpecs(nextProjectConfig);
5277
- for (const spec of existingSpecs) {
5278
- const previousEntry =
5279
- previousAppServices[spec.key] ||
5280
- findRailwayManifestAppServiceByName(previousAppServices, resolveRailwayServiceName(spec, nextManifest.projectSlug));
5281
-
5282
- nextManifest.appServices[spec.key] = {
5283
- serviceId: previousEntry?.serviceId || null,
5284
- serviceName: previousEntry?.serviceName || resolveRailwayServiceName(spec, nextManifest.projectSlug),
5285
- };
5286
- }
5287
-
5288
- return nextManifest;
5289
- }
5290
-
5291
- function findRailwayManifestAppServiceByName(appServices, serviceName) {
5292
- return Object.values(appServices || {}).find(
5293
- (entry) => normalizeRailwayServiceName(entry?.serviceName) === normalizeRailwayServiceName(serviceName),
5294
- ) || null;
5295
- }
5296
-
5297
3062
  async function writeProjectConfigFile(projectDir, projectConfig) {
5298
3063
  const configPath = path.join(projectDir, "asaje.config.json");
5299
3064
  await fs.writeJson(configPath, projectConfig, { spaces: 2 });
@@ -5326,20 +3091,6 @@ function printSyncProjectConfigSummary(config) {
5326
3091
  }
5327
3092
  }
5328
3093
 
5329
- function uniqueStrings(values) {
5330
- return [...new Set((values || []).map((value) => String(value || "").trim()).filter(Boolean))];
5331
- }
5332
-
5333
- function resolveProjectSlug(projectDir, projectConfig) {
5334
- return slugify(projectConfig?.projectSlug || projectConfig?.projectName || path.basename(projectDir) || "asaje-app");
5335
- }
5336
-
5337
- function buildRailwayAppServiceName(projectSlug, baseName) {
5338
- const normalizedSlug = slugify(projectSlug || "asaje-app");
5339
- const normalizedBaseName = slugify(baseName);
5340
- return `${normalizedSlug}-${normalizedBaseName}`;
5341
- }
5342
-
5343
3094
  async function updateProjectTemplateConfig(projectDir, projectConfig, templateRepository, templateBranch) {
5344
3095
  const configPath = path.join(projectDir, "asaje.config.json");
5345
3096
  const nextConfig = {
@@ -5420,81 +3171,6 @@ function printUpdateSummary(config) {
5420
3171
  }
5421
3172
  }
5422
3173
 
5423
- function splitCommaSeparatedPaths(value) {
5424
- return value
5425
- .split(",")
5426
- .map((entry) => normalizeRelativePath(entry))
5427
- .filter(Boolean);
5428
- }
5429
-
5430
- function uniquePaths(paths) {
5431
- return [...new Set(paths.map((entry) => normalizeRelativePath(entry)).filter(Boolean))];
5432
- }
5433
-
5434
- function normalizeRelativePath(value) {
5435
- const trimmed = String(value || "").trim();
5436
- if (!trimmed) {
5437
- return "";
5438
- }
5439
-
5440
- return trimmed.replace(/^\.\//, "").replace(/\\/g, "/").replace(/\/$/, "");
5441
- }
5442
-
5443
- async function ensureEnvFiles(projectDir) {
5444
- for (const spec of ENV_FILE_SPECS) {
5445
- const serviceDir = spec.envPath.split("/")[0];
5446
- if (!(await fs.pathExists(path.join(projectDir, serviceDir)))) {
5447
- continue;
5448
- }
5449
-
5450
- const envPath = path.join(projectDir, spec.envPath);
5451
- if (await fs.pathExists(envPath)) {
5452
- continue;
5453
- }
5454
-
5455
- const examplePath = path.join(projectDir, spec.examplePath);
5456
- if (!(await fs.pathExists(examplePath))) {
5457
- throw new Error(`Missing ${spec.envPath} and ${spec.examplePath}`);
5458
- }
5459
-
5460
- await fs.copyFile(examplePath, envPath);
5461
- console.log(pc.yellow(`- Created ${spec.envPath} from ${spec.examplePath}`));
5462
- }
5463
- }
5464
-
5465
- async function loadRuntimeConfig(projectDir) {
5466
- const configPath = path.join(projectDir, "asaje.config.json");
5467
- let ports = { admin: 5173, api: 8080, realtime: 8090, landing: 8088, pwa: 4174 };
5468
-
5469
- if (await fs.pathExists(configPath)) {
5470
- const fileConfig = await fs.readJson(configPath);
5471
- ports = {
5472
- admin: Number(fileConfig?.ports?.admin || ports.admin),
5473
- api: Number(fileConfig?.ports?.api || ports.api),
5474
- landing: Number(fileConfig?.ports?.landing || ports.landing),
5475
- pwa: Number(fileConfig?.ports?.pwa || ports.pwa),
5476
- realtime: Number(fileConfig?.ports?.realtime || ports.realtime),
5477
- };
5478
- } else {
5479
- const [apiEnv, realtimeEnv, adminEnv, landingEnv, pwaEnv] = await Promise.all([
5480
- readEnvFile(path.join(projectDir, "api/.env")),
5481
- readEnvFile(path.join(projectDir, "realtime-gateway/.env")),
5482
- readEnvFile(path.join(projectDir, "admin/.env")),
5483
- tryReadEnvFile(path.join(projectDir, "landing/.env")),
5484
- tryReadEnvFile(path.join(projectDir, "pwa/.env")),
5485
- ]);
5486
- ports = {
5487
- admin: Number(adminEnv.VITE_ADMIN_PORT || ports.admin),
5488
- api: Number(apiEnv.PORT || ports.api),
5489
- landing: Number(landingEnv.PORT || ports.landing),
5490
- pwa: Number(pwaEnv.PORT || pwaEnv.VITE_PORT || ports.pwa),
5491
- realtime: Number(realtimeEnv.PORT || ports.realtime),
5492
- };
5493
- }
5494
-
5495
- return { ports };
5496
- }
5497
-
5498
3174
  function printStartSummary(projectDir, runtimeConfig, profile, selectedServices) {
5499
3175
  console.log(pc.green("\nStarting project services."));
5500
3176
  console.log(`- Directory: ${pc.bold(projectDir)}`);
@@ -5610,275 +3286,3 @@ async function startManagedProcesses(projectDir, runtimeConfig, selectedServices
5610
3286
  process.removeListener("SIGTERM", onSignal);
5611
3287
  }
5612
3288
  }
5613
-
5614
- function createManagedProcess(spec) {
5615
- const child = execa(spec.command, spec.args, {
5616
- cwd: spec.cwd,
5617
- stderr: "pipe",
5618
- stdout: "pipe",
5619
- });
5620
-
5621
- const prefix = spec.color(`[${spec.name}] `);
5622
- child.stdout?.on("data", (chunk) => {
5623
- process.stdout.write(prefixChunk(prefix, chunk));
5624
- });
5625
- child.stderr?.on("data", (chunk) => {
5626
- process.stderr.write(prefixChunk(prefix, chunk));
5627
- });
5628
-
5629
- const managed = child.then((result) => ({ ...result, name: spec.name }));
5630
- managed.kill = (...args) => child.kill(...args);
5631
- return managed;
5632
- }
5633
-
5634
- function prefixChunk(prefix, chunk) {
5635
- const textValue = chunk.toString();
5636
- const normalized = textValue.replace(/\n/g, `\n${prefix}`);
5637
- return `${prefix}${normalized}`;
5638
- }
5639
-
5640
- async function cloneTemplate(template, branch, destinationDir) {
5641
- const fallbackBranches = [branch, "main", "master", "develop"].filter(
5642
- (value, index, array) => value && array.indexOf(value) === index,
5643
- );
5644
- let lastError = null;
5645
-
5646
- for (const candidate of fallbackBranches) {
5647
- try {
5648
- const emitter = degit(`${template}#${candidate}`, {
5649
- cache: false,
5650
- force: false,
5651
- verbose: false,
5652
- });
5653
- await emitter.clone(destinationDir);
5654
- return;
5655
- } catch (error) {
5656
- lastError = error;
5657
- }
5658
- }
5659
-
5660
- throw lastError instanceof Error ? lastError : new Error(`Unable to clone template ${template}`);
5661
- }
5662
-
5663
- async function cleanupTemplateFiles(destinationDir) {
5664
- for (const relativePath of EXCLUDED_TEMPLATE_PATHS) {
5665
- await fs.remove(path.join(destinationDir, relativePath));
5666
- }
5667
- }
5668
-
5669
- async function ensureScaffoldedSurfaces(destinationDir, answers) {
5670
- const requiredDirectories = ["admin", "api", "realtime-gateway"];
5671
-
5672
- if (answers.includeLanding) {
5673
- requiredDirectories.push("landing");
5674
- }
5675
- if (answers.includePwa) {
5676
- requiredDirectories.push("pwa");
5677
- }
5678
-
5679
- const missing = [];
5680
- for (const relativeDir of requiredDirectories) {
5681
- if (!(await fs.pathExists(path.join(destinationDir, relativeDir)))) {
5682
- missing.push(relativeDir);
5683
- }
5684
- }
5685
-
5686
- if (missing.length > 0) {
5687
- throw new Error(
5688
- `Template scaffold is missing expected directories: ${missing.join(", ")}. ` +
5689
- `This usually means the selected template/branch does not include the requested surfaces. ` +
5690
- `Try another branch or update the template repository before running create again.`,
5691
- );
5692
- }
5693
- }
5694
-
5695
- async function ensureDestinationIsAvailable(destinationDir) {
5696
- const exists = await fs.pathExists(destinationDir);
5697
- if (!exists) {
5698
- return;
5699
- }
5700
-
5701
- const files = await fs.readdir(destinationDir);
5702
- if (files.length > 0) {
5703
- throw new Error(`Destination already exists and is not empty: ${destinationDir}. Choose another directory or empty it before running create.`);
5704
- }
5705
- }
5706
-
5707
- async function installProjectDependencies(projectDir) {
5708
- await runCommand("pnpm", ["install"], path.join(projectDir, "admin"));
5709
- if (await fs.pathExists(path.join(projectDir, "landing/package.json"))) {
5710
- await runCommand("pnpm", ["install"], path.join(projectDir, "landing"));
5711
- }
5712
- if (await fs.pathExists(path.join(projectDir, "pwa/package.json"))) {
5713
- await runCommand("pnpm", ["install"], path.join(projectDir, "pwa"));
5714
- }
5715
- await runCommand("go", ["mod", "download"], path.join(projectDir, "api"));
5716
- await runCommand("go", ["mod", "download"], path.join(projectDir, "realtime-gateway"));
5717
- }
5718
-
5719
- async function startInfrastructure(projectDir) {
5720
- await runCommand(
5721
- "docker",
5722
- ["compose", "up", "-d", "postgres", "rabbitmq", "minio", "minio-create-bucket"],
5723
- projectDir,
5724
- );
5725
- }
5726
-
5727
- async function runCommand(command, args, cwd) {
5728
- await execa(command, args, {
5729
- cwd,
5730
- stdio: "inherit",
5731
- });
5732
- }
5733
-
5734
- async function checkCommand(spec) {
5735
- try {
5736
- const result = await execa(spec.command, spec.args, {
5737
- reject: false,
5738
- });
5739
-
5740
- if (result.exitCode !== 0) {
5741
- return { message: `${spec.name} unavailable`, ok: false };
5742
- }
5743
-
5744
- const version = (result.stdout || result.stderr || "").split(/\r?\n/)[0].trim();
5745
- return { message: `${spec.name} detected${version ? ` (${version})` : ""}`, ok: true };
5746
- } catch {
5747
- return { message: `${spec.name} unavailable`, ok: false };
5748
- }
5749
- }
5750
-
5751
- async function readEnvFile(filePath) {
5752
- const contents = await fs.readFile(filePath, "utf8");
5753
- const result = {};
5754
-
5755
- for (const line of contents.split(/\r?\n/)) {
5756
- const trimmed = line.trim();
5757
- if (!trimmed || trimmed.startsWith("#")) {
5758
- continue;
5759
- }
5760
-
5761
- const separatorIndex = trimmed.indexOf("=");
5762
- if (separatorIndex === -1) {
5763
- continue;
5764
- }
5765
-
5766
- const key = trimmed.slice(0, separatorIndex).trim();
5767
- let value = trimmed.slice(separatorIndex + 1).trim();
5768
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
5769
- value = value.slice(1, -1);
5770
- }
5771
- result[key] = value;
5772
- }
5773
-
5774
- return result;
5775
- }
5776
-
5777
- function toEnvContent(values) {
5778
- return `${Object.entries(values)
5779
- .map(([key, value]) => `${key}=${escapeEnvValue(String(value))}`)
5780
- .join("\n")}\n`;
5781
- }
5782
-
5783
- function escapeEnvValue(value) {
5784
- if (value === "") {
5785
- return "";
5786
- }
5787
-
5788
- if (/\s|#|"/.test(value)) {
5789
- return JSON.stringify(value);
5790
- }
5791
-
5792
- return value;
5793
- }
5794
-
5795
- async function prompt(promise) {
5796
- const result = await promise;
5797
- if (isCancel(result)) {
5798
- cancel("Operation cancelled");
5799
- process.exit(0);
5800
- }
5801
- return result;
5802
- }
5803
-
5804
- async function promptNumber(message, defaultValue) {
5805
- const result = await prompt(
5806
- text({
5807
- defaultValue: String(defaultValue),
5808
- message,
5809
- validate(value) {
5810
- const number = Number(value);
5811
- return Number.isInteger(number) && number > 0 ? undefined : "Enter a positive integer";
5812
- },
5813
- }),
5814
- );
5815
-
5816
- return Number(result);
5817
- }
5818
-
5819
- async function promptEmail(message, defaultValue) {
5820
- return prompt(
5821
- text({
5822
- defaultValue,
5823
- message,
5824
- validate(value) {
5825
- return value.includes("@") ? undefined : "Enter a valid email";
5826
- },
5827
- }),
5828
- );
5829
- }
5830
-
5831
- async function promptSecret(message, minLength, defaultValue) {
5832
- return prompt(
5833
- password({
5834
- message,
5835
- ...(defaultValue ? { mask: "*" } : {}),
5836
- validate(value) {
5837
- return value.length >= minLength ? undefined : `Must be at least ${minLength} characters`;
5838
- },
5839
- }),
5840
- );
5841
- }
5842
-
5843
- function splitCsv(value) {
5844
- return value
5845
- .split(",")
5846
- .map((item) => item.trim())
5847
- .filter(Boolean);
5848
- }
5849
-
5850
- function slugify(value) {
5851
- return value
5852
- .toLowerCase()
5853
- .replace(/[^a-z0-9]+/g, "-")
5854
- .replace(/^-+|-+$/g, "");
5855
- }
5856
-
5857
- function titleize(value) {
5858
- return value
5859
- .split(/[-_\s]+/)
5860
- .filter(Boolean)
5861
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
5862
- .join(" ");
5863
- }
5864
-
5865
- function randomSecret(bytes) {
5866
- return crypto.randomBytes(bytes).toString("base64url");
5867
- }
5868
-
5869
- function shellEscape(value) {
5870
- if (/^[a-zA-Z0-9_./-]+$/.test(value)) {
5871
- return value;
5872
- }
5873
-
5874
- return JSON.stringify(value);
5875
- }
5876
-
5877
- function isHttpUrl(value) {
5878
- try {
5879
- const parsed = new URL(value);
5880
- return parsed.protocol === "http:" || parsed.protocol === "https:";
5881
- } catch {
5882
- return false;
5883
- }
5884
- }