create-asaje-go-vue 0.3.0 → 0.3.2

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
@@ -8,6 +8,7 @@ CLI package for scaffolding and running the Asaje Go + Vue boilerplate.
8
8
 
9
9
  | Command | Purpose |
10
10
  | --- | --- |
11
+ | `asaje sync-project-config [directory]` | Scan the project, detect managed services, and rewrite `asaje.config.json` / `asaje.railway.json` |
11
12
  | `asaje setup-railway [directory]` | Provision infrastructure, create missing Railway services, wire variables, and deploy |
12
13
  | `asaje update-railway [directory]` | Reconcile an existing Railway project after changing `asaje.config.json` |
13
14
  | `asaje sync-railway-env [directory]` | Reapply Railway variables without reprovisioning infra |
@@ -66,6 +67,13 @@ npx -p create-asaje-go-vue@latest asaje update ./my-app --yes
66
67
  npx -p create-asaje-go-vue@latest asaje update ./my-app --include admin/src/stores/session.ts,admin/src/services/http/session.ts
67
68
  ```
68
69
 
70
+ ### Scan the project and regenerate local config manifests
71
+
72
+ ```bash
73
+ npx -p create-asaje-go-vue@latest asaje sync-project-config ./my-app --dry-run
74
+ npx -p create-asaje-go-vue@latest asaje sync-project-config ./my-app --yes
75
+ ```
76
+
69
77
  ### Provision Railway resources
70
78
 
71
79
  ```bash
@@ -74,6 +82,15 @@ npx -p create-asaje-go-vue@latest asaje setup-railway ./my-app --dry-run
74
82
  npx -p create-asaje-go-vue@latest asaje update-railway ./my-app --yes
75
83
  ```
76
84
 
85
+ By default this manages four Railway app services:
86
+
87
+ - `api`
88
+ - `worker`
89
+ - `realtime-gateway`
90
+ - `admin`
91
+
92
+ The default `worker` service reuses `api/Dockerfile` and starts with `API_COMMAND=worker`.
93
+
77
94
  ### Sync Railway app variables
78
95
 
79
96
  ```bash
@@ -119,6 +136,7 @@ npx -p create-asaje-go-vue@latest asaje diff-railway-config ./my-app --file ./sn
119
136
 
120
137
  ```bash
121
138
  npx -p create-asaje-go-vue@latest asaje deploy-railway ./my-app
139
+ npx -p create-asaje-go-vue@latest asaje deploy-railway ./my-app --service worker
122
140
  npx -p create-asaje-go-vue@latest asaje deploy-railway ./my-app --service api
123
141
  npx -p create-asaje-go-vue@latest asaje deploy-railway ./my-app --services api,admin --dry-run
124
142
  ```
@@ -171,6 +189,15 @@ npx -p create-asaje-go-vue@latest asaje destroy-railway ./my-app --scope project
171
189
  - supports `--dry-run` to preview which files would be updated
172
190
  - updates `asaje.config.json` with the template repository and branch used for the update
173
191
 
192
+ ## What `asaje sync-project-config` does
193
+
194
+ - scans the project tree for service-local `Dockerfile` files
195
+ - infers Railway managed services from the detected directories
196
+ - preserves existing service metadata when possible, while updating directories and Dockerfile paths from the scan
197
+ - rewrites `asaje.config.json` with the merged `railway.services` list
198
+ - rewrites `asaje.railway.json` so local service names line up with the current managed service list
199
+ - supports `--dry-run` to preview the rewrite without changing local files
200
+
174
201
  ## What `asaje setup-railway` does
175
202
 
176
203
  - validates the target project structure
@@ -178,9 +205,9 @@ npx -p create-asaje-go-vue@latest asaje destroy-railway ./my-app --scope project
178
205
  - reads the linked Railway project context
179
206
  - provisions PostgreSQL, RabbitMQ, and S3-compatible object storage on Railway
180
207
  - creates missing Railway app services for the configured app service list
181
- - defaults to `api`, `realtime-gateway`, and `admin` when no custom Railway service config is present
208
+ - defaults to `api`, `worker`, `realtime-gateway`, and `admin` when no custom Railway service config is present
182
209
  - applies Railway variables from `asaje.config.json` when configured
