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 +40 -11
- package/bin/create-asaje-go-vue.js +345 -0
- package/package.json +2 -1
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.
|
|
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",
|