create-asaje-go-vue 0.2.7 → 0.2.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.
package/README.md CHANGED
@@ -44,6 +44,14 @@ npx -p create-asaje-go-vue@latest asaje doctor ./my-app
44
44
  npx -p create-asaje-go-vue@latest asaje publish
45
45
  ```
46
46
 
47
+ ### Update an existing project from the template
48
+
49
+ ```bash
50
+ npx -p create-asaje-go-vue@latest asaje update ./my-app --dry-run
51
+ npx -p create-asaje-go-vue@latest asaje update ./my-app --yes
52
+ npx -p create-asaje-go-vue@latest asaje update ./my-app --include admin/src/stores/session.ts,admin/src/services/http/session.ts
53
+ ```
54
+
47
55
  ### Provision Railway resources
48
56
 
49
57
  ```bash
@@ -58,6 +66,13 @@ npx -p create-asaje-go-vue@latest asaje sync-railway-env ./my-app
58
66
  npx -p create-asaje-go-vue@latest asaje sync-railway-env ./my-app --dry-run
59
67
  ```
60
68
 
69
+ ### Destroy Railway resources
70
+
71
+ ```bash
72
+ npx -p create-asaje-go-vue@latest asaje destroy-railway ./my-app
73
+ npx -p create-asaje-go-vue@latest asaje destroy-railway ./my-app --scope project --yes
74
+ ```
75
+
61
76
  ## What `create` does
62
77
 
63
78
  - clones the boilerplate from GitHub with `degit`
@@ -89,6 +104,16 @@ npx -p create-asaje-go-vue@latest asaje sync-railway-env ./my-app --dry-run
89
104
  - runs `npm run pack:dry-run`
90
105
  - prints the final manual npm release steps
91
106
 
107
+ ## What `asaje update` does
108
+
109
+ - validates the target project structure
110
+ - reads the template repository and branch from `asaje.config.json` when available
111
+ - clones the latest template into a temporary directory
112
+ - overwrites a safe set of boilerplate-managed files such as Railway config, Dockerfiles, generated Swagger docs, and `.env.example` files
113
+ - supports `--include` for explicitly overwriting extra files or directories from the template, such as `admin/src/stores/session.ts`
114
+ - supports `--dry-run` to preview which files would be updated
115
+ - updates `asaje.config.json` with the template repository and branch used for the update
116
+
92
117
  ## What `asaje setup-railway` does
93
118
 
94
119
  - validates the target project structure
@@ -111,6 +136,15 @@ npx -p create-asaje-go-vue@latest asaje sync-railway-env ./my-app --dry-run
111
136
  - syncs variables for `api`, `realtime-gateway`, and `admin` without provisioning infra resources
112
137
  - supports `--dry-run` to preview variable changes without applying them
113
138
 
139
+ ## What `asaje destroy-railway` does
140
+
141
+ - validates the target project structure
142
+ - checks that the Railway CLI is installed and authenticated
143
+ - deletes either the linked Railway environment or the whole Railway project
144
+ - supports `--scope environment` (default) and `--scope project`
145
+ - supports `--dry-run` to preview the destructive action without applying it
146
+ - removes the local `asaje.railway.json` manifest after a successful deletion
147
+
114
148
  ## Useful flags
115
149
 
116
150
  ```bash
@@ -121,9 +155,12 @@ node ./bin/asaje.js start ../my-app --yes --profile frontend-only
121
155
  node ./bin/asaje.js start ../my-app --yes --skip-admin --skip-worker
122
156
  node ./bin/asaje.js doctor ../my-app
123
157
  node ./bin/asaje.js publish .
158
+ node ./bin/asaje.js update ../my-app --dry-run
159
+ node ./bin/asaje.js update ../my-app --include admin/src/stores/session.ts --yes
124
160
  node ./bin/asaje.js setup-railway ../my-app --yes
125
161
  node ./bin/asaje.js setup-railway ../my-app --yes --dry-run
126
162
  node ./bin/asaje.js sync-railway-env ../my-app --yes
