create-asaje-go-vue 0.3.0 → 0.3.1

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
@@ -171,6 +179,15 @@ npx -p create-asaje-go-vue@latest asaje destroy-railway ./my-app --scope project
171
179
  - supports `--dry-run` to preview which files would be updated
172
180
  - updates `asaje.config.json` with the template repository and branch used for the update
173
181
 
182
+ ## What `asaje sync-project-config` does
183
+
184
+ - scans the project tree for service-local `Dockerfile` files
185
+ - infers Railway managed services from the detected directories
186
+ - preserves existing service metadata when possible, while updating directories and Dockerfile paths from the scan
187
+ - rewrites `asaje.config.json` with the merged `railway.services` list
188
+ - rewrites `asaje.railway.json` so local service names line up with the current managed service list
189
+ - supports `--dry-run` to preview the rewrite without changing local files
190
+
174
191
  ## What `asaje setup-railway` does
175
192
 
176
193
  - validates the target project structure
@@ -104,6 +104,12 @@ async function main() {
104
104
  return;
105
105
  }
106
106
 
107
+ if (invocation.command === "sync-project-config") {
108
+ await runSyncProjectConfig(invocation.argv);
109
+ outro(pc.green("Project config sync complete."));
110
+ return;
111
+ }
112
+
107
113
  if (invocation.command === "setup-railway") {
108
114
  await runSetupRailway(invocation.argv);
109
115
  outro(pc.green("Railway setup complete."));
@@ -202,6 +208,10 @@ function resolveInvocation(argv) {
202
208
  return { argv: rawArgs.slice(1), command: "update", title: "asaje update" };
203
209
  }
204
210
 
211
+ if (firstArg === "sync-project-config") {
212
+ return { argv: rawArgs.slice(1), command: "sync-project-config", title: "asaje sync-project-config" };
213
+ }
214
+
205
215
  if (firstArg === "setup-railway") {
206
216
  return { argv: rawArgs.slice(1), command: "setup-railway", title: "asaje setup-railway" };
207
217
  }
@@ -261,6 +271,10 @@ function resolveInvocation(argv) {
261
271
  return { argv: rawArgs.slice(1), command: "update", title: "create-asaje-go-vue" };
262
272
  }
263
273
 
274
+ if (firstArg === "sync-project-config") {
275
+ return { argv: rawArgs.slice(1), command: "sync-project-config", title: "create-asaje-go-vue" };
276
+ }
277
+
264
278
  if (firstArg === "setup-railway") {
265
279
  return { argv: rawArgs.slice(1), command: "setup-railway", title: "create-asaje-go-vue" };
266
280
  }
@@ -308,6 +322,7 @@ function printHelp() {
308
322
  console.log(`- ${pc.bold("asaje doctor [directory]")} check environment and project readiness`);
309
323
  console.log(`- ${pc.bold("asaje publish")} run npm publish checklist for the CLI package`);
310
324
  console.log(`- ${pc.bold("asaje update [directory]")} update managed boilerplate files from the template`);
325
+ console.log(`- ${pc.bold("asaje sync-project-config [directory]")} scan the project and rewrite Asaje config manifests`);
311
326
  console.log(`- ${pc.bold("asaje setup-railway [directory]")} provision Railway infrastructure for a project`);
312
327
  console.log(`- ${pc.bold("asaje update-railway [directory]")} reconcile Railway resources/services/vars from current config`);
313
328
  console.log(`- ${pc.bold("asaje sync-railway-env [directory]")} sync Railway app variables without provisioning`);
@@ -324,6 +339,7 @@ function printHelp() {
324
339
  console.log(`- ${pc.bold("asaje doctor ..")}`);
325
340
  console.log(`- ${pc.bold("asaje publish")}`);
326
341
  console.log(`- ${pc.bold("asaje update .. --dry-run")}`);
342
+ console.log(`- ${pc.bold("asaje sync-project-config .. --dry-run")}`);
327
343
  console.log(`- ${pc.bold("asaje setup-railway ..")}`);
328
344
  console.log(`- ${pc.bold("asaje update-railway ..")}`);
329
345
  console.log(`- ${pc.bold("asaje sync-railway-env ..")}`);
@@ -1207,6 +1223,34 @@ async function runUpdate(argv) {
1207
1223
  }
1208
1224
  }
1209
1225
 
1226
+ async function runSyncProjectConfig(argv) {
1227
+ const args = await collectSyncProjectConfigAnswers(parseSyncProjectConfigArgs(argv));
1228
+ const projectDir = path.resolve(process.cwd(), args.directory);
1229
+
1230
+ await ensureProjectStructure(projectDir);
1231
+
1232
+ const projectConfig = await loadProjectConfig(projectDir);
1233
+ const manifest = await readRailwayManifest(projectDir);
1234
+ const scanSummary = await scanProjectForManagedRailwayServices(projectDir);
1235
+ const nextProjectConfig = buildSyncedProjectConfig(projectDir, projectConfig, scanSummary.serviceSpecs);
1236
+ const nextManifest = buildSyncedRailwayManifest(manifest, nextProjectConfig, scanSummary.serviceSpecs);
1237
+
1238
+ if (!args.dryRun) {
1239
+ await writeProjectConfigFile(projectDir, nextProjectConfig);
1240
+ await writeRailwayManifest(projectDir, nextManifest);
1241
+ }
1242
+
1243
+ printSyncProjectConfigSummary({
1244
+ dryRun: args.dryRun,
1245
+ manifest,
1246
+ nextManifest,
1247
+ nextProjectConfig,
1248
+ previousProjectConfig: projectConfig,
1249
+ projectDir,
1250
+ scanSummary,
1251
+ });
1252
+ }
1253
+
1210
1254
  async function runSetupRailway(argv) {
1211
1255
  const args = parseSetupRailwayArgs(argv);
1212
1256
  const answers = await collectSetupRailwayAnswers(args);
@@ -1910,6 +1954,34 @@ function parseImportRailwayConfigArgs(argv) {
1910
1954
  return options;
1911
1955
  }
1912
1956
 
1957
+ function parseSyncProjectConfigArgs(argv) {
1958
+ const options = {
1959
+ directory: ".",
1960
+ dryRun: false,
1961
+ yes: false,
1962
+ };
1963
+ const positionals = [];
1964
+
1965
+ for (let index = 0; index < argv.length; index += 1) {
1966
+ const arg = argv[index];
1967
+
1968
+ if (arg === "--yes" || arg === "-y") {
1969
+ options.yes = true;
1970
+ continue;
1971
+ }
1972
+
1973
+ if (arg === "--dry-run") {
1974
+ options.dryRun = true;
1975
+ continue;
1976
+ }
1977
+
1978
+ positionals.push(arg);
1979
+ }
1980
+
1981
+ options.directory = positionals[0] || options.directory;
1982
+ return options;
1983
+ }
1984
+
1913
1985
  function parseDiffRailwayConfigArgs(argv) {
1914
1986
  const options = {
1915
1987
  compareEnvironment: undefined,
@@ -2235,6 +2307,25 @@ async function collectImportRailwayConfigAnswers(args) {
2235
2307
  return args;
2236
2308
  }
2237
2309
 
2310
+ async function collectSyncProjectConfigAnswers(args) {
2311
+ if (args.yes || args.dryRun) {
2312
+ return args;
2313
+ }
2314
+
2315
+ const confirmed = await prompt(
2316
+ confirm({
2317
+ initialValue: true,
2318
+ message: `Scan ${args.directory} and rewrite asaje.config.json / ${RAILWAY_MANIFEST_FILENAME}?`,
2319
+ }),
2320
+ );
2321
+
2322
+ if (!confirmed) {
2323
+ throw new Error("Project config sync cancelled.");
2324
+ }
2325
+
2326
+ return args;
2327
+ }
2328
+
2238
2329
  async function collectDestroyRailwayAnswers(args) {
2239
2330
  if (args.yes) {
2240
2331
  return args;
@@ -4149,6 +4240,206 @@ async function ensureRailwayAppServiceTargets(projectDir, appServiceSpecs) {
4149
4240
  }
4150
4241
  }
4151
4242
 
4243
+ async function scanProjectForManagedRailwayServices(projectDir) {
4244
+ const dockerfiles = [];
4245
+ await collectDockerfiles(projectDir, "", dockerfiles);
4246
+
4247
+ const serviceSpecs = dockerfiles
4248
+ .map((dockerfilePath) => buildScannedRailwayServiceSpec(projectDir, dockerfilePath))
4249
+ .filter(Boolean)
4250
+ .sort((left, right) => left.directory.localeCompare(right.directory));
4251
+
4252
+ return {
4253
+ dockerfiles,
4254
+ serviceSpecs,
4255
+ };
4256
+ }
4257
+
4258
+ async function collectDockerfiles(projectDir, relativeDir, results) {
4259
+ const absoluteDir = path.join(projectDir, relativeDir);
4260
+ const entries = await fs.readdir(absoluteDir, { withFileTypes: true });
4261
+
4262
+ for (const entry of entries) {
4263
+ const nextRelativePath = relativeDir ? path.posix.join(relativeDir, entry.name) : entry.name;
4264
+ if (entry.isDirectory()) {
4265
+ if (shouldSkipProjectScanDirectory(entry.name, nextRelativePath)) {
4266
+ continue;
4267
+ }
4268
+
4269
+ await collectDockerfiles(projectDir, nextRelativePath, results);
4270
+ continue;
4271
+ }
4272
+
4273
+ if (entry.isFile() && entry.name === "Dockerfile") {
4274
+ results.push(nextRelativePath);
4275
+ }
4276
+ }
4277
+ }
4278
+
4279
+ function shouldSkipProjectScanDirectory(name, relativePath) {
4280
+ const normalized = String(name || "").trim();
4281
+ if (!normalized) {
4282
+ return false;
4283
+ }
4284
+
4285
+ if ([".git", ".turbo", ".next", ".nuxt", "node_modules", "dist", "build", "coverage", "tmp", "vendor"].includes(normalized)) {
4286
+ return true;
4287
+ }
4288
+
4289
+ if (relativePath === "cli") {
4290
+ return true;
4291
+ }
4292
+
4293
+ return normalized.startsWith(".") && normalized !== ".well-known";
4294
+ }
4295
+
4296
+ function buildScannedRailwayServiceSpec(projectDir, dockerfilePath) {
4297
+ const directory = path.posix.dirname(dockerfilePath);
4298
+ if (!directory || directory === ".") {
4299
+ return null;
4300
+ }
4301
+
4302
+ const inferred = inferRailwayServiceIdentity(directory);
4303
+ return {
4304
+ aliases: inferred.aliases,
4305
+ baseName: inferred.baseName,
4306
+ directory,
4307
+ dockerfile: dockerfilePath,
4308
+ key: inferred.key,
4309
+ seedImage: inferred.key === "admin" ? "nginx:1.29-alpine" : "alpine:3.22",
4310
+ serviceName: null,
4311
+ };
4312
+ }
4313
+
4314
+ function inferRailwayServiceIdentity(directory) {
4315
+ const normalizedDirectory = directory.replace(/\/+$/g, "");
4316
+ const directoryName = path.posix.basename(normalizedDirectory);
4317
+ const normalizedName = normalizeRailwayServiceName(directoryName);
4318
+
4319
+ if (["api", "backend", "server"].includes(normalizedName)) {
4320
+ return { aliases: ["api", "backend", "server"], baseName: "api", key: "api" };
4321
+ }
4322
+ if (["admin", "frontend", "web"].includes(normalizedName)) {
4323
+ return { aliases: ["admin", "frontend", "web"], baseName: "admin", key: "admin" };
4324
+ }
4325
+ if (["realtime", "realtime-gateway"].includes(normalizedName)) {
4326
+ return { aliases: ["realtime-gateway", "realtime"], baseName: "realtime-gateway", key: "realtime" };
4327
+ }
4328
+
4329
+ const slug = slugify(directoryName);
4330
+ return { aliases: [slug], baseName: slug, key: slug };
4331
+ }
4332
+
4333
+ function buildSyncedProjectConfig(projectDir, projectConfig, scannedServiceSpecs) {
4334
+ const nextConfig = {
4335
+ ...(projectConfig || {}),
4336
+ projectName: projectConfig?.projectName || path.basename(projectDir),
4337
+ projectSlug: projectConfig?.projectSlug || slugify(path.basename(projectDir)),
4338
+ };
4339
+
4340
+ const previousServices = resolveRailwayAppServiceSpecs(projectConfig);
4341
+ const mergedServices = mergeScannedRailwayServices(previousServices, scannedServiceSpecs);
4342
+
4343
+ nextConfig.railway = {
4344
+ ...(getRailwayConfig(projectConfig) || {}),
4345
+ services: mergedServices.map((service) => ({
4346
+ baseName: service.baseName,
4347
+ directory: service.directory,
4348
+ ...(service.dockerfile ? { dockerfile: service.dockerfile } : {}),
4349
+ key: service.key,
4350
+ ...(service.aliases?.length > 0 ? { aliases: service.aliases } : {}),
4351
+ ...(service.serviceName ? { serviceName: service.serviceName } : {}),
4352
+ ...(service.seedImage ? { seedImage: service.seedImage } : {}),
4353
+ })),
4354
+ };
4355
+
4356
+ return nextConfig;
4357
+ }
4358
+
4359
+ function mergeScannedRailwayServices(previousServices, scannedServiceSpecs) {
4360
+ const previousByKey = new Map(previousServices.map((service) => [service.key, service]));
4361
+ const previousByDirectory = new Map(previousServices.map((service) => [service.directory, service]));
4362
+
4363
+ return scannedServiceSpecs.map((scanned) => {
4364
+ const previous = previousByKey.get(scanned.key) || previousByDirectory.get(scanned.directory);
4365
+ return {
4366
+ aliases: uniqueStrings([...(previous?.aliases || []), ...scanned.aliases]),
4367
+ baseName: previous?.baseName || scanned.baseName,
4368
+ directory: scanned.directory,
4369
+ dockerfile: scanned.dockerfile,
4370
+ key: previous?.key || scanned.key,
4371
+ seedImage: previous?.seedImage || scanned.seedImage,
4372
+ serviceName: previous?.serviceName || null,
4373
+ };
4374
+ });
4375
+ }
4376
+
4377
+ function buildSyncedRailwayManifest(manifest, nextProjectConfig, scannedServiceSpecs) {
4378
+ const nextManifest = {
4379
+ ...(manifest || {}),
4380
+ appServices: {},
4381
+ projectSlug: nextProjectConfig.projectSlug || manifest?.projectSlug || null,
4382
+ updatedAt: new Date().toISOString(),
4383
+ };
4384
+
4385
+ const previousAppServices = manifest?.appServices || {};
4386
+ const existingSpecs = resolveRailwayAppServiceSpecs(nextProjectConfig);
4387
+ for (const spec of existingSpecs) {
4388
+ const previousEntry =
4389
+ previousAppServices[spec.key] ||
4390
+ findRailwayManifestAppServiceByName(previousAppServices, resolveRailwayServiceName(spec, nextManifest.projectSlug));
4391
+
4392
+ nextManifest.appServices[spec.key] = {
4393
+ serviceId: previousEntry?.serviceId || null,
4394
+ serviceName: previousEntry?.serviceName || resolveRailwayServiceName(spec, nextManifest.projectSlug),
4395
+ };
4396
+ }
4397
+
4398
+ return nextManifest;
4399
+ }
4400
+
4401
+ function findRailwayManifestAppServiceByName(appServices, serviceName) {
4402
+ return Object.values(appServices || {}).find(
4403
+ (entry) => normalizeRailwayServiceName(entry?.serviceName) === normalizeRailwayServiceName(serviceName),
4404
+ ) || null;
4405
+ }
4406
+
4407
+ async function writeProjectConfigFile(projectDir, projectConfig) {
4408
+ const configPath = path.join(projectDir, "asaje.config.json");
4409
+ await fs.writeJson(configPath, projectConfig, { spaces: 2 });
4410
+ }
4411
+
4412
+ function printSyncProjectConfigSummary(config) {
4413
+ const previousServices = new Map(resolveRailwayAppServiceSpecs(config.previousProjectConfig).map((service) => [service.key, service]));
4414
+ const nextServices = config.nextProjectConfig.railway?.services || [];
4415
+
4416
+ console.log(pc.bold("\nProject config sync"));
4417
+ console.log(`- Directory: ${pc.bold(config.projectDir)}`);
4418
+ console.log(`- Dockerfiles found: ${pc.bold(String(config.scanSummary.dockerfiles.length))}`);
4419
+
4420
+ console.log(pc.bold("\nRailway services"));
4421
+ if (nextServices.length === 0) {
4422
+ console.log("- No managed Railway services detected");
4423
+ } else {
4424
+ for (const service of nextServices) {
4425
+ const previous = previousServices.get(service.key);
4426
+ const status = !previous ? "new" : previous.directory !== service.directory || previous.dockerfile !== service.dockerfile ? "updated" : "unchanged";
4427
+ console.log(`- ${pc.bold(service.key)}: ${status} (${service.directory})`);
4428
+ }
4429
+ }
4430
+
4431
+ console.log(pc.bold("\nFiles"));
4432
+ console.log(`- ${config.dryRun ? "would write" : "wrote"} ${pc.bold("asaje.config.json")}`);
4433
+ console.log(`- ${config.dryRun ? "would write" : "wrote"} ${pc.bold(RAILWAY_MANIFEST_FILENAME)}`);
4434
+ if (config.dryRun) {
4435
+ console.log("- Dry run only, local files were not modified");
4436
+ }
4437
+ }
4438
+
4439
+ function uniqueStrings(values) {
4440
+ return [...new Set((values || []).map((value) => String(value || "").trim()).filter(Boolean))];
4441
+ }
4442
+
4152
4443
  function resolveProjectSlug(projectDir, projectConfig) {
4153
4444
  return slugify(projectConfig?.projectSlug || projectConfig?.projectName || path.basename(projectDir) || "asaje-app");
4154
4445
  }
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.1",
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",