create-asaje-go-vue 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/bin/create-asaje-go-vue.js +1258 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -44,6 +44,20 @@ npx -p create-asaje-go-vue@latest asaje doctor ./my-app
|
|
|
44
44
|
npx -p create-asaje-go-vue@latest asaje publish
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
### Provision Railway resources
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx -p create-asaje-go-vue@latest asaje setup-railway ./my-app
|
|
51
|
+
npx -p create-asaje-go-vue@latest asaje setup-railway ./my-app --dry-run
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Sync Railway app variables
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx -p create-asaje-go-vue@latest asaje sync-railway-env ./my-app
|
|
58
|
+
npx -p create-asaje-go-vue@latest asaje sync-railway-env ./my-app --dry-run
|
|
59
|
+
```
|
|
60
|
+
|
|
47
61
|
## What `create` does
|
|
48
62
|
|
|
49
63
|
- clones the boilerplate from GitHub with `degit`
|
|
@@ -75,6 +89,28 @@ npx -p create-asaje-go-vue@latest asaje publish
|
|
|
75
89
|
- runs `npm run pack:dry-run`
|
|
76
90
|
- prints the final manual npm release steps
|
|
77
91
|
|
|
92
|
+
## What `asaje setup-railway` does
|
|
93
|
+
|
|
94
|
+
- validates the target project structure
|
|
95
|
+
- checks that the Railway CLI is installed and authenticated
|
|
96
|
+
- reads the linked Railway project context
|
|
97
|
+
- provisions PostgreSQL, RabbitMQ, and S3-compatible object storage on Railway
|
|
98
|
+
- creates missing Railway app services for `api`, `realtime-gateway`, and `admin`
|
|
99
|
+
- wires Railway variables for `api`, `realtime-gateway`, and `admin`
|
|
100
|
+
- triggers the first Railway deployment for each app service using the service-local `Dockerfile` and `railway.json`
|
|
101
|
+
- generates missing app secrets such as `JWT_SECRET` and `SWAGGER_PASSWORD`, while reusing existing Railway values when present
|
|
102
|
+
- supports `--dry-run` to preview provisioning and variable changes without applying them
|
|
103
|
+
- writes an `asaje.railway.json` manifest in the target project for future runs, including discovered Railway app service names
|
|
104
|
+
|
|
105
|
+
## What `asaje sync-railway-env` does
|
|
106
|
+
|
|
107
|
+
- validates the target project structure
|
|
108
|
+
- checks that the Railway CLI is installed and authenticated
|
|
109
|
+
- reads the linked Railway project context
|
|
110
|
+
- discovers existing Railway app and infra services
|
|
111
|
+
- syncs variables for `api`, `realtime-gateway`, and `admin` without provisioning infra resources
|
|
112
|
+
- supports `--dry-run` to preview variable changes without applying them
|
|
113
|
+
|
|
78
114
|
## Useful flags
|
|
79
115
|
|
|
80
116
|
```bash
|
|
@@ -85,6 +121,9 @@ node ./bin/asaje.js start ../my-app --yes --profile frontend-only
|
|
|
85
121
|
node ./bin/asaje.js start ../my-app --yes --skip-admin --skip-worker
|
|
86
122
|
node ./bin/asaje.js doctor ../my-app
|
|
87
123
|
node ./bin/asaje.js publish .
|
|
124
|
+
node ./bin/asaje.js setup-railway ../my-app --yes
|
|
125
|
+
node ./bin/asaje.js setup-railway ../my-app --yes --dry-run
|
|
126
|
+
node ./bin/asaje.js sync-railway-env ../my-app --yes
|
|
88
127
|
```
|
|
89
128
|
|
|
90
129
|
## Publish checklist
|
|
@@ -103,5 +142,6 @@ npm publish
|
|
|
103
142
|
- default template repo comes from `ASAJE_TEMPLATE_REPO` or falls back to `asaje379/boilerplate-go-vue`
|
|
104
143
|
- default branch comes from `ASAJE_TEMPLATE_BRANCH` or falls back to `main`
|
|
105
144
|
- if the selected branch is missing, the CLI retries `main`, `master`, then `develop`
|
|
145
|
+
- `asaje setup-railway` works best with `RAILWAY_API_TOKEN` or `RAILWAY_TOKEN` set so it can verify existing remote services before provisioning
|
|
106
146
|
- the package currently uses `UNLICENSED`; change that before public distribution if needed
|
|
107
147
|
- OTP email delivery still requires a valid Mailchimp Transactional key for real email sends
|
|
@@ -21,6 +21,31 @@ import pc from "picocolors";
|
|
|
21
21
|
const DEFAULT_TEMPLATE = process.env.ASAJE_TEMPLATE_REPO || "asaje379/boilerplate-go-vue";
|
|
22
22
|
const DEFAULT_BRANCH = process.env.ASAJE_TEMPLATE_BRANCH || "main";
|
|
23
23
|
const EXCLUDED_TEMPLATE_PATHS = ["cli"];
|
|
24
|
+
const RAILWAY_GRAPHQL_ENDPOINT = "https://backboard.railway.com/graphql/v2";
|
|
25
|
+
const RAILWAY_MANIFEST_FILENAME = "asaje.railway.json";
|
|
26
|
+
const DEFAULT_RAILWAY_BUCKET = "boilerplate-files";
|
|
27
|
+
const RAILWAY_SERVICE_DISCOVERY_RETRY_DELAY_MS = 2000;
|
|
28
|
+
const RAILWAY_SERVICE_DISCOVERY_RETRY_COUNT = 5;
|
|
29
|
+
const RAILWAY_APP_SERVICE_SPECS = [
|
|
30
|
+
{
|
|
31
|
+
aliases: ["api", "backend", "server"],
|
|
32
|
+
directory: "api",
|
|
33
|
+
key: "api",
|
|
34
|
+
serviceName: "api",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
aliases: ["admin", "frontend", "web"],
|
|
38
|
+
directory: "admin",
|
|
39
|
+
key: "admin",
|
|
40
|
+
serviceName: "admin",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
aliases: ["realtime-gateway", "realtime"],
|
|
44
|
+
directory: "realtime-gateway",
|
|
45
|
+
key: "realtime",
|
|
46
|
+
serviceName: "realtime-gateway",
|
|
47
|
+
},
|
|
48
|
+
];
|
|
24
49
|
const ENV_FILE_SPECS = [
|
|
25
50
|
{ envPath: "admin/.env", examplePath: "admin/.env.example" },
|
|
26
51
|
{ envPath: "api/.env", examplePath: "api/.env.example" },
|
|
@@ -58,6 +83,18 @@ async function main() {
|
|
|
58
83
|
return;
|
|
59
84
|
}
|
|
60
85
|
|
|
86
|
+
if (invocation.command === "setup-railway") {
|
|
87
|
+
await runSetupRailway(invocation.argv);
|
|
88
|
+
outro(pc.green("Railway setup complete."));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (invocation.command === "sync-railway-env") {
|
|
93
|
+
await runSyncRailwayEnv(invocation.argv);
|
|
94
|
+
outro(pc.green("Railway environment sync complete."));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
61
98
|
await runCreate(invocation.argv);
|
|
62
99
|
outro(pc.green("Project ready."));
|
|
63
100
|
} catch (error) {
|
|
@@ -98,6 +135,14 @@ function resolveInvocation(argv) {
|
|
|
98
135
|
return { argv: rawArgs.slice(1), command: "publish", title: "asaje publish" };
|
|
99
136
|
}
|
|
100
137
|
|
|
138
|
+
if (firstArg === "setup-railway") {
|
|
139
|
+
return { argv: rawArgs.slice(1), command: "setup-railway", title: "asaje setup-railway" };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (firstArg === "sync-railway-env") {
|
|
143
|
+
return { argv: rawArgs.slice(1), command: "sync-railway-env", title: "asaje sync-railway-env" };
|
|
144
|
+
}
|
|
145
|
+
|
|
101
146
|
if (firstArg === "create") {
|
|
102
147
|
return { argv: rawArgs.slice(1), command: "create", title: "asaje create" };
|
|
103
148
|
}
|
|
@@ -127,12 +172,16 @@ function printHelp() {
|
|
|
127
172
|
console.log(`- ${pc.bold("asaje start [directory]")} start a configured project`);
|
|
128
173
|
console.log(`- ${pc.bold("asaje doctor [directory]")} check environment and project readiness`);
|
|
129
174
|
console.log(`- ${pc.bold("asaje publish")} run npm publish checklist for the CLI package`);
|
|
175
|
+
console.log(`- ${pc.bold("asaje setup-railway [directory]")} provision Railway infrastructure for a project`);
|
|
176
|
+
console.log(`- ${pc.bold("asaje sync-railway-env [directory]")} sync Railway app variables without provisioning`);
|
|
130
177
|
console.log(pc.bold("\nExamples"));
|
|
131
178
|
console.log(`- ${pc.bold("npx create-asaje-go-vue my-app")}`);
|
|
132
179
|
console.log(`- ${pc.bold("node ./bin/create-asaje-go-vue.js my-app --yes")}`);
|
|
133
180
|
console.log(`- ${pc.bold("node ./bin/asaje.js start ../my-app")}`);
|
|
134
181
|
console.log(`- ${pc.bold("node ./bin/asaje.js doctor ..")}`);
|
|
135
182
|
console.log(`- ${pc.bold("node ./bin/asaje.js publish")}`);
|
|
183
|
+
console.log(`- ${pc.bold("node ./bin/asaje.js setup-railway ..")}`);
|
|
184
|
+
console.log(`- ${pc.bold("node ./bin/asaje.js sync-railway-env ..")}`);
|
|
136
185
|
}
|
|
137
186
|
|
|
138
187
|
async function runCreate(argv) {
|
|
@@ -862,10 +911,1219 @@ async function runPublish(argv) {
|
|
|
862
911
|
console.log(`- Publish with ${pc.bold("npm publish")}`);
|
|
863
912
|
}
|
|
864
913
|
|
|
914
|
+
async function runSetupRailway(argv) {
|
|
915
|
+
const args = parseSetupRailwayArgs(argv);
|
|
916
|
+
const answers = await collectSetupRailwayAnswers(args);
|
|
917
|
+
const projectDir = path.resolve(process.cwd(), answers.directory);
|
|
918
|
+
|
|
919
|
+
await ensureProjectStructure(projectDir);
|
|
920
|
+
await ensureRailwayCliInstalled();
|
|
921
|
+
await ensureRailwayAuthenticated(projectDir, answers.environment);
|
|
922
|
+
|
|
923
|
+
const manifest = await readRailwayManifest(projectDir);
|
|
924
|
+
manifest.resources ||= {};
|
|
925
|
+
const railwayContext = await loadRailwayContext(projectDir, answers.environment);
|
|
926
|
+
railwayContext.environmentRef = answers.environment || railwayContext.environmentId || railwayContext.environmentName;
|
|
927
|
+
const existingServices = await discoverRailwayServices(railwayContext, projectDir);
|
|
928
|
+
const resourceSummary = [];
|
|
929
|
+
const appServiceSummary = [];
|
|
930
|
+
const deploySummary = [];
|
|
931
|
+
const variableSummary = [];
|
|
932
|
+
|
|
933
|
+
console.log(pc.bold("\nProvisioning"));
|
|
934
|
+
|
|
935
|
+
const postgresResult = await ensureRailwayResource({
|
|
936
|
+
aliases: ["postgres", "postgresql"],
|
|
937
|
+
commandArgs: ["add", "--database", "postgres"],
|
|
938
|
+
dryRun: answers.dryRun,
|
|
939
|
+
existingServices,
|
|
940
|
+
key: "postgres",
|
|
941
|
+
manifest,
|
|
942
|
+
projectDir,
|
|
943
|
+
railwayContext,
|
|
944
|
+
});
|
|
945
|
+
resourceSummary.push(postgresResult);
|
|
946
|
+
|
|
947
|
+
const rabbitMqResult = await ensureRailwayResource({
|
|
948
|
+
aliases: ["rabbitmq"],
|
|
949
|
+
commandArgs: ["deploy", "--template", "RabbitMQ"],
|
|
950
|
+
dryRun: answers.dryRun,
|
|
951
|
+
existingServices,
|
|
952
|
+
key: "rabbitmq",
|
|
953
|
+
manifest,
|
|
954
|
+
projectDir,
|
|
955
|
+
railwayContext,
|
|
956
|
+
});
|
|
957
|
+
resourceSummary.push(rabbitMqResult);
|
|
958
|
+
|
|
959
|
+
const objectStorageResult = await ensureRailwayResource({
|
|
960
|
+
aliases: ["object-storage", "storage", "simple-s3", "minio"],
|
|
961
|
+
commandArgs: [
|
|
962
|
+
"deploy",
|
|
963
|
+
"--template",
|
|
964
|
+
"simple-s3",
|
|
965
|
+
"--variable",
|
|
966
|
+
`MINIO_BUCKET=${answers.bucket}`,
|
|
967
|
+
],
|
|
968
|
+
dryRun: answers.dryRun,
|
|
969
|
+
existingServices,
|
|
970
|
+
key: "objectStorage",
|
|
971
|
+
manifest,
|
|
972
|
+
metadata: { bucket: answers.bucket },
|
|
973
|
+
projectDir,
|
|
974
|
+
railwayContext,
|
|
975
|
+
});
|
|
976
|
+
resourceSummary.push(objectStorageResult);
|
|
977
|
+
|
|
978
|
+
console.log(pc.bold("\nApplication services"));
|
|
979
|
+
manifest.appServices ||= {};
|
|
980
|
+
for (const spec of RAILWAY_APP_SERVICE_SPECS) {
|
|
981
|
+
const serviceResult = await ensureRailwayAppService({
|
|
982
|
+
aliases: spec.aliases,
|
|
983
|
+
dryRun: answers.dryRun,
|
|
984
|
+
existingServices,
|
|
985
|
+
key: spec.key,
|
|
986
|
+
manifest,
|
|
987
|
+
projectDir,
|
|
988
|
+
railwayContext,
|
|
989
|
+
serviceName: spec.serviceName,
|
|
990
|
+
});
|
|
991
|
+
appServiceSummary.push(serviceResult);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
manifest.bucket = answers.bucket;
|
|
995
|
+
manifest.environmentId = railwayContext.environmentId || manifest.environmentId || null;
|
|
996
|
+
manifest.environmentName = railwayContext.environmentName || manifest.environmentName || null;
|
|
997
|
+
manifest.projectId = railwayContext.projectId || manifest.projectId || null;
|
|
998
|
+
manifest.projectName = railwayContext.projectName || manifest.projectName || null;
|
|
999
|
+
manifest.updatedAt = new Date().toISOString();
|
|
1000
|
+
|
|
1001
|
+
const servicesAfterProvision = await discoverRailwayServices(railwayContext, projectDir);
|
|
1002
|
+
updateRailwayManifestAppServices(manifest, servicesAfterProvision);
|
|
1003
|
+
await wireRailwayVariables({
|
|
1004
|
+
dryRun: answers.dryRun,
|
|
1005
|
+
manifest,
|
|
1006
|
+
projectDir,
|
|
1007
|
+
railwayContext,
|
|
1008
|
+
services: servicesAfterProvision,
|
|
1009
|
+
summary: variableSummary,
|
|
1010
|
+
});
|
|
1011
|
+
const deploymentResults = await deployRailwayAppServices({
|
|
1012
|
+
dryRun: answers.dryRun,
|
|
1013
|
+
manifest,
|
|
1014
|
+
projectDir,
|
|
1015
|
+
railwayContext,
|
|
1016
|
+
services: servicesAfterProvision,
|
|
1017
|
+
});
|
|
1018
|
+
deploySummary.push(...deploymentResults);
|
|
1019
|
+
|
|
1020
|
+
if (!answers.dryRun) {
|
|
1021
|
+
await writeRailwayManifest(projectDir, manifest);
|
|
1022
|
+
}
|
|
1023
|
+
printRailwaySetupSummary({
|
|
1024
|
+
appServiceSummary,
|
|
1025
|
+
bucket: answers.bucket,
|
|
1026
|
+
deploySummary,
|
|
1027
|
+
dryRun: answers.dryRun,
|
|
1028
|
+
projectDir,
|
|
1029
|
+
railwayContext,
|
|
1030
|
+
resourceSummary,
|
|
1031
|
+
variableSummary,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
async function runSyncRailwayEnv(argv) {
|
|
1036
|
+
const args = parseSetupRailwayArgs(argv);
|
|
1037
|
+
const answers = await collectSetupRailwayAnswers(args);
|
|
1038
|
+
const projectDir = path.resolve(process.cwd(), answers.directory);
|
|
1039
|
+
|
|
1040
|
+
await ensureProjectStructure(projectDir);
|
|
1041
|
+
await ensureRailwayCliInstalled();
|
|
1042
|
+
await ensureRailwayAuthenticated(projectDir, answers.environment);
|
|
1043
|
+
|
|
1044
|
+
const manifest = await readRailwayManifest(projectDir);
|
|
1045
|
+
manifest.resources ||= {};
|
|
1046
|
+
manifest.appServices ||= {};
|
|
1047
|
+
|
|
1048
|
+
const railwayContext = await loadRailwayContext(projectDir, answers.environment);
|
|
1049
|
+
railwayContext.environmentRef = answers.environment || railwayContext.environmentId || railwayContext.environmentName;
|
|
1050
|
+
const services = await discoverRailwayServices(railwayContext, projectDir);
|
|
1051
|
+
const variableSummary = [];
|
|
1052
|
+
|
|
1053
|
+
updateRailwayManifestAppServices(manifest, services);
|
|
1054
|
+
await wireRailwayVariables({
|
|
1055
|
+
dryRun: answers.dryRun,
|
|
1056
|
+
manifest,
|
|
1057
|
+
projectDir,
|
|
1058
|
+
railwayContext,
|
|
1059
|
+
services,
|
|
1060
|
+
summary: variableSummary,
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
manifest.bucket = manifest.bucket || answers.bucket;
|
|
1064
|
+
manifest.environmentId = railwayContext.environmentId || manifest.environmentId || null;
|
|
1065
|
+
manifest.environmentName = railwayContext.environmentName || manifest.environmentName || null;
|
|
1066
|
+
manifest.projectId = railwayContext.projectId || manifest.projectId || null;
|
|
1067
|
+
manifest.projectName = railwayContext.projectName || manifest.projectName || null;
|
|
1068
|
+
manifest.updatedAt = new Date().toISOString();
|
|
1069
|
+
|
|
1070
|
+
if (!answers.dryRun) {
|
|
1071
|
+
await writeRailwayManifest(projectDir, manifest);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
printRailwaySetupSummary({
|
|
1075
|
+
appServiceSummary: [],
|
|
1076
|
+
bucket: manifest.bucket || answers.bucket,
|
|
1077
|
+
deploySummary: [],
|
|
1078
|
+
dryRun: answers.dryRun,
|
|
1079
|
+
projectDir,
|
|
1080
|
+
railwayContext,
|
|
1081
|
+
resourceSummary: [],
|
|
1082
|
+
variableSummary,
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
865
1086
|
function parseDirectoryArgs(argv) {
|
|
866
1087
|
return { directory: argv[0] || "." };
|
|
867
1088
|
}
|
|
868
1089
|
|
|
1090
|
+
function parseSetupRailwayArgs(argv) {
|
|
1091
|
+
const options = {
|
|
1092
|
+
bucket: DEFAULT_RAILWAY_BUCKET,
|
|
1093
|
+
directory: ".",
|
|
1094
|
+
dryRun: false,
|
|
1095
|
+
environment: undefined,
|
|
1096
|
+
yes: false,
|
|
1097
|
+
};
|
|
1098
|
+
const positionals = [];
|
|
1099
|
+
|
|
1100
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1101
|
+
const arg = argv[index];
|
|
1102
|
+
|
|
1103
|
+
if (arg === "--yes" || arg === "-y") {
|
|
1104
|
+
options.yes = true;
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
if (arg === "--dry-run") {
|
|
1109
|
+
options.dryRun = true;
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (arg === "--bucket") {
|
|
1114
|
+
options.bucket = argv[index + 1] || options.bucket;
|
|
1115
|
+
index += 1;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if (arg.startsWith("--bucket=")) {
|
|
1120
|
+
options.bucket = arg.split("=")[1] || options.bucket;
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (arg === "--environment" || arg === "-e") {
|
|
1125
|
+
options.environment = argv[index + 1] || options.environment;
|
|
1126
|
+
index += 1;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (arg.startsWith("--environment=")) {
|
|
1131
|
+
options.environment = arg.split("=")[1] || options.environment;
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
positionals.push(arg);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
options.directory = positionals[0] || options.directory;
|
|
1139
|
+
return options;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async function collectSetupRailwayAnswers(args) {
|
|
1143
|
+
if (args.yes) {
|
|
1144
|
+
return {
|
|
1145
|
+
bucket: args.bucket,
|
|
1146
|
+
directory: args.directory,
|
|
1147
|
+
dryRun: args.dryRun,
|
|
1148
|
+
environment: args.environment,
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const directory = await prompt(
|
|
1153
|
+
text({
|
|
1154
|
+
defaultValue: args.directory,
|
|
1155
|
+
message: "Project directory to configure on Railway?",
|
|
1156
|
+
placeholder: ".",
|
|
1157
|
+
validate(value) {
|
|
1158
|
+
return value.trim().length === 0 ? "Project directory is required" : undefined;
|
|
1159
|
+
},
|
|
1160
|
+
}),
|
|
1161
|
+
);
|
|
1162
|
+
|
|
1163
|
+
const bucket = await prompt(
|
|
1164
|
+
text({
|
|
1165
|
+
defaultValue: args.bucket,
|
|
1166
|
+
message: "Object storage bucket name?",
|
|
1167
|
+
placeholder: DEFAULT_RAILWAY_BUCKET,
|
|
1168
|
+
validate(value) {
|
|
1169
|
+
return /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(value)
|
|
1170
|
+
? undefined
|
|
1171
|
+
: "Use 3-63 lowercase letters, numbers, dots, or hyphens";
|
|
1172
|
+
},
|
|
1173
|
+
}),
|
|
1174
|
+
);
|
|
1175
|
+
|
|
1176
|
+
let environment = args.environment;
|
|
1177
|
+
if (!environment) {
|
|
1178
|
+
environment = await prompt(
|
|
1179
|
+
text({
|
|
1180
|
+
defaultValue: "",
|
|
1181
|
+
message: "Railway environment name or ID? (leave empty for linked default)",
|
|
1182
|
+
placeholder: "production",
|
|
1183
|
+
}),
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
return {
|
|
1188
|
+
bucket,
|
|
1189
|
+
directory,
|
|
1190
|
+
dryRun: args.dryRun,
|
|
1191
|
+
environment: environment?.trim() || undefined,
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
async function ensureRailwayCliInstalled() {
|
|
1196
|
+
const result = await checkCommand({ command: "railway", args: ["--version"], name: "railway" });
|
|
1197
|
+
if (!result.ok) {
|
|
1198
|
+
throw new Error(
|
|
1199
|
+
"Railway CLI is required for this command. Install it with `brew install railway` or `npm i -g @railway/cli`.",
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
async function ensureRailwayAuthenticated(projectDir, environment) {
|
|
1205
|
+
const result = await execa("railway", buildRailwayArgs(["whoami"], environment), {
|
|
1206
|
+
cwd: projectDir,
|
|
1207
|
+
reject: false,
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
if (result.exitCode !== 0) {
|
|
1211
|
+
throw new Error("Railway CLI is not authenticated. Run `railway login` and try again.");
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
async function readRailwayManifest(projectDir) {
|
|
1216
|
+
const manifestPath = path.join(projectDir, RAILWAY_MANIFEST_FILENAME);
|
|
1217
|
+
if (!(await fs.pathExists(manifestPath))) {
|
|
1218
|
+
return {
|
|
1219
|
+
appServices: {},
|
|
1220
|
+
bucket: DEFAULT_RAILWAY_BUCKET,
|
|
1221
|
+
environmentId: null,
|
|
1222
|
+
environmentName: null,
|
|
1223
|
+
projectId: null,
|
|
1224
|
+
projectName: null,
|
|
1225
|
+
resources: {},
|
|
1226
|
+
updatedAt: null,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return fs.readJson(manifestPath);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
async function writeRailwayManifest(projectDir, manifest) {
|
|
1234
|
+
const manifestPath = path.join(projectDir, RAILWAY_MANIFEST_FILENAME);
|
|
1235
|
+
await fs.writeJson(manifestPath, manifest, { spaces: 2 });
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
async function loadRailwayContext(projectDir, environment) {
|
|
1239
|
+
const result = await execa("railway", buildRailwayArgs(["status", "--json"], environment), {
|
|
1240
|
+
cwd: projectDir,
|
|
1241
|
+
reject: false,
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
if (result.exitCode !== 0) {
|
|
1245
|
+
throw new Error(
|
|
1246
|
+
`Unable to read Railway project status for ${projectDir}. Link the directory first with \`railway link\` or \`railway init\`.`,
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const payload = parseJsonOutput(result.stdout);
|
|
1251
|
+
if (!payload) {
|
|
1252
|
+
throw new Error("Railway status returned an unexpected response. Make sure the project is linked and try again.");
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const project = payload.project || payload.linkedProject || payload.workspace?.project || null;
|
|
1256
|
+
const environmentData = payload.environment || payload.linkedEnvironment || payload.deployment?.environment || null;
|
|
1257
|
+
|
|
1258
|
+
return {
|
|
1259
|
+
environmentId: pickFirstString([
|
|
1260
|
+
environmentData?.id,
|
|
1261
|
+
payload.environmentId,
|
|
1262
|
+
findFirstNestedValue(payload, "environmentId"),
|
|
1263
|
+
]),
|
|
1264
|
+
environmentName: pickFirstString([
|
|
1265
|
+
environmentData?.name,
|
|
1266
|
+
payload.environmentName,
|
|
1267
|
+
environment,
|
|
1268
|
+
findFirstNestedValue(payload, "environmentName"),
|
|
1269
|
+
]),
|
|
1270
|
+
projectId: pickFirstString([project?.id, payload.projectId, findFirstNestedValue(payload, "projectId")]),
|
|
1271
|
+
projectName: pickFirstString([
|
|
1272
|
+
project?.name,
|
|
1273
|
+
payload.projectName,
|
|
1274
|
+
findFirstNestedValue(payload, "projectName"),
|
|
1275
|
+
]),
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
async function discoverRailwayServices(railwayContext, projectDir) {
|
|
1280
|
+
const cliServices = await discoverRailwayServicesViaCli(railwayContext, projectDir);
|
|
1281
|
+
if (cliServices.length > 0) {
|
|
1282
|
+
return cliServices;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const auth = getRailwayApiAuth();
|
|
1286
|
+
if (!auth || !railwayContext.projectId) {
|
|
1287
|
+
return [];
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
try {
|
|
1291
|
+
const response = await fetch(RAILWAY_GRAPHQL_ENDPOINT, {
|
|
1292
|
+
body: JSON.stringify({
|
|
1293
|
+
query: `query SetupRailwayServices($projectId: String!) {
|
|
1294
|
+
project(id: $projectId) {
|
|
1295
|
+
services {
|
|
1296
|
+
edges {
|
|
1297
|
+
node {
|
|
1298
|
+
id
|
|
1299
|
+
name
|
|
1300
|
+
icon
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}`,
|
|
1306
|
+
variables: {
|
|
1307
|
+
projectId: railwayContext.projectId,
|
|
1308
|
+
},
|
|
1309
|
+
}),
|
|
1310
|
+
headers: {
|
|
1311
|
+
...auth.headers,
|
|
1312
|
+
"Content-Type": "application/json",
|
|
1313
|
+
},
|
|
1314
|
+
method: "POST",
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
if (!response.ok) {
|
|
1318
|
+
return [];
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const payload = await response.json();
|
|
1322
|
+
const nodes = payload?.data?.project?.services?.edges || [];
|
|
1323
|
+
return nodes
|
|
1324
|
+
.map((edge) => edge?.node)
|
|
1325
|
+
.filter(Boolean)
|
|
1326
|
+
.map((service) => ({
|
|
1327
|
+
icon: typeof service.icon === "string" ? service.icon : null,
|
|
1328
|
+
id: typeof service.id === "string" ? service.id : null,
|
|
1329
|
+
name: typeof service.name === "string" ? service.name : null,
|
|
1330
|
+
}));
|
|
1331
|
+
} catch {
|
|
1332
|
+
return [];
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
async function discoverRailwayServicesViaCli(railwayContext, projectDir) {
|
|
1337
|
+
try {
|
|
1338
|
+
const result = await execa(
|
|
1339
|
+
"railway",
|
|
1340
|
+
buildRailwayArgs(["service", "status", "--all", "--json"], railwayContext.environmentRef),
|
|
1341
|
+
{
|
|
1342
|
+
cwd: projectDir,
|
|
1343
|
+
reject: false,
|
|
1344
|
+
},
|
|
1345
|
+
);
|
|
1346
|
+
|
|
1347
|
+
if (result.exitCode !== 0) {
|
|
1348
|
+
return [];
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const payload = parseJsonOutput(result.stdout);
|
|
1352
|
+
if (!payload) {
|
|
1353
|
+
return [];
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
return normalizeRailwayServices(extractRailwayServiceCandidates(payload));
|
|
1357
|
+
} catch {
|
|
1358
|
+
return [];
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function getRailwayApiAuth() {
|
|
1363
|
+
if (process.env.RAILWAY_API_TOKEN) {
|
|
1364
|
+
return {
|
|
1365
|
+
headers: {
|
|
1366
|
+
Authorization: `Bearer ${process.env.RAILWAY_API_TOKEN}`,
|
|
1367
|
+
},
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (process.env.RAILWAY_TOKEN) {
|
|
1372
|
+
return {
|
|
1373
|
+
headers: {
|
|
1374
|
+
"Project-Access-Token": process.env.RAILWAY_TOKEN,
|
|
1375
|
+
},
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
async function ensureRailwayResource(config) {
|
|
1383
|
+
const manifestEntry = config.manifest.resources?.[config.key];
|
|
1384
|
+
const existingService = findRailwayService(config.existingServices, config.aliases, manifestEntry?.serviceName);
|
|
1385
|
+
|
|
1386
|
+
if (existingService) {
|
|
1387
|
+
config.manifest.resources[config.key] = {
|
|
1388
|
+
bucket: config.metadata?.bucket || manifestEntry?.bucket || null,
|
|
1389
|
+
detectedAt: new Date().toISOString(),
|
|
1390
|
+
serviceId: existingService.id || manifestEntry?.serviceId || null,
|
|
1391
|
+
serviceName: existingService.name || manifestEntry?.serviceName || null,
|
|
1392
|
+
source: "remote",
|
|
1393
|
+
status: "existing",
|
|
1394
|
+
};
|
|
1395
|
+
|
|
1396
|
+
console.log(`- ${pc.cyan(config.key)} already present${existingService.name ? ` (${existingService.name})` : ""}`);
|
|
1397
|
+
return {
|
|
1398
|
+
key: config.key,
|
|
1399
|
+
serviceName: existingService.name || manifestEntry?.serviceName || null,
|
|
1400
|
+
status: "existing",
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
if (manifestEntry?.status === "created" || manifestEntry?.status === "existing") {
|
|
1405
|
+
console.log(`- ${pc.yellow(config.key)} tracked in ${RAILWAY_MANIFEST_FILENAME} but not found remotely, recreating...`);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
console.log(`- creating ${pc.cyan(config.key)}...`);
|
|
1409
|
+
const servicesBefore = normalizeRailwayServices(config.existingServices);
|
|
1410
|
+
if (!config.dryRun) {
|
|
1411
|
+
await runRailwayCommand(config.projectDir, config.railwayContext.environmentRef, config.commandArgs);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
let createdService = null;
|
|
1415
|
+
if (!config.dryRun) {
|
|
1416
|
+
createdService = await waitForCreatedRailwayService({
|
|
1417
|
+
aliases: config.aliases,
|
|
1418
|
+
beforeServices: servicesBefore,
|
|
1419
|
+
key: config.key,
|
|
1420
|
+
manifestEntry,
|
|
1421
|
+
projectDir: config.projectDir,
|
|
1422
|
+
railwayContext: config.railwayContext,
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
config.manifest.resources[config.key] = {
|
|
1427
|
+
bucket: config.metadata?.bucket || null,
|
|
1428
|
+
detectedAt: new Date().toISOString(),
|
|
1429
|
+
serviceId: createdService?.id || null,
|
|
1430
|
+
serviceName: createdService?.name || null,
|
|
1431
|
+
source: "cli",
|
|
1432
|
+
status: "created",
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
return {
|
|
1436
|
+
key: config.key,
|
|
1437
|
+
serviceName: createdService?.name || null,
|
|
1438
|
+
status: config.dryRun ? "dry-run" : "created",
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
async function ensureRailwayAppService(config) {
|
|
1443
|
+
const manifestEntry = config.manifest.appServices?.[config.key];
|
|
1444
|
+
const existingService = findRailwayService(
|
|
1445
|
+
config.existingServices,
|
|
1446
|
+
config.aliases,
|
|
1447
|
+
manifestEntry?.serviceName || config.serviceName,
|
|
1448
|
+
);
|
|
1449
|
+
|
|
1450
|
+
if (existingService) {
|
|
1451
|
+
config.manifest.appServices[config.key] = {
|
|
1452
|
+
serviceId: existingService.id || manifestEntry?.serviceId || null,
|
|
1453
|
+
serviceName: existingService.name || manifestEntry?.serviceName || config.serviceName,
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
console.log(`- ${pc.cyan(config.serviceName)} already present${existingService.name ? ` (${existingService.name})` : ""}`);
|
|
1457
|
+
return {
|
|
1458
|
+
key: config.serviceName,
|
|
1459
|
+
serviceName: existingService.name || config.serviceName,
|
|
1460
|
+
status: "existing",
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
if (manifestEntry?.serviceName) {
|
|
1465
|
+
console.log(`- ${pc.yellow(config.serviceName)} tracked in ${RAILWAY_MANIFEST_FILENAME} but not found remotely, recreating...`);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
console.log(`- creating ${pc.cyan(config.serviceName)} service...`);
|
|
1469
|
+
const servicesBefore = normalizeRailwayServices(config.existingServices);
|
|
1470
|
+
if (!config.dryRun) {
|
|
1471
|
+
await runRailwayCommand(config.projectDir, config.railwayContext.environmentRef, ["add", "--service", config.serviceName]);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
let createdService = null;
|
|
1475
|
+
if (!config.dryRun) {
|
|
1476
|
+
createdService = await waitForCreatedRailwayService({
|
|
1477
|
+
aliases: config.aliases,
|
|
1478
|
+
beforeServices: servicesBefore,
|
|
1479
|
+
key: config.serviceName,
|
|
1480
|
+
manifestEntry,
|
|
1481
|
+
projectDir: config.projectDir,
|
|
1482
|
+
railwayContext: config.railwayContext,
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
config.manifest.appServices[config.key] = {
|
|
1487
|
+
serviceId: createdService?.id || null,
|
|
1488
|
+
serviceName: createdService?.name || config.serviceName,
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
return {
|
|
1492
|
+
key: config.serviceName,
|
|
1493
|
+
serviceName: createdService?.name || config.serviceName,
|
|
1494
|
+
status: config.dryRun ? "dry-run" : "created",
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
async function deployRailwayAppServices(config) {
|
|
1499
|
+
console.log(pc.bold("\nDeployments"));
|
|
1500
|
+
|
|
1501
|
+
const summary = [];
|
|
1502
|
+
for (const spec of RAILWAY_APP_SERVICE_SPECS) {
|
|
1503
|
+
const manifestEntry = config.manifest.appServices?.[spec.key];
|
|
1504
|
+
const service = findRailwayService(config.services, spec.aliases, manifestEntry?.serviceName || spec.serviceName);
|
|
1505
|
+
const targetServiceName = service?.name || manifestEntry?.serviceName || spec.serviceName;
|
|
1506
|
+
|
|
1507
|
+
if (!service && !config.dryRun) {
|
|
1508
|
+
console.log(`- ${pc.yellow(spec.serviceName)} service not found, skipping deployment`);
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
if (config.dryRun) {
|
|
1513
|
+
console.log(`- would deploy ${pc.cyan(targetServiceName)} from ${spec.directory}/`);
|
|
1514
|
+
summary.push({ directory: spec.directory, serviceName: targetServiceName, status: "dry-run" });
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
console.log(`- deploying ${pc.cyan(targetServiceName)} from ${spec.directory}/...`);
|
|
1519
|
+
await runRailwayCommand(config.projectDir, config.railwayContext.environmentRef, [
|
|
1520
|
+
"up",
|
|
1521
|
+
spec.directory,
|
|
1522
|
+
"--service",
|
|
1523
|
+
targetServiceName,
|
|
1524
|
+
"--path-as-root",
|
|
1525
|
+
"--detach",
|
|
1526
|
+
"--message",
|
|
1527
|
+
`asaje setup-railway: deploy ${targetServiceName}`,
|
|
1528
|
+
]);
|
|
1529
|
+
summary.push({ directory: spec.directory, serviceName: targetServiceName, status: "deployed" });
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
return summary;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
async function wireRailwayVariables(config) {
|
|
1536
|
+
console.log(pc.bold("\nVariables"));
|
|
1537
|
+
|
|
1538
|
+
const localEnv = await loadRailwayLocalEnvDefaults(config.projectDir);
|
|
1539
|
+
|
|
1540
|
+
const infra = {
|
|
1541
|
+
objectStorage: findRailwayService(
|
|
1542
|
+
config.services,
|
|
1543
|
+
["object-storage", "storage", "simple-s3", "minio"],
|
|
1544
|
+
config.manifest.resources.objectStorage?.serviceName,
|
|
1545
|
+
),
|
|
1546
|
+
postgres: findRailwayService(
|
|
1547
|
+
config.services,
|
|
1548
|
+
["postgres", "postgresql"],
|
|
1549
|
+
config.manifest.resources.postgres?.serviceName,
|
|
1550
|
+
),
|
|
1551
|
+
rabbitmq: findRailwayService(
|
|
1552
|
+
config.services,
|
|
1553
|
+
["rabbitmq"],
|
|
1554
|
+
config.manifest.resources.rabbitmq?.serviceName,
|
|
1555
|
+
),
|
|
1556
|
+
};
|
|
1557
|
+
const appServices = {
|
|
1558
|
+
admin: findRailwayService(config.services, ["admin", "frontend", "web"], config.manifest.appServices?.admin?.serviceName),
|
|
1559
|
+
api: findRailwayService(config.services, ["api", "backend", "server"], config.manifest.appServices?.api?.serviceName),
|
|
1560
|
+
realtime: findRailwayService(
|
|
1561
|
+
config.services,
|
|
1562
|
+
["realtime-gateway", "realtime"],
|
|
1563
|
+
config.manifest.appServices?.realtime?.serviceName,
|
|
1564
|
+
),
|
|
1565
|
+
};
|
|
1566
|
+
|
|
1567
|
+
updateRailwayManifestAppServices(config.manifest, Object.values(appServices).filter(Boolean));
|
|
1568
|
+
|
|
1569
|
+
if (!appServices.api) {
|
|
1570
|
+
console.log(`- ${pc.yellow("api")} service not found, skipping API variable wiring`);
|
|
1571
|
+
}
|
|
1572
|
+
if (!appServices.realtime) {
|
|
1573
|
+
console.log(`- ${pc.yellow("realtime-gateway")} service not found, skipping realtime variable wiring`);
|
|
1574
|
+
}
|
|
1575
|
+
if (!appServices.admin) {
|
|
1576
|
+
console.log(`- ${pc.yellow("admin")} service not found, skipping admin variable wiring`);
|
|
1577
|
+
}
|
|
1578
|
+
if (!infra.postgres) {
|
|
1579
|
+
console.log(`- ${pc.yellow("postgres")} resource not found, DATABASE_URL wiring will be skipped`);
|
|
1580
|
+
}
|
|
1581
|
+
if (!infra.rabbitmq) {
|
|
1582
|
+
console.log(`- ${pc.yellow("rabbitmq")} resource not found, RABBITMQ_URL wiring will be skipped`);
|
|
1583
|
+
}
|
|
1584
|
+
if (!infra.objectStorage) {
|
|
1585
|
+
console.log(`- ${pc.yellow("object-storage")} resource not found, S3 variable wiring will be skipped`);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
if (appServices.api) {
|
|
1589
|
+
const existingApiVariables = await loadRailwayServiceVariables(
|
|
1590
|
+
config.projectDir,
|
|
1591
|
+
config.railwayContext.environmentRef,
|
|
1592
|
+
appServices.api.name,
|
|
1593
|
+
);
|
|
1594
|
+
const variables = {};
|
|
1595
|
+
const sharedSecrets = buildRailwaySharedSecrets(localEnv, existingApiVariables);
|
|
1596
|
+
Object.assign(variables, sharedSecrets.api);
|
|
1597
|
+
if (infra.postgres?.name) {
|
|
1598
|
+
variables.DATABASE_URL = railwayReference(infra.postgres.name, "DATABASE_URL");
|
|
1599
|
+
}
|
|
1600
|
+
if (infra.rabbitmq?.name) {
|
|
1601
|
+
variables.RABBITMQ_URL = buildRabbitMqUrlReference(infra.rabbitmq.name);
|
|
1602
|
+
}
|
|
1603
|
+
if (infra.objectStorage?.name) {
|
|
1604
|
+
Object.assign(variables, buildObjectStorageVariables(infra.objectStorage.name));
|
|
1605
|
+
}
|
|
1606
|
+
if (appServices.admin?.name) {
|
|
1607
|
+
variables.CORS_ALLOWED_ORIGINS = `https://${railwayReference(appServices.admin.name, "RAILWAY_PUBLIC_DOMAIN")}`;
|
|
1608
|
+
}
|
|
1609
|
+
await applyRailwayVariables({
|
|
1610
|
+
dryRun: config.dryRun,
|
|
1611
|
+
environment: config.railwayContext.environmentRef,
|
|
1612
|
+
projectDir: config.projectDir,
|
|
1613
|
+
serviceName: appServices.api.name,
|
|
1614
|
+
summary: config.summary,
|
|
1615
|
+
variables,
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
if (appServices.realtime) {
|
|
1620
|
+
const variables = {};
|
|
1621
|
+
Object.assign(variables, buildRealtimeDefaults(localEnv));
|
|
1622
|
+
if (infra.rabbitmq?.name) {
|
|
1623
|
+
variables.RABBITMQ_URL = buildRabbitMqUrlReference(infra.rabbitmq.name);
|
|
1624
|
+
}
|
|
1625
|
+
if (appServices.api?.name) {
|
|
1626
|
+
variables.JWT_SECRET = railwayReference(appServices.api.name, "JWT_SECRET");
|
|
1627
|
+
}
|
|
1628
|
+
if (appServices.admin?.name) {
|
|
1629
|
+
variables.CORS_ALLOWED_ORIGINS = `https://${railwayReference(appServices.admin?.name || "admin", "RAILWAY_PUBLIC_DOMAIN")}`;
|
|
1630
|
+
}
|
|
1631
|
+
await applyRailwayVariables({
|
|
1632
|
+
dryRun: config.dryRun,
|
|
1633
|
+
environment: config.railwayContext.environmentRef,
|
|
1634
|
+
projectDir: config.projectDir,
|
|
1635
|
+
serviceName: appServices.realtime.name,
|
|
1636
|
+
summary: config.summary,
|
|
1637
|
+
variables,
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
if (appServices.admin) {
|
|
1642
|
+
const variables = {};
|
|
1643
|
+
Object.assign(variables, buildAdminDefaults(localEnv));
|
|
1644
|
+
if (appServices.api?.name) {
|
|
1645
|
+
variables.VITE_API_BASE_URL = `https://${railwayReference(appServices.api.name, "RAILWAY_PUBLIC_DOMAIN")}/api/v1`;
|
|
1646
|
+
}
|
|
1647
|
+
if (appServices.realtime?.name) {
|
|
1648
|
+
variables.VITE_REALTIME_BASE_URL = `https://${railwayReference(appServices.realtime.name, "RAILWAY_PUBLIC_DOMAIN")}`;
|
|
1649
|
+
}
|
|
1650
|
+
await applyRailwayVariables({
|
|
1651
|
+
dryRun: config.dryRun,
|
|
1652
|
+
environment: config.railwayContext.environmentRef,
|
|
1653
|
+
projectDir: config.projectDir,
|
|
1654
|
+
serviceName: appServices.admin.name,
|
|
1655
|
+
summary: config.summary,
|
|
1656
|
+
variables,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
async function loadRailwayLocalEnvDefaults(projectDir) {
|
|
1662
|
+
const [apiEnv, realtimeEnv, adminEnv] = await Promise.all([
|
|
1663
|
+
tryReadEnvFile(path.join(projectDir, "api/.env")),
|
|
1664
|
+
tryReadEnvFile(path.join(projectDir, "realtime-gateway/.env")),
|
|
1665
|
+
tryReadEnvFile(path.join(projectDir, "admin/.env")),
|
|
1666
|
+
]);
|
|
1667
|
+
|
|
1668
|
+
return {
|
|
1669
|
+
admin: adminEnv,
|
|
1670
|
+
api: apiEnv,
|
|
1671
|
+
realtime: realtimeEnv,
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function buildRailwaySharedSecrets(localEnv, existingVariables) {
|
|
1676
|
+
const jwtSecret =
|
|
1677
|
+
sanitizeSecret(localEnv.api.JWT_SECRET, "change-me") ||
|
|
1678
|
+
sanitizeSecret(existingVariables.JWT_SECRET, "${{ api.JWT_SECRET }}") ||
|
|
1679
|
+
randomSecret(32);
|
|
1680
|
+
const swaggerPassword =
|
|
1681
|
+
sanitizeSecret(localEnv.api.SWAGGER_PASSWORD, "change-me-too") ||
|
|
1682
|
+
sanitizeSecret(existingVariables.SWAGGER_PASSWORD, "${{ api.SWAGGER_PASSWORD }}") ||
|
|
1683
|
+
randomSecret(18);
|
|
1684
|
+
|
|
1685
|
+
return {
|
|
1686
|
+
api: {
|
|
1687
|
+
ACCESS_TOKEN_TTL_MINUTES: localEnv.api.ACCESS_TOKEN_TTL_MINUTES || "60",
|
|
1688
|
+
DEFAULT_LOCALE: localEnv.api.DEFAULT_LOCALE || "fr",
|
|
1689
|
+
FILE_MAX_SIZE_MB: localEnv.api.FILE_MAX_SIZE_MB || "25",
|
|
1690
|
+
FILE_SIGNED_URL_TTL_MINUTES: localEnv.api.FILE_SIGNED_URL_TTL_MINUTES || "15",
|
|
1691
|
+
JWT_SECRET: jwtSecret,
|
|
1692
|
+
LOGIN_OTP_TTL_MINUTES: localEnv.api.LOGIN_OTP_TTL_MINUTES || "10",
|
|
1693
|
+
MAILCHIMP_TRANSACTIONAL_API_KEY: localEnv.api.MAILCHIMP_TRANSACTIONAL_API_KEY || "dev-placeholder-key",
|
|
1694
|
+
MAIL_FROM_EMAIL: localEnv.api.MAIL_FROM_EMAIL || "no-reply@example.com",
|
|
1695
|
+
MAIL_FROM_NAME: localEnv.api.MAIL_FROM_NAME || "Boilerplate API",
|
|
1696
|
+
PASSWORD_RESET_OTP_TTL_MINUTES: localEnv.api.PASSWORD_RESET_OTP_TTL_MINUTES || "15",
|
|
1697
|
+
RABBITMQ_REALTIME_EXCHANGE: localEnv.api.RABBITMQ_REALTIME_EXCHANGE || "boilerplate.realtime",
|
|
1698
|
+
RABBITMQ_TASKS_EXCHANGE: localEnv.api.RABBITMQ_TASKS_EXCHANGE || "boilerplate.tasks",
|
|
1699
|
+
RABBITMQ_WORKER_CONSUMER_TAG: localEnv.api.RABBITMQ_WORKER_CONSUMER_TAG || "api-worker",
|
|
1700
|
+
RABBITMQ_WORKER_QUEUE: localEnv.api.RABBITMQ_WORKER_QUEUE || "api.worker.default",
|
|
1701
|
+
RATE_LIMIT_BURST: localEnv.api.RATE_LIMIT_BURST || "60",
|
|
1702
|
+
RATE_LIMIT_RPM: localEnv.api.RATE_LIMIT_RPM || "120",
|
|
1703
|
+
REFRESH_TOKEN_TTL_MINUTES: localEnv.api.REFRESH_TOKEN_TTL_MINUTES || "10080",
|
|
1704
|
+
SWAGGER_PASSWORD: swaggerPassword,
|
|
1705
|
+
SWAGGER_USERNAME: localEnv.api.SWAGGER_USERNAME || "swagger",
|
|
1706
|
+
},
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function buildRealtimeDefaults(localEnv) {
|
|
1711
|
+
return {
|
|
1712
|
+
RABBITMQ_REALTIME_EXCHANGE: localEnv.realtime.RABBITMQ_REALTIME_EXCHANGE || "boilerplate.realtime",
|
|
1713
|
+
REALTIME_HEARTBEAT_SECONDS: localEnv.realtime.REALTIME_HEARTBEAT_SECONDS || "25",
|
|
1714
|
+
REALTIME_INSTANCE_ID: localEnv.realtime.REALTIME_INSTANCE_ID || "realtime-gateway-railway",
|
|
1715
|
+
REALTIME_QUEUE_PREFIX: localEnv.realtime.REALTIME_QUEUE_PREFIX || "realtime-gateway",
|
|
1716
|
+
REALTIME_WRITE_TIMEOUT_SECONDS: localEnv.realtime.REALTIME_WRITE_TIMEOUT_SECONDS || "10",
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function buildAdminDefaults(localEnv) {
|
|
1721
|
+
return {
|
|
1722
|
+
VITE_API_TIMEOUT_MS: localEnv.admin.VITE_API_TIMEOUT_MS || "15000",
|
|
1723
|
+
VITE_APP_NAME: localEnv.admin.VITE_APP_NAME || "Admin Blueprint",
|
|
1724
|
+
VITE_REALTIME_DEFAULT_TRANSPORT: localEnv.admin.VITE_REALTIME_DEFAULT_TRANSPORT || "sse",
|
|
1725
|
+
VITE_REALTIME_RECONNECT_DELAY_MS: localEnv.admin.VITE_REALTIME_RECONNECT_DELAY_MS || "3000",
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function sanitizeSecret(value, placeholder) {
|
|
1730
|
+
const normalized = String(value || "").trim();
|
|
1731
|
+
if (!normalized || normalized === placeholder) {
|
|
1732
|
+
return "";
|
|
1733
|
+
}
|
|
1734
|
+
return normalized;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
async function tryReadEnvFile(filePath) {
|
|
1738
|
+
if (!(await fs.pathExists(filePath))) {
|
|
1739
|
+
return {};
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
return readEnvFile(filePath);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function buildObjectStorageVariables(serviceName) {
|
|
1746
|
+
return {
|
|
1747
|
+
MINIO_ACCESS_KEY: railwayReference(serviceName, "MINIO_ROOT_USER"),
|
|
1748
|
+
MINIO_BUCKET_NAME: railwayReference(serviceName, "MINIO_BUCKET"),
|
|
1749
|
+
MINIO_ENDPOINT: railwayReference(serviceName, "RAILWAY_PRIVATE_DOMAIN"),
|
|
1750
|
+
MINIO_PORT: "9000",
|
|
1751
|
+
MINIO_PUBLIC_URL: `https://${railwayReference(serviceName, "RAILWAY_PUBLIC_DOMAIN")}`,
|
|
1752
|
+
MINIO_SECRET_KEY: railwayReference(serviceName, "MINIO_ROOT_PASSWORD"),
|
|
1753
|
+
MINIO_USE_SSL: "false",
|
|
1754
|
+
OBJECT_STORAGE_PROVIDER: "minio",
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
function buildRabbitMqUrlReference(serviceName) {
|
|
1759
|
+
return `amqp://${railwayReference(serviceName, "RABBITMQ_DEFAULT_USER")}:${railwayReference(serviceName, "RABBITMQ_DEFAULT_PASS")}@${railwayReference(serviceName, "RAILWAY_PRIVATE_DOMAIN")}:5672/`;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function railwayReference(serviceName, variableName) {
|
|
1763
|
+
return "${{ " + serviceName + "." + variableName + " }}";
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
async function applyRailwayVariables(config) {
|
|
1767
|
+
const entries = Object.entries(config.variables).filter(([, value]) => typeof value === "string" && value.length > 0);
|
|
1768
|
+
if (entries.length === 0) {
|
|
1769
|
+
console.log(`- ${pc.dim(config.serviceName)} no variables to set`);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
if (config.dryRun) {
|
|
1774
|
+
console.log(`- ${pc.cyan(config.serviceName)} would set ${entries.map(([key]) => key).join(", ")}`);
|
|
1775
|
+
config.summary.push({
|
|
1776
|
+
keys: entries.map(([key]) => key),
|
|
1777
|
+
serviceName: config.serviceName,
|
|
1778
|
+
status: "dry-run",
|
|
1779
|
+
});
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
await runRailwayCommand(config.projectDir, config.environment, [
|
|
1784
|
+
"variable",
|
|
1785
|
+
"set",
|
|
1786
|
+
...entries.map(([key, value]) => `${key}=${value}`),
|
|
1787
|
+
"--service",
|
|
1788
|
+
config.serviceName,
|
|
1789
|
+
]);
|
|
1790
|
+
console.log(`- ${pc.cyan(config.serviceName)} set ${entries.map(([key]) => key).join(", ")}`);
|
|
1791
|
+
config.summary.push({
|
|
1792
|
+
keys: entries.map(([key]) => key),
|
|
1793
|
+
serviceName: config.serviceName,
|
|
1794
|
+
status: "updated",
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
async function loadRailwayServiceVariables(projectDir, environment, serviceName) {
|
|
1799
|
+
try {
|
|
1800
|
+
const result = await execa(
|
|
1801
|
+
"railway",
|
|
1802
|
+
buildRailwayArgs(["variable", "list", "--json", "--service", serviceName], environment),
|
|
1803
|
+
{
|
|
1804
|
+
cwd: projectDir,
|
|
1805
|
+
reject: false,
|
|
1806
|
+
},
|
|
1807
|
+
);
|
|
1808
|
+
|
|
1809
|
+
if (result.exitCode !== 0) {
|
|
1810
|
+
return {};
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
return normalizeRailwayVariables(parseJsonOutput(result.stdout));
|
|
1814
|
+
} catch {
|
|
1815
|
+
return {};
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
async function waitForCreatedRailwayService(config) {
|
|
1820
|
+
for (let attempt = 0; attempt < RAILWAY_SERVICE_DISCOVERY_RETRY_COUNT; attempt += 1) {
|
|
1821
|
+
const servicesAfter = await discoverRailwayServices(config.railwayContext, config.projectDir);
|
|
1822
|
+
const createdService = findCreatedRailwayService({
|
|
1823
|
+
aliases: config.aliases,
|
|
1824
|
+
beforeServices: config.beforeServices,
|
|
1825
|
+
manifestServiceName: config.manifestEntry?.serviceName,
|
|
1826
|
+
servicesAfter,
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
if (createdService) {
|
|
1830
|
+
return createdService;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
if (attempt < RAILWAY_SERVICE_DISCOVERY_RETRY_COUNT - 1) {
|
|
1834
|
+
await sleep(RAILWAY_SERVICE_DISCOVERY_RETRY_DELAY_MS);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
throw new Error(
|
|
1839
|
+
`Railway command for ${config.key} finished but the new service was not detected afterwards. Check the Railway dashboard/logs, then rerun \`asaje setup-railway\` or \`asaje sync-railway-env\` once the service appears.`,
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function findRailwayService(services, aliases, preferredName) {
|
|
1844
|
+
if (preferredName) {
|
|
1845
|
+
const exact = services.find(
|
|
1846
|
+
(service) => normalizeRailwayServiceName(service.name) === normalizeRailwayServiceName(preferredName),
|
|
1847
|
+
);
|
|
1848
|
+
if (exact) {
|
|
1849
|
+
return exact;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
const normalizedAliases = aliases.map(normalizeRailwayServiceName);
|
|
1854
|
+
return services.find((service) => {
|
|
1855
|
+
const normalizedName = normalizeRailwayServiceName(service.name);
|
|
1856
|
+
return normalizedAliases.some((alias) => normalizedName === alias || normalizedName.includes(alias));
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
function normalizeRailwayServiceName(value) {
|
|
1861
|
+
return String(value || "")
|
|
1862
|
+
.trim()
|
|
1863
|
+
.toLowerCase()
|
|
1864
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1865
|
+
.replace(/^-+|-+$/g, "");
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function normalizeRailwayServices(services) {
|
|
1869
|
+
const seen = new Set();
|
|
1870
|
+
const normalized = [];
|
|
1871
|
+
|
|
1872
|
+
for (const service of services) {
|
|
1873
|
+
const name = pickFirstString([service.name, service.serviceName]);
|
|
1874
|
+
if (!name) {
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
const id = pickFirstString([service.id, service.serviceId]);
|
|
1879
|
+
const key = `${normalizeRailwayServiceName(name)}:${id || ""}`;
|
|
1880
|
+
if (seen.has(key)) {
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
seen.add(key);
|
|
1885
|
+
normalized.push({ id, name });
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
return normalized;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
function findCreatedRailwayService(config) {
|
|
1892
|
+
const beforeServices = normalizeRailwayServices(config.beforeServices);
|
|
1893
|
+
const afterServices = normalizeRailwayServices(config.servicesAfter);
|
|
1894
|
+
const beforeKeys = new Set(beforeServices.map(createRailwayServiceIdentity));
|
|
1895
|
+
const newServices = afterServices.filter((service) => !beforeKeys.has(createRailwayServiceIdentity(service)));
|
|
1896
|
+
|
|
1897
|
+
if (newServices.length === 1) {
|
|
1898
|
+
return newServices[0];
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
const aliasMatch = findRailwayService(newServices, config.aliases, config.manifestServiceName);
|
|
1902
|
+
if (aliasMatch) {
|
|
1903
|
+
return aliasMatch;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function createRailwayServiceIdentity(service) {
|
|
1910
|
+
if (service?.id) {
|
|
1911
|
+
return `id:${service.id}`;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
return `name:${normalizeRailwayServiceName(service?.name)}`;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function normalizeRailwayVariables(input) {
|
|
1918
|
+
const normalized = {};
|
|
1919
|
+
|
|
1920
|
+
visitRailwayJson(input, (value) => {
|
|
1921
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
const key = pickFirstString([value.name, value.key]);
|
|
1926
|
+
const rawValue = pickFirstString([value.value, value.resolvedValue]);
|
|
1927
|
+
if (!key || rawValue === null) {
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
normalized[key] = rawValue;
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
return normalized;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function updateRailwayManifestAppServices(manifest, services) {
|
|
1938
|
+
manifest.appServices ||= {};
|
|
1939
|
+
|
|
1940
|
+
const entries = [
|
|
1941
|
+
["api", findRailwayService(services, ["api", "backend", "server"], manifest.appServices.api?.serviceName)],
|
|
1942
|
+
["admin", findRailwayService(services, ["admin", "frontend", "web"], manifest.appServices.admin?.serviceName)],
|
|
1943
|
+
[
|
|
1944
|
+
"realtime",
|
|
1945
|
+
findRailwayService(
|
|
1946
|
+
services,
|
|
1947
|
+
["realtime-gateway", "realtime"],
|
|
1948
|
+
manifest.appServices.realtime?.serviceName,
|
|
1949
|
+
),
|
|
1950
|
+
],
|
|
1951
|
+
];
|
|
1952
|
+
|
|
1953
|
+
for (const [key, service] of entries) {
|
|
1954
|
+
if (!service?.name) {
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
manifest.appServices[key] = {
|
|
1959
|
+
serviceId: service.id || manifest.appServices[key]?.serviceId || null,
|
|
1960
|
+
serviceName: service.name,
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function extractRailwayServiceCandidates(input) {
|
|
1966
|
+
const candidates = [];
|
|
1967
|
+
|
|
1968
|
+
visitRailwayJson(input, (value) => {
|
|
1969
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
if (typeof value.name === "string" || typeof value.serviceName === "string") {
|
|
1974
|
+
candidates.push(value);
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
return candidates;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
function visitRailwayJson(input, visitor) {
|
|
1982
|
+
visitor(input);
|
|
1983
|
+
if (!input || typeof input !== "object") {
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
if (Array.isArray(input)) {
|
|
1988
|
+
for (const item of input) {
|
|
1989
|
+
visitRailwayJson(item, visitor);
|
|
1990
|
+
}
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
for (const value of Object.values(input)) {
|
|
1995
|
+
visitRailwayJson(value, visitor);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function sleep(delayMs) {
|
|
2000
|
+
return new Promise((resolve) => {
|
|
2001
|
+
setTimeout(resolve, delayMs);
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
async function runRailwayCommand(projectDir, environment, args) {
|
|
2006
|
+
const result = await execa("railway", buildRailwayArgs(args, environment), {
|
|
2007
|
+
cwd: projectDir,
|
|
2008
|
+
reject: false,
|
|
2009
|
+
stderr: "inherit",
|
|
2010
|
+
stdout: "inherit",
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
if (result.exitCode !== 0) {
|
|
2014
|
+
throw new Error(`Railway command failed: railway ${args.join(" ")}`);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function buildRailwayArgs(args, environment) {
|
|
2019
|
+
if (!environment) {
|
|
2020
|
+
return args;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
return [...args, "--environment", environment];
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
function parseJsonOutput(value) {
|
|
2027
|
+
try {
|
|
2028
|
+
return JSON.parse(value);
|
|
2029
|
+
} catch {
|
|
2030
|
+
return null;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
function pickFirstString(values) {
|
|
2035
|
+
for (const value of values) {
|
|
2036
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
2037
|
+
return value.trim();
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
return null;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
function findFirstNestedValue(input, key) {
|
|
2045
|
+
if (!input || typeof input !== "object") {
|
|
2046
|
+
return null;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
if (typeof input[key] === "string") {
|
|
2050
|
+
return input[key];
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
for (const value of Object.values(input)) {
|
|
2054
|
+
if (!value || typeof value !== "object") {
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
const nested = findFirstNestedValue(value, key);
|
|
2059
|
+
if (nested) {
|
|
2060
|
+
return nested;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
function printRailwaySetupSummary(config) {
|
|
2068
|
+
console.log(pc.bold("\nRailway"));
|
|
2069
|
+
console.log(`- Directory: ${pc.bold(config.projectDir)}`);
|
|
2070
|
+
if (config.railwayContext.projectName || config.railwayContext.projectId) {
|
|
2071
|
+
console.log(`- Project: ${pc.bold(config.railwayContext.projectName || config.railwayContext.projectId)}`);
|
|
2072
|
+
}
|
|
2073
|
+
if (config.railwayContext.environmentName || config.railwayContext.environmentId) {
|
|
2074
|
+
console.log(`- Environment: ${pc.bold(config.railwayContext.environmentName || config.railwayContext.environmentId)}`);
|
|
2075
|
+
}
|
|
2076
|
+
console.log(`- Bucket: ${pc.bold(config.bucket)}`);
|
|
2077
|
+
|
|
2078
|
+
console.log(pc.bold("\nResources"));
|
|
2079
|
+
if (config.resourceSummary.length === 0) {
|
|
2080
|
+
console.log("- No infrastructure resources were changed");
|
|
2081
|
+
} else {
|
|
2082
|
+
for (const item of config.resourceSummary) {
|
|
2083
|
+
const detail = item.serviceName ? ` (${item.serviceName})` : "";
|
|
2084
|
+
console.log(`- ${pc.bold(item.key)}: ${item.status}${detail}`);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
console.log(pc.bold("\nApplication services"));
|
|
2089
|
+
if (config.appServiceSummary.length === 0) {
|
|
2090
|
+
console.log("- No application services were changed");
|
|
2091
|
+
} else {
|
|
2092
|
+
for (const item of config.appServiceSummary) {
|
|
2093
|
+
const detail = item.serviceName ? ` (${item.serviceName})` : "";
|
|
2094
|
+
console.log(`- ${pc.bold(item.key)}: ${item.status}${detail}`);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
console.log(pc.bold("\nDeployments"));
|
|
2099
|
+
if (config.deploySummary.length === 0) {
|
|
2100
|
+
console.log("- No application deployments were triggered");
|
|
2101
|
+
} else {
|
|
2102
|
+
for (const item of config.deploySummary) {
|
|
2103
|
+
console.log(`- ${pc.bold(item.serviceName)}: ${item.status} from ${item.directory}/`);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
console.log(pc.bold("\nVariables"));
|
|
2108
|
+
if (config.variableSummary.length === 0) {
|
|
2109
|
+
console.log("- No application variables were updated");
|
|
2110
|
+
} else {
|
|
2111
|
+
for (const item of config.variableSummary) {
|
|
2112
|
+
console.log(`- ${pc.bold(item.serviceName)}: ${item.status} ${item.keys.join(", ")}`);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
if (config.dryRun) {
|
|
2117
|
+
console.log(`- Dry run only, ${pc.bold(RAILWAY_MANIFEST_FILENAME)} was not written`);
|
|
2118
|
+
} else {
|
|
2119
|
+
console.log(`- Manifest written to ${pc.bold(RAILWAY_MANIFEST_FILENAME)} for future runs`);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
if (!getRailwayApiAuth()) {
|
|
2123
|
+
console.log(pc.yellow("\nNote: Set RAILWAY_API_TOKEN or RAILWAY_TOKEN to let future runs verify remote services before provisioning."));
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
869
2127
|
function parseStartArgs(argv) {
|
|
870
2128
|
const options = {
|
|
871
2129
|
directory: ".",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-asaje-go-vue",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "CLI to scaffold, configure, and run the Asaje Go + Vue boilerplate",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"create": "node ./bin/create-asaje-go-vue.js",
|
|
12
12
|
"start": "node ./bin/asaje.js start .",
|
|
13
13
|
"doctor": "node ./bin/asaje.js doctor ..",
|
|
14
|
+
"setup-railway": "node ./bin/asaje.js setup-railway .. --yes",
|
|
15
|
+
"sync-railway-env": "node ./bin/asaje.js sync-railway-env .. --yes",
|
|
14
16
|
"publish:check": "node ./bin/asaje.js publish .",
|
|
15
17
|
"check": "node --check ./bin/create-asaje-go-vue.js && node --check ./bin/asaje.js",
|
|
16
18
|
"pack:dry-run": "npm pack --dry-run"
|