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