163
+ node ./bin/asaje.js destroy-railway ../my-app --scope environment --yes
127
164
  ```
128
165
 
129
166
  ## Publish checklist
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import crypto from "node:crypto";
4
+ import os from "node:os";
4
5
  import path from "node:path";
5
6
  import process from "node:process";
6
7
  import {
@@ -29,23 +30,37 @@ const RAILWAY_SERVICE_DISCOVERY_RETRY_COUNT = 5;
29
30
  const RAILWAY_APP_SERVICE_SPECS = [
30
31
  {
31
32
  aliases: ["api", "backend", "server"],
33
+ baseName: "api",
32
34
  directory: "api",
33
35
  key: "api",
34
- serviceName: "api",
35
36
  },
36
37
  {
37
38
  aliases: ["admin", "frontend", "web"],
39
+ baseName: "admin",
38
40
  directory: "admin",
39
41
  key: "admin",
40
- serviceName: "admin",
41
42
  },
42
43
  {
43
44
  aliases: ["realtime-gateway", "realtime"],
45
+ baseName: "realtime-gateway",
44
46
  directory: "realtime-gateway",
45
47
  key: "realtime",
46
- serviceName: "realtime-gateway",
47
48
  },
48
49
  ];
50
+ const SAFE_UPDATE_PATHS = [
51
+ "docker-compose.yml",
52
+ "admin/.env.example",
53
+ "admin/Dockerfile",
54
+ "admin/railway.json",
55
+ "admin/nginx",
56
+ "api/.env.example",
57
+ "api/Dockerfile",
58
+ "api/railway.json",
59
+ "api/docs",
60
+ "realtime-gateway/.env.example",
61
+ "realtime-gateway/Dockerfile",
62
+ "realtime-gateway/railway.json",
63
+ ];
49
64
  const ENV_FILE_SPECS = [
50
65
  { envPath: "admin/.env", examplePath: "admin/.env.example" },
51
66
  { envPath: "api/.env", examplePath: "api/.env.example" },
@@ -83,6 +98,12 @@ async function main() {
83
98
  return;
84
99
  }
85
100
 
101
+ if (invocation.command === "update") {
102
+ await runUpdate(invocation.argv);
103
+ outro(pc.green("Project update complete."));
104
+ return;
105
+ }
106
+
86
107
  if (invocation.command === "setup-railway") {
87
108
  await runSetupRailway(invocation.argv);
88
109
  outro(pc.green("Railway setup complete."));
@@ -95,6 +116,12 @@ async function main() {
95
116
  return;
96
117
  }
97
118
 
119
+ if (invocation.command === "destroy-railway") {
120
+ await runDestroyRailway(invocation.argv);
121
+ outro(pc.green("Railway teardown complete."));
122
+ return;
123
+ }
124
+
98
125
  await runCreate(invocation.argv);
99
126
  outro(pc.green("Project ready."));
100
127
  } catch (error) {
@@ -135,6 +162,10 @@ function resolveInvocation(argv) {
135
162
  return { argv: rawArgs.slice(1), command: "publish", title: "asaje publish" };
136
163
  }
137
164
 
165
+ if (firstArg === "update") {
166
+ return { argv: rawArgs.slice(1), command: "update", title: "asaje update" };
167
+ }
168
+
138
169
  if (firstArg === "setup-railway") {
139
170
  return { argv: rawArgs.slice(1), command: "setup-railway", title: "asaje setup-railway" };
140
171
  }
@@ -143,6 +174,10 @@ function resolveInvocation(argv) {
143
174
  return { argv: rawArgs.slice(1), command: "sync-railway-env", title: "asaje sync-railway-env" };
144
175
  }
145
176
 
177
+ if (firstArg === "destroy-railway") {
178
+ return { argv: rawArgs.slice(1), command: "destroy-railway", title: "asaje destroy-railway" };
179
+ }
180
+
146
181
  if (firstArg === "create") {
147
182
  return { argv: rawArgs.slice(1), command: "create", title: "asaje create" };
148
183
  }
@@ -162,6 +197,22 @@ function resolveInvocation(argv) {
162
197
  return { argv: rawArgs.slice(1), command: "publish", title: "create-asaje-go-vue" };
163
198
  }
164
199
 
200
+ if (firstArg === "update") {
201
+ return { argv: rawArgs.slice(1), command: "update", title: "create-asaje-go-vue" };
202
+ }
203
+
204
+ if (firstArg === "setup-railway") {
205
+ return { argv: rawArgs.slice(1), command: "setup-railway", title: "create-asaje-go-vue" };
206
+ }
207
+
208
+ if (firstArg === "sync-railway-env") {
209
+ return { argv: rawArgs.slice(1), command: "sync-railway-env", title: "create-asaje-go-vue" };
210
+ }
211
+
212
+ if (firstArg === "destroy-railway") {
213
+ return { argv: rawArgs.slice(1), command: "destroy-railway", title: "create-asaje-go-vue" };
214
+ }
215
+
165
216
  return { argv: rawArgs, command: "create", title: "create-asaje-go-vue" };
166
217
  }
167
218
 
@@ -172,16 +223,20 @@ function printHelp() {
172
223
  console.log(`- ${pc.bold("asaje start [directory]")} start a configured project`);
173
224
  console.log(`- ${pc.bold("asaje doctor [directory]")} check environment and project readiness`);
174
225
  console.log(`- ${pc.bold("asaje publish")} run npm publish checklist for the CLI package`);
226
+ console.log(`- ${pc.bold("asaje update [directory]")} update managed boilerplate files from the template`);
175
227
  console.log(`- ${pc.bold("asaje setup-railway [directory]")} provision Railway infrastructure for a project`);
176
228
  console.log(`- ${pc.bold("asaje sync-railway-env [directory]")} sync Railway app variables without provisioning`);
229
+ console.log(`- ${pc.bold("asaje destroy-railway [directory]")} delete the linked Railway environment or project`);
177
230
  console.log(pc.bold("\nExamples"));
178
231
  console.log(`- ${pc.bold("npx create-asaje-go-vue my-app")}`);
179
232
  console.log(`- ${pc.bold("node ./bin/create-asaje-go-vue.js my-app --yes")}`);
180
233
  console.log(`- ${pc.bold("node ./bin/asaje.js start ../my-app")}`);
181
234
  console.log(`- ${pc.bold("node ./bin/asaje.js doctor ..")}`);
182
235
  console.log(`- ${pc.bold("node ./bin/asaje.js publish")}`);
236
+ console.log(`- ${pc.bold("node ./bin/asaje.js update .. --dry-run")}`);
183
237
  console.log(`- ${pc.bold("node ./bin/asaje.js setup-railway ..")}`);
184
238
  console.log(`- ${pc.bold("node ./bin/asaje.js sync-railway-env ..")}`);
239
+ console.log(`- ${pc.bold("node ./bin/asaje.js destroy-railway ..")}`);
185
240
  }
186
241
 
187
242
  async function runCreate(argv) {
@@ -276,6 +331,71 @@ function parseCreateArgs(argv) {
276
331
  return { ...options, directory: positionals[0] };
277
332
  }
278
333
 
334
+ function parseUpdateArgs(argv) {
335
+ const options = {
336
+ branch: undefined,
337
+ directory: ".",
338
+ dryRun: false,
339
+ include: [],
340
+ template: undefined,
341
+ yes: false,
342
+ };
343
+ const positionals = [];
344
+
345
+ for (let index = 0; index < argv.length; index += 1) {
346
+ const arg = argv[index];
347
+
348
+ if (arg === "--yes" || arg === "-y") {
349
+ options.yes = true;
350
+ continue;
351
+ }
352
+
353
+ if (arg === "--dry-run") {
354
+ options.dryRun = true;
355
+ continue;
356
+ }
357
+
358
+ if (arg === "--template") {
359
+ options.template = argv[index + 1] || options.template;
360
+ index += 1;
361
+ continue;
362
+ }
363
+
364
+ if (arg.startsWith("--template=")) {
365
+ options.template = arg.split("=")[1] || options.template;
366
+ continue;
367
+ }
368
+
369
+ if (arg === "--branch") {
370
+ options.branch = argv[index + 1] || options.branch;
371
+ index += 1;
372
+ continue;
373
+ }
374
+
375
+ if (arg.startsWith("--branch=")) {
376
+ options.branch = arg.split("=")[1] || options.branch;
377
+ continue;
378
+ }
379
+
380
+ if (arg === "--include") {
381
+ options.include.push(...splitCommaSeparatedPaths(argv[index + 1] || ""));
382
+ index += 1;
383
+ continue;
384
+ }
385
+
386
+ if (arg.startsWith("--include=")) {
387
+ options.include.push(...splitCommaSeparatedPaths(arg.split("=")[1] || ""));
388
+ continue;
389
+ }
390
+
391
+ positionals.push(arg);
392
+ }
393
+
394
+ options.directory = positionals[0] || options.directory;
395
+ options.include = uniquePaths(options.include);
396
+ return options;
397
+ }
398
+
279
399
  async function collectCreateAnswers(args) {
280
400
  const defaultDirectory = args.directory || "my-asaje-app";
281
401
  const defaultSlug = slugify(path.basename(defaultDirectory));
@@ -610,6 +730,44 @@ async function collectCreateAnswers(args) {
610
730
  });
611
731
  }
612
732
 
733
+ async function collectUpdateAnswers(args) {
734
+ if (args.yes) {
735
+ return args;
736
+ }
737
+
738
+ const directory = await prompt(
739
+ text({
740
+ defaultValue: args.directory,
741
+ message: "Project directory to update?",
742
+ placeholder: ".",
743
+ validate(value) {
744
+ return value.trim().length === 0 ? "Project directory is required" : undefined;
745
+ },
746
+ }),
747
+ );
748
+
749
+ const include = args.include.length
750
+ ? args.include
751
+ : splitCommaSeparatedPaths(
752
+ await prompt(
753
+ text({
754
+ defaultValue: "",
755
+ message: "Additional files or directories to overwrite from the template? (optional, comma-separated)",
756
+ placeholder: "admin/src/stores/session.ts,admin/src/services/http/session.ts",
757
+ }),
758
+ ),
759
+ );
760
+
761
+ return {
762
+ branch: args.branch,
763
+ directory,
764
+ dryRun: args.dryRun,
765
+ include,
766
+ template: args.template,
767
+ yes: true,
768
+ };
769
+ }
770
+
613
771
  function buildCreateAnswers(input) {
614
772
  const directory = input.directory.trim();
615
773
  const slug = slugify(path.basename(directory));
@@ -911,10 +1069,54 @@ async function runPublish(argv) {
911
1069
  console.log(`- Publish with ${pc.bold("npm publish")}`);
912
1070
  }
913
1071
 
1072
+ async function runUpdate(argv) {
1073
+ const args = parseUpdateArgs(argv);
1074
+ const answers = await collectUpdateAnswers(args);
1075
+ const projectDir = path.resolve(process.cwd(), answers.directory);
1076
+
1077
+ await ensureProjectStructure(projectDir);
1078
+
1079
+ const projectConfig = await loadProjectConfig(projectDir);
1080
+ const templateRepository = answers.template || projectConfig?.template?.repository || DEFAULT_TEMPLATE;
1081
+ const templateBranch = answers.branch || projectConfig?.template?.branch || DEFAULT_BRANCH;
1082
+ const templateDir = await fs.mkdtemp(path.join(os.tmpdir(), "asaje-update-"));
1083
+
1084
+ try {
1085
+ console.log(pc.dim(`\nCloning template ${templateRepository}#${templateBranch}...`));
1086
+ await cloneTemplate(templateRepository, templateBranch, templateDir);
1087
+ await cleanupTemplateFiles(templateDir);
1088
+
1089
+ const selectedPaths = uniquePaths([...SAFE_UPDATE_PATHS, ...answers.include]);
1090
+ const summary = await applyTemplateUpdates({
1091
+ dryRun: answers.dryRun,
1092
+ projectDir,
1093
+ selectedPaths,
1094
+ templateDir,
1095
+ });
1096
+
1097
+ if (!answers.dryRun) {
1098
+ await updateProjectTemplateConfig(projectDir, projectConfig, templateRepository, templateBranch);
1099
+ }
1100
+
1101
+ printUpdateSummary({
1102
+ branch: templateBranch,
1103
+ dryRun: answers.dryRun,
1104
+ include: answers.include,
1105
+ projectDir,
1106
+ repository: templateRepository,
1107
+ summary,
1108
+ });
1109
+ } finally {
1110
+ await fs.remove(templateDir);
1111
+ }
1112
+ }
1113
+
914
1114
  async function runSetupRailway(argv) {
915
1115
  const args = parseSetupRailwayArgs(argv);
916
1116
  const answers = await collectSetupRailwayAnswers(args);
917
1117
  const projectDir = path.resolve(process.cwd(), answers.directory);
1118
+ const projectConfig = await loadProjectConfig(projectDir);
1119
+ const projectSlug = resolveProjectSlug(projectDir, projectConfig);
918
1120
 
919
1121
  await ensureProjectStructure(projectDir);
920
1122
  await ensureRailwayCliInstalled();
@@ -991,17 +1193,18 @@ async function runSetupRailway(argv) {
991
1193
  console.log(pc.bold("\nApplication services"));
992
1194
  manifest.appServices ||= {};
993
1195
  for (const spec of RAILWAY_APP_SERVICE_SPECS) {
1196
+ const serviceName = buildRailwayAppServiceName(projectSlug, spec.baseName);
994
1197
  const serviceResult = await ensureRailwayAppService({
995
1198
  aliases: spec.aliases,
996
1199
  dryRun: answers.dryRun,
997
1200
  existingServices,
998
1201
  key: spec.key,
999
1202
  manifest,
1000
- projectDir,
1001
- railwayContext,
1002
- serviceName: spec.serviceName,
1003
- seedImage: spec.key === "admin" ? "nginx:1.29-alpine" : "alpine:3.22",
1004
- });
1203
+ projectDir,
1204
+ railwayContext,
1205
+ serviceName,
1206
+ seedImage: spec.key === "admin" ? "nginx:1.29-alpine" : "alpine:3.22",
1207
+ });
1005
1208
  appServiceSummary.push(serviceResult);
1006
1209
  }