183
- - keeps the legacy automatic variable wiring for `api`, `realtime-gateway`, and `admin` unless `railway.variablesMode` is set to `replace`
210
+ - keeps the legacy automatic variable wiring for `api`, `worker`, `realtime-gateway`, and `admin` unless `railway.variablesMode` is set to `replace`
184
211
  - triggers the first Railway deployment for each app service using the service-local `Dockerfile` and `railway.json`
185
212
  - generates missing app secrets such as `JWT_SECRET` and `SWAGGER_PASSWORD`, while reusing existing Railway values when present
186
213
  - supports `--dry-run` to preview provisioning and variable changes without applying them
@@ -201,7 +228,7 @@ npx -p create-asaje-go-vue@latest asaje destroy-railway ./my-app --scope project
201
228
  - reads the linked Railway project context
202
229
  - discovers existing Railway app and infra services
203
230
  - syncs configured Railway variables without provisioning infra resources
204
- - keeps the legacy automatic variable wiring for `api`, `realtime-gateway`, and `admin` unless `railway.variablesMode` is set to `replace`
231
+ - keeps the legacy automatic variable wiring for `api`, `worker`, `realtime-gateway`, and `admin` unless `railway.variablesMode` is set to `replace`
205
232
  - supports `--diff` to show what would be added or changed compared with the current Railway variables
206
233
  - supports `--dry-run` to preview variable changes without applying them
207
234
 
@@ -246,7 +273,7 @@ npx -p create-asaje-go-vue@latest asaje destroy-railway ./my-app --scope project
246
273
  - reads the linked Railway project context
247
274
  - discovers the existing Railway app services from the linked project or `asaje.railway.json`
248
275
  - triggers fresh Railway builds/deployments for the configured app service list from the current local source tree
249
- - defaults to `api`, `realtime-gateway`, and `admin` when no custom Railway service config is present
276
+ - defaults to `api`, `worker`, `realtime-gateway`, and `admin` when no custom Railway service config is present
250
277
  - supports `--service` and `--services` to redeploy only selected app services
251
278
  - supports `--dry-run` to preview which services would be redeployed
252
279
 
@@ -268,6 +295,13 @@ If the `railway` block is omitted, the CLI keeps the default built-in services a
268
295
  "directory": "api",
269
296
  "dockerfile": "api/Dockerfile"
270
297
  },
298
+ {
299
+ "key": "worker",
300
+ "directory": "api",
301
+ "baseName": "worker",
302
+ "aliases": ["worker", "api-worker"],
303
+ "dockerfile": "api/Dockerfile"
304
+ },
271
305
  {
272
306
  "key": "admin",
273
307
  "directory": "admin",
@@ -280,12 +314,6 @@ If the `railway` block is omitted, the CLI keeps the default built-in services a
280
314
  "aliases": ["realtime-gateway"],
281
315
  "dockerfile": "realtime-gateway/Dockerfile"
282
316
  },
