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