1007
1210
 
@@ -1010,6 +1213,7 @@ async function runSetupRailway(argv) {
1010
1213
  manifest.environmentName = railwayContext.environmentName || manifest.environmentName || null;
1011
1214
  manifest.projectId = railwayContext.projectId || manifest.projectId || null;
1012
1215
  manifest.projectName = railwayContext.projectName || manifest.projectName || null;
1216
+ manifest.projectSlug = projectSlug;
1013
1217
  manifest.updatedAt = new Date().toISOString();
1014
1218
 
1015
1219
  const servicesAfterProvision = await discoverRailwayServices(railwayContext, projectDir);
@@ -1026,6 +1230,7 @@ async function runSetupRailway(argv) {
1026
1230
  dryRun: answers.dryRun,
1027
1231
  manifest,
1028
1232
  projectDir,
1233
+ projectSlug,
1029
1234
  railwayContext,
1030
1235
  services: servicesAfterProvision,
1031
1236
  });
@@ -1050,6 +1255,8 @@ async function runSyncRailwayEnv(argv) {
1050
1255
  const args = parseSetupRailwayArgs(argv);
1051
1256
  const answers = await collectSetupRailwayAnswers(args);
1052
1257
  const projectDir = path.resolve(process.cwd(), answers.directory);
1258
+ const projectConfig = await loadProjectConfig(projectDir);
1259
+ const projectSlug = resolveProjectSlug(projectDir, projectConfig);
1053
1260
 
1054
1261
  await ensureProjectStructure(projectDir);
1055
1262
  await ensureRailwayCliInstalled();
@@ -1080,6 +1287,7 @@ async function runSyncRailwayEnv(argv) {
1080
1287
  manifest.environmentName = railwayContext.environmentName || manifest.environmentName || null;
1081
1288
  manifest.projectId = railwayContext.projectId || manifest.projectId || null;
1082
1289
  manifest.projectName = railwayContext.projectName || manifest.projectName || null;
1290
+ manifest.projectSlug = manifest.projectSlug || projectSlug;
1083
1291
  manifest.updatedAt = new Date().toISOString();
1084
1292
 
1085
1293
  if (!answers.dryRun) {
@@ -1098,6 +1306,67 @@ async function runSyncRailwayEnv(argv) {
1098
1306
  });
1099
1307
  }
1100
1308
 
1309
+ async function runDestroyRailway(argv) {
1310
+ const args = parseDestroyRailwayArgs(argv);
1311
+ const answers = await collectDestroyRailwayAnswers(args);
1312
+ const projectDir = path.resolve(process.cwd(), answers.directory);
1313
+
1314
+ await ensureProjectStructure(projectDir);
1315
+ await ensureRailwayCliInstalled();
1316
+ await ensureRailwayAuthenticated(projectDir, answers.environment);
1317
+
1318
+ const railwayContext = await loadRailwayContext(projectDir, answers.environment);
1319
+ const targetEnvironment = answers.environment || railwayContext.environmentId || railwayContext.environmentName;
1320
+ const targetProject = railwayContext.projectId || railwayContext.projectName;
1321
+
1322
+ if (answers.scope === "project") {
1323
+ if (!targetProject) {
1324
+ throw new Error(`Unable to determine Railway project for ${projectDir}. Link the directory first with \`railway link\`.`);
1325
+ }
1326
+
1327
+ console.log(pc.bold("\nDestroy Railway project"));
1328
+ console.log(`- Project: ${pc.bold(railwayContext.projectName || railwayContext.projectId)}`);
1329
+ if (answers.dryRun) {
1330
+ console.log(`- Dry run: would delete project ${pc.bold(targetProject)}`);
1331
+ return;
1332
+ }
1333
+
1334
+ const commandArgs = ["project", "delete", "--project", targetProject, "--yes"];
1335
+ if (answers.twoFactorCode) {
1336
+ commandArgs.push("--2fa-code", answers.twoFactorCode);
1337
+ }
1338
+ await runRailwayCommand(projectDir, undefined, commandArgs);
1339
+ } else {
1340
+ if (!targetEnvironment) {
1341
+ throw new Error(`Unable to determine Railway environment for ${projectDir}. Pass \`--environment\` or link the directory first.`);
1342
+ }
1343
+
1344
+ await ensureRailwayEnvironmentLinked(projectDir, targetEnvironment);
1345
+
1346
+ console.log(pc.bold("\nDestroy Railway environment"));
1347
+ console.log(`- Environment: ${pc.bold(targetEnvironment)}`);
1348
+ if (railwayContext.projectName || railwayContext.projectId) {
1349
+ console.log(`- Project: ${pc.bold(railwayContext.projectName || railwayContext.projectId)}`);
1350
+ }
1351
+ if (answers.dryRun) {
1352
+ console.log(`- Dry run: would delete environment ${pc.bold(targetEnvironment)}`);
1353
+ return;
1354
+ }
1355
+
1356
+ const commandArgs = ["environment", "delete", targetEnvironment, "--yes"];
1357
+ if (answers.twoFactorCode) {
1358
+ commandArgs.push("--2fa-code", answers.twoFactorCode);
1359
+ }
1360
+ await runRailwayCommand(projectDir, undefined, commandArgs);
1361
+ }
1362
+
1363
+ const manifestPath = path.join(projectDir, RAILWAY_MANIFEST_FILENAME);
1364
+ if (await fs.pathExists(manifestPath)) {
1365
+ await fs.remove(manifestPath);
1366
+ console.log(`- Removed local ${pc.bold(RAILWAY_MANIFEST_FILENAME)} manifest`);
1367
+ }
1368
+ }
1369
+
1101
1370
  function parseDirectoryArgs(argv) {
1102
1371
  return { directory: argv[0] || "." };
1103
1372
  }
@@ -1154,6 +1423,74 @@ function parseSetupRailwayArgs(argv) {
1154
1423
  return options;
1155
1424
  }
1156
1425
 
1426
+ function parseDestroyRailwayArgs(argv) {
1427
+ const options = {
1428
+ directory: ".",
1429
+ dryRun: false,
1430
+ environment: undefined,
1431
+ scope: "environment",
1432
+ twoFactorCode: undefined,
1433
+ yes: false,
1434
+ };
1435
+ const positionals = [];
1436
+
1437
+ for (let index = 0; index < argv.length; index += 1) {
1438
+ const arg = argv[index];
1439
+
1440
+ if (arg === "--yes" || arg === "-y") {
1441
+ options.yes = true;
1442
+ continue;
1443
+ }
1444
+
1445
+ if (arg === "--dry-run") {
1446
+ options.dryRun = true;
1447
+ continue;
1448
+ }
1449
+
1450
+ if (arg === "--environment" || arg === "-e") {
1451
+ options.environment = argv[index + 1] || options.environment;
1452
+ index += 1;
1453
+ continue;
1454
+ }
1455
+
1456
+ if (arg.startsWith("--environment=")) {
1457
+ options.environment = arg.split("=")[1] || options.environment;
1458
+ continue;
1459
+ }
1460
+
1461
+ if (arg === "--scope") {
1462
+ options.scope = argv[index + 1] || options.scope;
1463
+ index += 1;
1464
+ continue;
1465
+ }
1466
+
1467
+ if (arg.startsWith("--scope=")) {
1468
+ options.scope = arg.split("=")[1] || options.scope;
1469
+ continue;
1470
+ }
1471
+
1472
+ if (arg === "--2fa-code") {
1473
+ options.twoFactorCode = argv[index + 1] || options.twoFactorCode;
1474
+ index += 1;
1475
+ continue;
1476
+ }
1477
+
1478
+ if (arg.startsWith("--2fa-code=")) {
1479
+ options.twoFactorCode = arg.split("=")[1] || options.twoFactorCode;
1480
+ continue;
1481
+ }
1482
+
1483
+ positionals.push(arg);
1484
+ }
1485
+
1486
+ options.directory = positionals[0] || options.directory;
1487
+ if (!["environment", "project"].includes(options.scope)) {
1488
+ throw new Error("--scope must be either 'environment' or 'project'");
1489
+ }
1490
+
1491
+ return options;
1492
+ }
1493
+
1157
1494
  async function collectSetupRailwayAnswers(args) {
1158
1495
  if (args.yes) {
1159
1496
  return {
@@ -1207,6 +1544,68 @@ async function collectSetupRailwayAnswers(args) {
1207
1544
  };
1208
1545
  }
1209
1546
 
1547
+ async function collectDestroyRailwayAnswers(args) {
1548
+ if (args.yes) {
1549
+ return args;
1550
+ }
1551
+
1552
+ const directory = await prompt(
1553
+ text({
1554
+ defaultValue: args.directory,
1555
+ message: "Project directory linked to Railway?",
1556
+ placeholder: ".",
1557
+ validate(value) {
1558
+ return value.trim().length === 0 ? "Project directory is required" : undefined;
1559
+ },
1560
+ }),
1561
+ );
1562
+
1563
+ const scope = await prompt(
1564
+ select({
1565
+ initialValue: args.scope,
1566
+ message: "What should be deleted?",
1567
+ options: [
1568
+ { label: "Environment", value: "environment", hint: "Delete one Railway environment and its services/resources" },
1569
+ { label: "Project", value: "project", hint: "Delete the whole Railway project" },
1570
+ ],
1571
+ }),
1572
+ );
1573
+
1574
+ let environment = args.environment;
1575
+ if (scope === "environment" && !environment) {
1576
+ environment = await prompt(
1577
+ text({
1578
+ defaultValue: "",
1579
+ message: "Railway environment name or ID? (leave empty for linked default)",
1580
+ placeholder: "production",
1581
+ }),
1582
+ );
1583
+ }
1584
+
1585
+ const confirmed = await prompt(
1586
+ confirm({
1587
+ initialValue: false,
1588
+ message:
1589
+ scope === "project"
1590
+ ? "Delete the whole Railway project and all its environments?"
1591
+ : `Delete the Railway environment${environment ? ` ${environment}` : ""} and all its services/resources?`,
1592
+ }),
1593
+ );
1594
+
1595
+ if (!confirmed) {
1596
+ throw new Error("Railway teardown cancelled.");
1597
+ }
1598
+
1599
+ return {
1600
+ directory,
1601
+ dryRun: args.dryRun,
1602
+ environment: environment?.trim() || undefined,
1603
+ scope,
1604
+ twoFactorCode: args.twoFactorCode,
1605
+ yes: true,
1606
+ };
1607
+ }
1608
+
1210
1609
  async function ensureRailwayCliInstalled() {
1211
1610
  const result = await checkCommand({ command: "railway", args: ["--version"], name: "railway" });
1212
1611
  if (!result.ok) {
@@ -1250,6 +1649,7 @@ async function readRailwayManifest(projectDir) {
1250
1649
  bucket: DEFAULT_RAILWAY_BUCKET,
1251
1650
  environmentId: null,
1252
1651
  environmentName: null,
1652
+ projectSlug: null,
1253
1653
  projectId: null,
1254
1654
  projectName: null,
1255
1655
  resources: {},
@@ -1539,11 +1939,12 @@ async function deployRailwayAppServices(config) {
1539
1939
  const summary = [];
1540
1940
  for (const spec of RAILWAY_APP_SERVICE_SPECS) {
1541
1941
  const manifestEntry = config.manifest.appServices?.[spec.key];
1542
- const service = findRailwayService(config.services, spec.aliases, manifestEntry?.serviceName || spec.serviceName);
1543
- const targetServiceName = service?.name || manifestEntry?.serviceName || spec.serviceName;
1942
+ const defaultServiceName = buildRailwayAppServiceName(config.projectSlug, spec.baseName);
1943
+ const service = findRailwayService(config.services, spec.aliases, manifestEntry?.serviceName || defaultServiceName);
1944
+ const targetServiceName = service?.name || manifestEntry?.serviceName || defaultServiceName;
1544
1945
 
1545
1946
  if (!service && !config.dryRun) {
1546
- console.log(`- ${pc.yellow(spec.serviceName)} service not found, skipping deployment`);
1947
+ console.log(`- ${pc.yellow(defaultServiceName)} service not found, skipping deployment`);
1547
1948
  continue;
1548
1949
  }
1549
1950
 
@@ -1891,7 +2292,7 @@ function findRailwayService(services, aliases, preferredName) {
1891
2292
  const normalizedAliases = aliases.map(normalizeRailwayServiceName);
1892
2293
  return services.find((service) => {
1893
2294
  const normalizedName = normalizeRailwayServiceName(service.name);
1894
- return normalizedAliases.some((alias) => normalizedName === alias || normalizedName.includes(alias));
2295
+ return normalizedAliases.some((alias) => normalizedName === alias || normalizedName.endsWith(`-${alias}`));
1895
2296
  });
1896
2297
  }
1897
2298
 
@@ -1976,14 +2377,28 @@ function updateRailwayManifestAppServices(manifest, services) {
1976
2377
  manifest.appServices ||= {};
1977
2378
 
1978
2379
  const entries = [
1979
- ["api", findRailwayService(services, ["api", "backend", "server"], manifest.appServices.api?.serviceName)],
1980
- ["admin", findRailwayService(services, ["admin", "frontend", "web"], manifest.appServices.admin?.serviceName)],
2380
+ [
2381
+ "api",
2382
+ findRailwayService(
2383
+ services,
2384
+ ["api", "backend", "server"],
2385
+ manifest.appServices.api?.serviceName || buildRailwayAppServiceName(manifest.projectSlug, "api"),
2386
+ ),
2387
+ ],
2388
+ [
2389
+ "admin",
2390
+ findRailwayService(
2391
+ services,
2392
+ ["admin", "frontend", "web"],
2393
+ manifest.appServices.admin?.serviceName || buildRailwayAppServiceName(manifest.projectSlug, "admin"),
2394
+ ),
2395
+ ],
1981
2396
  [
1982
2397
  "realtime",
1983
2398
  findRailwayService(
1984
2399
  services,
1985
2400
  ["realtime-gateway", "realtime"],
1986
- manifest.appServices.realtime?.serviceName,
2401
+ manifest.appServices.realtime?.serviceName || buildRailwayAppServiceName(manifest.projectSlug, "realtime-gateway"),
1987
2402
  ),
1988
2403
  ],
1989
2404
  ];
@@ -2401,6 +2816,125 @@ async function ensureProjectStructure(projectDir) {
2401
2816
  }
2402
2817
  }
2403
2818
 
2819
+ async function loadProjectConfig(projectDir) {
2820
+ const configPath = path.join(projectDir, "asaje.config.json");
2821
+ if (!(await fs.pathExists(configPath))) {
2822
+ return null;
2823
+ }
2824
+
2825
+ return fs.readJson(configPath);
2826
+ }
2827
+
2828
+ function resolveProjectSlug(projectDir, projectConfig) {
2829
+ return slugify(projectConfig?.projectSlug || projectConfig?.projectName || path.basename(projectDir) || "asaje-app");
2830
+ }
2831
+
2832
+ function buildRailwayAppServiceName(projectSlug, baseName) {
2833
+ const normalizedSlug = slugify(projectSlug || "asaje-app");
2834
+ const normalizedBaseName = slugify(baseName);
2835
+ return `${normalizedSlug}-${normalizedBaseName}`;
2836
+ }
2837
+
2838
+ async function updateProjectTemplateConfig(projectDir, projectConfig, templateRepository, templateBranch) {
2839
+ const configPath = path.join(projectDir, "asaje.config.json");
2840
+ const nextConfig = {
2841
+ ...(projectConfig || {}),
2842
+ template: {
2843
+ branch: templateBranch,
2844
+ repository: templateRepository,
2845
+ },
2846
+ };
2847
+
2848
+ await fs.writeJson(configPath, nextConfig, { spaces: 2 });
2849
+ }
2850
+
2851
+ async function applyTemplateUpdates(config) {
2852
+ const summary = {
2853
+ missing: [],
2854
+ skipped: [],
2855
+ updated: [],
2856
+ };
2857
+
2858
+ for (const relativePath of config.selectedPaths) {
2859
+ const sourcePath = path.join(config.templateDir, relativePath);
2860
+ const destinationPath = path.join(config.projectDir, relativePath);
2861
+
2862
+ if (!(await fs.pathExists(sourcePath))) {
2863
+ summary.missing.push(relativePath);
2864
+ continue;
2865
+ }
2866
+
2867
+ const sourceStats = await fs.stat(sourcePath);
2868
+ if (config.dryRun) {
2869
+ summary.updated.push(relativePath);
2870
+ continue;
2871
+ }
2872
+
2873
+ if (sourceStats.isDirectory()) {
2874
+ await fs.remove(destinationPath);
2875
+ await fs.copy(sourcePath, destinationPath);
2876
+ } else {
2877
+ await fs.ensureDir(path.dirname(destinationPath));
2878
+ await fs.copyFile(sourcePath, destinationPath);
2879
+ }
2880
+
2881
+ summary.updated.push(relativePath);
2882
+ }
2883
+
2884
+ summary.skipped = config.selectedPaths.filter((relativePath) => !summary.updated.includes(relativePath) && !summary.missing.includes(relativePath));
2885
+ return summary;
2886
+ }
2887
+
2888
+ function printUpdateSummary(config) {
2889
+ console.log(pc.bold("\nUpdate"));
2890
+ console.log(`- Directory: ${pc.bold(config.projectDir)}`);
2891
+ console.log(`- Template: ${pc.bold(`${config.repository}#${config.branch}`)}`);
2892
+ console.log(`- Safe paths: ${pc.bold(String(SAFE_UPDATE_PATHS.length))}`);
2893
+ if (config.include.length > 0) {
2894
+ console.log(`- Extra include: ${pc.bold(config.include.join(", "))}`);
2895
+ }
2896
+
2897
+ console.log(pc.bold("\nUpdated"));
2898
+ if (config.summary.updated.length === 0) {
2899
+ console.log("- No files selected for update");
2900
+ } else {
2901
+ for (const relativePath of config.summary.updated) {
2902
+ console.log(`- ${config.dryRun ? "would update" : "updated"} ${pc.bold(relativePath)}`);
2903
+ }
2904
+ }
2905
+
2906
+ if (config.summary.missing.length > 0) {
2907
+ console.log(pc.bold("\nMissing In Template"));
2908
+ for (const relativePath of config.summary.missing) {
2909
+ console.log(`- ${pc.bold(relativePath)}`);
2910
+ }
2911
+ }
2912
+
2913
+ if (config.dryRun) {
2914
+ console.log("- Dry run only, local files were not modified");
2915
+ }
2916
+ }
2917
+
2918
+ function splitCommaSeparatedPaths(value) {
2919
+ return value
2920
+ .split(",")
2921
+ .map((entry) => normalizeRelativePath(entry))
2922
+ .filter(Boolean);
2923
+ }
2924
+
2925
+ function uniquePaths(paths) {
2926
+ return [...new Set(paths.map((entry) => normalizeRelativePath(entry)).filter(Boolean))];
2927
+ }
2928
+
2929
+ function normalizeRelativePath(value) {
2930
+ const trimmed = String(value || "").trim();
2931
+ if (!trimmed) {
2932
+ return "";
2933
+ }
2934
+
2935
+ return trimmed.replace(/^\.\//, "").replace(/\\/g, "/").replace(/\/$/, "");
2936
+ }
2937
+
2404
2938
  async function ensureEnvFiles(projectDir) {
2405
2939
  for (const spec of ENV_FILE_SPECS) {
2406
2940
  const envPath = path.join(projectDir, spec.envPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-asaje-go-vue",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "CLI to scaffold, configure, and run the Asaje Go + Vue boilerplate",
5
5
  "type": "module",
6
6
  "bin": {