283
- {
284
- "key": "worker",
285
- "directory": "worker-api",
286
- "baseName": "worker",
287
- "dockerfile": "worker-api/Dockerfile"
288
- },
289
317
  {
290
318
  "key": "marketing",
291
319
  "directory": "marketing",
@@ -386,7 +414,8 @@ Notes:
386
414
  - the service directory should contain the `Dockerfile` Railway will build from
387
415
  - custom services are provisioned and deployed by `setup-railway` and `deploy-railway`
388
416
  - `sync-railway-env` can now apply declarative variables to any managed service key, including custom services
389
- - with `variablesMode: "merge"`, the core services `api`, `realtime-gateway`, and `admin` still receive the legacy generated defaults unless you override them
417
+ - with `variablesMode: "merge"`, the core services `api`, `worker`, `realtime-gateway`, and `admin` still receive the legacy generated defaults unless you override them
418
+ - the default `worker` service reuses the `api` image and starts with `API_COMMAND=worker`
390
419
  - with `variablesMode: "replace"`, only the variables declared in `asaje.config.json` are applied
391
420
  - after changing the Railway config, run `asaje update-railway ./my-app --yes` to reconcile the linked Railway project with the new configuration
392
421
  - you can target a custom service with `asaje deploy-railway ./my-app --service worker`
@@ -34,6 +34,12 @@ const DEFAULT_RAILWAY_APP_SERVICE_SPECS = [
34
34
  directory: "api",
35
35
  key: "api",
36
36
  },
37
+ {
38
+ aliases: ["worker", "api-worker"],
39
+ baseName: "worker",
40
+ directory: "api",
41
+ key: "worker",
42
+ },
37
43
  {
38
44
  aliases: ["admin", "frontend", "web"],
39
45
  baseName: "admin",
@@ -104,6 +110,12 @@ async function main() {
104
110
  return;
105
111
  }
106
112
 
113
+ if (invocation.command === "sync-project-config") {
114
+ await runSyncProjectConfig(invocation.argv);
115
+ outro(pc.green("Project config sync complete."));
116
+ return;
117
+ }
118
+
107
119
  if (invocation.command === "setup-railway") {
108
120
  await runSetupRailway(invocation.argv);
109
121
  outro(pc.green("Railway setup complete."));
@@ -202,6 +214,10 @@ function resolveInvocation(argv) {
202
214
  return { argv: rawArgs.slice(1), command: "update", title: "asaje update" };
203
215
  }
204
216
 
217
+ if (firstArg === "sync-project-config") {
218
+ return { argv: rawArgs.slice(1), command: "sync-project-config", title: "asaje sync-project-config" };
219
+ }
220
+
205
221
  if (firstArg === "setup-railway") {
206
222
  return { argv: rawArgs.slice(1), command: "setup-railway", title: "asaje setup-railway" };
207
223
  }
@@ -261,6 +277,10 @@ function resolveInvocation(argv) {
261
277
  return { argv: rawArgs.slice(1), command: "update", title: "create-asaje-go-vue" };
262
278
  }
263
279
 
280
+ if (firstArg === "sync-project-config") {
281
+ return { argv: rawArgs.slice(1), command: "sync-project-config", title: "create-asaje-go-vue" };
282
+ }
283
+
264
284
  if (firstArg === "setup-railway") {
265
285
  return { argv: rawArgs.slice(1), command: "setup-railway", title: "create-asaje-go-vue" };
266
286
  }
@@ -308,6 +328,7 @@ function printHelp() {
308
328
  console.log(`- ${pc.bold("asaje doctor [directory]")} check environment and project readiness`);
309
329
  console.log(`- ${pc.bold("asaje publish")} run npm publish checklist for the CLI package`);
310
330
  console.log(`- ${pc.bold("asaje update [directory]")} update managed boilerplate files from the template`);
331
+ console.log(`- ${pc.bold("asaje sync-project-config [directory]")} scan the project and rewrite Asaje config manifests`);
311
332
  console.log(`- ${pc.bold("asaje setup-railway [directory]")} provision Railway infrastructure for a project`);
312
333
  console.log(`- ${pc.bold("asaje update-railway [directory]")} reconcile Railway resources/services/vars from current config`);
313
334
  console.log(`- ${pc.bold("asaje sync-railway-env [directory]")} sync Railway app variables without provisioning`);
@@ -324,6 +345,7 @@ function printHelp() {
324
345
  console.log(`- ${pc.bold("asaje doctor ..")}`);
325
346
  console.log(`- ${pc.bold("asaje publish")}`);
326
347
  console.log(`- ${pc.bold("asaje update .. --dry-run")}`);
348
+ console.log(`- ${pc.bold("asaje sync-project-config .. --dry-run")}`);
327
349
  console.log(`- ${pc.bold("asaje setup-railway ..")}`);
328
350
  console.log(`- ${pc.bold("asaje update-railway ..")}`);
329
351
  console.log(`- ${pc.bold("asaje sync-railway-env ..")}`);
@@ -1207,6 +1229,34 @@ async function runUpdate(argv) {
1207
1229
  }
1208
1230
  }
1209
1231
 
1232
+ async function runSyncProjectConfig(argv) {
1233
+ const args = await collectSyncProjectConfigAnswers(parseSyncProjectConfigArgs(argv));
1234
+ const projectDir = path.resolve(process.cwd(), args.directory);
1235
+
1236
+ await ensureProjectStructure(projectDir);
1237
+
1238
+ const projectConfig = await loadProjectConfig(projectDir);
1239
+ const manifest = await readRailwayManifest(projectDir);
1240
+ const scanSummary = await scanProjectForManagedRailwayServices(projectDir);
1241
+ const nextProjectConfig = buildSyncedProjectConfig(projectDir, projectConfig, scanSummary.serviceSpecs);
1242
+ const nextManifest = buildSyncedRailwayManifest(manifest, nextProjectConfig, scanSummary.serviceSpecs);
1243
+
1244
+ if (!args.dryRun) {
1245
+ await writeProjectConfigFile(projectDir, nextProjectConfig);
1246
+ await writeRailwayManifest(projectDir, nextManifest);
1247
+ }
1248
+
1249
+ printSyncProjectConfigSummary({
1250
+ dryRun: args.dryRun,
1251
+ manifest,
1252
+ nextManifest,
1253
+ nextProjectConfig,
1254
+ previousProjectConfig: projectConfig,
1255
+ projectDir,
1256
+ scanSummary,
1257
+ });
1258
+ }
1259
+
1210
1260
  async function runSetupRailway(argv) {
1211
1261
  const args = parseSetupRailwayArgs(argv);
1212
1262
  const answers = await collectSetupRailwayAnswers(args);
@@ -1910,6 +1960,34 @@ function parseImportRailwayConfigArgs(argv) {
1910
1960
  return options;
1911
1961
  }
1912
1962
 
1963
+ function parseSyncProjectConfigArgs(argv) {
1964
+ const options = {
1965
+ directory: ".",
1966
+ dryRun: false,
1967
+ yes: false,
1968
+ };
1969
+ const positionals = [];
1970
+
1971
+ for (let index = 0; index < argv.length; index += 1) {
1972
+ const arg = argv[index];
1973
+
1974
+ if (arg === "--yes" || arg === "-y") {
1975
+ options.yes = true;
1976
+ continue;
1977
+ }
1978
+
1979
+ if (arg === "--dry-run") {
1980
+ options.dryRun = true;
1981
+ continue;
1982
+ }
1983
+
1984
+ positionals.push(arg);
1985
+ }
1986
+
1987
+ options.directory = positionals[0] || options.directory;
1988
+ return options;
1989
+ }
1990
+
1913
1991
  function parseDiffRailwayConfigArgs(argv) {
1914
1992
  const options = {
1915
1993
  compareEnvironment: undefined,
@@ -2235,6 +2313,25 @@ async function collectImportRailwayConfigAnswers(args) {
2235
2313
  return args;
2236
2314
  }
2237
2315
 
2316
+ async function collectSyncProjectConfigAnswers(args) {
2317
+ if (args.yes || args.dryRun) {
2318
+ return args;
2319
+ }
2320
+
2321
+ const confirmed = await prompt(
2322
+ confirm({
2323
+ initialValue: true,
2324
+ message: `Scan ${args.directory} and rewrite asaje.config.json / ${RAILWAY_MANIFEST_FILENAME}?`,
2325
+ }),
2326
+ );
2327
+
2328
+ if (!confirmed) {
2329
+ throw new Error("Project config sync cancelled.");
2330
+ }
2331
+
2332
+ return args;
2333
+ }
2334
+
2238
2335
  async function collectDestroyRailwayAnswers(args) {
2239
2336
  if (args.yes) {
2240
2337
  return args;
@@ -2979,6 +3076,7 @@ async function resolveRailwayVariablePlan(config) {
2979
3076
  admin: findRailwayServiceByKey(config.services, config.appServiceSpecs, config.manifest, "admin"),
2980
3077
  api: findRailwayServiceByKey(config.services, config.appServiceSpecs, config.manifest, "api"),
2981
3078
  realtime: findRailwayServiceByKey(config.services, config.appServiceSpecs, config.manifest, "realtime"),
3079
+ worker: findRailwayServiceByKey(config.services, config.appServiceSpecs, config.manifest, "worker"),
2982
3080
  };
2983
3081
 
2984
3082
  const serviceRegistry = {
@@ -2988,6 +3086,7 @@ async function resolveRailwayVariablePlan(config) {
2988
3086
  postgres: infra.postgres ? { name: infra.postgres.name, variables: {} } : null,
2989
3087
  rabbitmq: infra.rabbitmq ? { name: infra.rabbitmq.name, variables: {} } : null,
2990
3088
  realtime: appServices.realtime ? { name: appServices.realtime.name, variables: {} } : null,
3089
+ worker: appServices.worker ? { name: appServices.worker.name, variables: {} } : null,
2991
3090
  };
2992
3091
 
2993
3092
  for (const spec of config.appServiceSpecs) {
@@ -3018,6 +3117,9 @@ async function resolveRailwayVariablePlan(config) {
3018
3117
  if (!appServices.admin) {
3019
3118
  notices.push("admin service not found, skipping admin variable wiring");
3020
3119
  }
3120
+ if (!appServices.worker) {
3121
+ notices.push("worker service not found, skipping worker variable wiring");
3122
+ }
3021
3123
  if (!infra.postgres) {
3022
3124
  notices.push("postgres resource not found, DATABASE_URL wiring will be skipped");
3023
3125
  }
@@ -3079,6 +3181,26 @@ async function resolveRailwayVariablePlan(config) {
3079
3181
  mergeRailwayServiceVariables(serviceRegistry.admin, variables);
3080
3182
  }
3081
3183
 
3184
+ if (variablesMode !== "replace" && appServices.worker) {
3185
+ const existingApiVariables = appServices.api?.name
3186
+ ? await loadRailwayServiceVariables(config.projectDir, config.railwayContext.environmentRef, appServices.api.name)
3187
+ : {};
3188
+ const variables = {};
3189
+ const sharedSecrets = buildRailwaySharedSecrets(localEnv, existingApiVariables);
3190
+ Object.assign(variables, sharedSecrets.api);
3191
+ variables.API_COMMAND = "worker";
3192
+ if (infra.postgres?.name) {
3193
+ variables.DATABASE_URL = railwayReference(infra.postgres.name, "DATABASE_URL");
3194
+ }
3195
+ if (infra.rabbitmq?.name) {
3196
+ variables.RABBITMQ_URL = buildRabbitMqUrlReference(infra.rabbitmq.name);
3197
+ }
3198
+ if (infra.objectStorage?.name) {
3199
+ Object.assign(variables, buildObjectStorageVariables(infra.objectStorage.name));
3200
+ }
3201
+ mergeRailwayServiceVariables(serviceRegistry.worker, variables);
3202
+ }
3203
+
3082
3204
  if (declaredVariables.hasDeclaredVariables) {
3083
3205
  if (Object.keys(declaredVariables.sharedVariables).length > 0) {
3084
3206
  for (const entry of Object.values(serviceRegistry)) {
@@ -3818,6 +3940,7 @@ function printRailwaySetupSummary(config) {
3818
3940
  if (config.railwayContext.environmentName || config.railwayContext.environmentId) {
3819
3941
  console.log(`- Environment: ${pc.bold(config.railwayContext.environmentName || config.railwayContext.environmentId)}`);
3820
3942
  }
3943
+ console.log(`- Managed services: ${pc.bold("api, worker, realtime-gateway, admin")}`);
3821
3944
  console.log(`- Bucket: ${pc.bold(config.bucket)}`);
3822
3945
 
3823
3946
  console.log(pc.bold("\nResources"));
@@ -3878,6 +4001,7 @@ function printRailwayDeploySummary(config) {
3878
4001
  if (config.railwayContext.environmentName || config.railwayContext.environmentId) {
3879
4002
  console.log(`- Environment: ${pc.bold(config.railwayContext.environmentName || config.railwayContext.environmentId)}`);
3880
4003
  }
4004
+ console.log(`- Default managed services: ${pc.bold("api, worker, realtime-gateway, admin")}`);
3881
4005
  console.log(`- Services: ${pc.bold(config.selectedServices.join(", "))}`);
3882
4006
 
3883
4007
  console.log(pc.bold("\nDeployments"));
@@ -4149,6 +4273,227 @@ async function ensureRailwayAppServiceTargets(projectDir, appServiceSpecs) {
4149
4273
  }
4150
4274
  }
4151
4275
 
4276
+ async function scanProjectForManagedRailwayServices(projectDir) {
4277
+ const dockerfiles = [];
4278
+ await collectDockerfiles(projectDir, "", dockerfiles);
4279
+
4280
+ const scannedSpecs = dockerfiles
4281
+ .map((dockerfilePath) => buildScannedRailwayServiceSpec(projectDir, dockerfilePath))
4282
+ .filter(Boolean)
4283
+ .sort((left, right) => left.directory.localeCompare(right.directory));
4284
+
4285
+ const serviceSpecs = synthesizeDerivedRailwayServices(scannedSpecs);
4286
+
4287
+ return {
4288
+ dockerfiles,
4289
+ serviceSpecs,
4290
+ };
4291
+ }
4292
+
4293
+ function synthesizeDerivedRailwayServices(serviceSpecs) {
4294
+ const nextSpecs = [...serviceSpecs];
4295
+ const hasAPI = serviceSpecs.some((spec) => spec.key === "api" && spec.directory === "api");
4296
+ const hasWorker = serviceSpecs.some((spec) => spec.key === "worker");
4297
+ if (hasAPI && !hasWorker) {
4298
+ nextSpecs.push({
4299
+ aliases: ["worker", "api-worker"],
4300
+ baseName: "worker",
4301
+ directory: "api",
4302
+ dockerfile: "api/Dockerfile",
4303
+ key: "worker",
4304
+ seedImage: "alpine:3.22",
4305
+ serviceName: null,
4306
+ });
4307
+ }
4308
+
4309
+ return nextSpecs.sort((left, right) => `${left.directory}:${left.key}`.localeCompare(`${right.directory}:${right.key}`));
4310
+ }
4311
+
4312
+ async function collectDockerfiles(projectDir, relativeDir, results) {
4313
+ const absoluteDir = path.join(projectDir, relativeDir);
4314
+ const entries = await fs.readdir(absoluteDir, { withFileTypes: true });
4315
+
4316
+ for (const entry of entries) {
4317
+ const nextRelativePath = relativeDir ? path.posix.join(relativeDir, entry.name) : entry.name;
4318
+ if (entry.isDirectory()) {
4319
+ if (shouldSkipProjectScanDirectory(entry.name, nextRelativePath)) {
4320
+ continue;
4321
+ }
4322
+
4323
+ await collectDockerfiles(projectDir, nextRelativePath, results);
4324
+ continue;
4325
+ }
4326
+
4327
+ if (entry.isFile() && entry.name === "Dockerfile") {
4328
+ results.push(nextRelativePath);
4329
+ }
4330
+ }
4331
+ }
4332
+
4333
+ function shouldSkipProjectScanDirectory(name, relativePath) {
4334
+ const normalized = String(name || "").trim();
4335
+ if (!normalized) {
4336
+ return false;
4337
+ }
4338
+
4339
+ if ([".git", ".turbo", ".next", ".nuxt", "node_modules", "dist", "build", "coverage", "tmp", "vendor"].includes(normalized)) {
4340
+ return true;
4341
+ }
4342
+
4343
+ if (relativePath === "cli") {
4344
+ return true;
4345
+ }
4346
+
4347
+ return normalized.startsWith(".") && normalized !== ".well-known";
4348
+ }
4349
+
4350
+ function buildScannedRailwayServiceSpec(projectDir, dockerfilePath) {
4351
+ const directory = path.posix.dirname(dockerfilePath);
4352
+ if (!directory || directory === ".") {
4353
+ return null;
4354
+ }
4355
+
4356
+ const inferred = inferRailwayServiceIdentity(directory);
4357
+ return {
4358
+ aliases: inferred.aliases,
4359
+ baseName: inferred.baseName,
4360
+ directory,
4361
+ dockerfile: dockerfilePath,
4362
+ key: inferred.key,
4363
+ seedImage: inferred.key === "admin" ? "nginx:1.29-alpine" : "alpine:3.22",
4364
+ serviceName: null,
4365
+ };
4366
+ }
4367
+
4368
+ function inferRailwayServiceIdentity(directory) {
4369
+ const normalizedDirectory = directory.replace(/\/+$/g, "");
4370
+ const directoryName = path.posix.basename(normalizedDirectory);
4371
+ const normalizedName = normalizeRailwayServiceName(directoryName);
4372
+
4373
+ if (["api", "backend", "server"].includes(normalizedName)) {
4374
+ return { aliases: ["api", "backend", "server"], baseName: "api", key: "api" };
4375
+ }
4376
+ if (["admin", "frontend", "web"].includes(normalizedName)) {
4377
+ return { aliases: ["admin", "frontend", "web"], baseName: "admin", key: "admin" };
4378
+ }
4379
+ if (["realtime", "realtime-gateway"].includes(normalizedName)) {
4380
+ return { aliases: ["realtime-gateway", "realtime"], baseName: "realtime-gateway", key: "realtime" };
4381
+ }
4382
+
4383
+ const slug = slugify(directoryName);
4384
+ return { aliases: [slug], baseName: slug, key: slug };
4385
+ }
4386
+
4387
+ function buildSyncedProjectConfig(projectDir, projectConfig, scannedServiceSpecs) {
4388
+ const nextConfig = {
4389
+ ...(projectConfig || {}),
4390
+ projectName: projectConfig?.projectName || path.basename(projectDir),
4391
+ projectSlug: projectConfig?.projectSlug || slugify(path.basename(projectDir)),
4392
+ };
4393
+
4394
+ const previousServices = resolveRailwayAppServiceSpecs(projectConfig);
4395
+ const mergedServices = mergeScannedRailwayServices(previousServices, scannedServiceSpecs);
4396
+
4397
+ nextConfig.railway = {
4398
+ ...(getRailwayConfig(projectConfig) || {}),
4399
+ services: mergedServices.map((service) => ({
4400
+ baseName: service.baseName,
4401
+ directory: service.directory,
4402
+ ...(service.dockerfile ? { dockerfile: service.dockerfile } : {}),
4403
+ key: service.key,
4404
+ ...(service.aliases?.length > 0 ? { aliases: service.aliases } : {}),
4405
+ ...(service.serviceName ? { serviceName: service.serviceName } : {}),
4406
+ ...(service.seedImage ? { seedImage: service.seedImage } : {}),
4407
+ })),
4408
+ };
4409
+
4410
+ return nextConfig;
4411
+ }
4412
+
4413
+ function mergeScannedRailwayServices(previousServices, scannedServiceSpecs) {
4414
+ const previousByKey = new Map(previousServices.map((service) => [service.key, service]));
4415
+ const previousByDirectory = new Map(previousServices.map((service) => [service.directory, service]));
4416
+
4417
+ return scannedServiceSpecs.map((scanned) => {
4418
+ const previous = previousByKey.get(scanned.key) || previousByDirectory.get(scanned.directory);
4419
+ return {
4420
+ aliases: uniqueStrings([...(previous?.aliases || []), ...scanned.aliases]),
4421
+ baseName: previous?.baseName || scanned.baseName,
4422
+ directory: scanned.directory,
4423
+ dockerfile: scanned.dockerfile,
4424
+ key: previous?.key || scanned.key,
4425
+ seedImage: previous?.seedImage || scanned.seedImage,
4426
+ serviceName: previous?.serviceName || null,
4427
+ };
4428
+ });
4429
+ }
4430
+
4431
+ function buildSyncedRailwayManifest(manifest, nextProjectConfig, scannedServiceSpecs) {
4432
+ const nextManifest = {
4433
+ ...(manifest || {}),
4434
+ appServices: {},
4435
+ projectSlug: nextProjectConfig.projectSlug || manifest?.projectSlug || null,
4436
+ updatedAt: new Date().toISOString(),
4437
+ };
4438
+
4439
+ const previousAppServices = manifest?.appServices || {};
4440
+ const existingSpecs = resolveRailwayAppServiceSpecs(nextProjectConfig);
4441
+ for (const spec of existingSpecs) {
4442
+ const previousEntry =
4443
+ previousAppServices[spec.key] ||
4444
+ findRailwayManifestAppServiceByName(previousAppServices, resolveRailwayServiceName(spec, nextManifest.projectSlug));
4445
+
4446
+ nextManifest.appServices[spec.key] = {
4447
+ serviceId: previousEntry?.serviceId || null,
4448
+ serviceName: previousEntry?.serviceName || resolveRailwayServiceName(spec, nextManifest.projectSlug),
4449
+ };
4450
+ }
4451
+
4452
+ return nextManifest;
4453
+ }
4454
+
4455
+ function findRailwayManifestAppServiceByName(appServices, serviceName) {
4456
+ return Object.values(appServices || {}).find(
4457
+ (entry) => normalizeRailwayServiceName(entry?.serviceName) === normalizeRailwayServiceName(serviceName),
4458
+ ) || null;
4459
+ }
4460
+
4461
+ async function writeProjectConfigFile(projectDir, projectConfig) {
4462
+ const configPath = path.join(projectDir, "asaje.config.json");
4463
+ await fs.writeJson(configPath, projectConfig, { spaces: 2 });
4464
+ }
4465
+
4466
+ function printSyncProjectConfigSummary(config) {
4467
+ const previousServices = new Map(resolveRailwayAppServiceSpecs(config.previousProjectConfig).map((service) => [service.key, service]));
4468
+ const nextServices = config.nextProjectConfig.railway?.services || [];
4469
+
4470
+ console.log(pc.bold("\nProject config sync"));
4471
+ console.log(`- Directory: ${pc.bold(config.projectDir)}`);
4472
+ console.log(`- Dockerfiles found: ${pc.bold(String(config.scanSummary.dockerfiles.length))}`);
4473
+
4474
+ console.log(pc.bold("\nRailway services"));
4475
+ if (nextServices.length === 0) {
4476
+ console.log("- No managed Railway services detected");
4477
+ } else {
4478
+ for (const service of nextServices) {
4479
+ const previous = previousServices.get(service.key);
4480
+ const status = !previous ? "new" : previous.directory !== service.directory || previous.dockerfile !== service.dockerfile ? "updated" : "unchanged";
4481
+ console.log(`- ${pc.bold(service.key)}: ${status} (${service.directory})`);
4482
+ }
4483
+ }
4484
+
4485
+ console.log(pc.bold("\nFiles"));
4486
+ console.log(`- ${config.dryRun ? "would write" : "wrote"} ${pc.bold("asaje.config.json")}`);
4487
+ console.log(`- ${config.dryRun ? "would write" : "wrote"} ${pc.bold(RAILWAY_MANIFEST_FILENAME)}`);
4488
+ if (config.dryRun) {
4489
+ console.log("- Dry run only, local files were not modified");
4490
+ }
4491
+ }
4492
+
4493
+ function uniqueStrings(values) {
4494
+ return [...new Set((values || []).map((value) => String(value || "").trim()).filter(Boolean))];
4495
+ }
4496
+
4152
4497
  function resolveProjectSlug(projectDir, projectConfig) {
4153
4498
  return slugify(projectConfig?.projectSlug || projectConfig?.projectName || path.basename(projectDir) || "asaje-app");
4154
4499
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-asaje-go-vue",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "CLI to scaffold, configure, and run the Asaje Go + Vue boilerplate",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "create": "node ./bin/create-asaje-go-vue.js",
12
12
  "start": "node ./bin/asaje.js start .",
13
13
  "doctor": "node ./bin/asaje.js doctor ..",
14
+ "sync-project-config": "node ./bin/asaje.js sync-project-config .. --dry-run",
14
15
  "setup-railway": "node ./bin/asaje.js setup-railway .. --yes",
15
16
  "update-railway": "node ./bin/asaje.js update-railway .. --yes",
16
17
  "sync-railway-env": "node ./bin/asaje.js sync-railway-env .. --yes",