create-asaje-go-vue 0.2.7 → 0.2.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/README.md +37 -0
- package/bin/create-asaje-go-vue.js +549 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,14 @@ 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
|
+
### Update an existing project from the template
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx -p create-asaje-go-vue@latest asaje update ./my-app --dry-run
|
|
51
|
+
npx -p create-asaje-go-vue@latest asaje update ./my-app --yes
|
|
52
|
+
npx -p create-asaje-go-vue@latest asaje update ./my-app --include admin/src/stores/session.ts,admin/src/services/http/session.ts
|
|
53
|
+
```
|
|
54
|
+
|
|
47
55
|
### Provision Railway resources
|
|
48
56
|
|
|
49
57
|
```bash
|
|
@@ -58,6 +66,13 @@ npx -p create-asaje-go-vue@latest asaje sync-railway-env ./my-app
|
|
|
58
66
|
npx -p create-asaje-go-vue@latest asaje sync-railway-env ./my-app --dry-run
|
|
59
67
|
```
|
|
60
68
|
|
|
69
|
+
### Destroy Railway resources
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx -p create-asaje-go-vue@latest asaje destroy-railway ./my-app
|
|
73
|
+
npx -p create-asaje-go-vue@latest asaje destroy-railway ./my-app --scope project --yes
|
|
74
|
+
```
|
|
75
|
+
|
|
61
76
|
## What `create` does
|
|
62
77
|
|
|
63
78
|
- clones the boilerplate from GitHub with `degit`
|
|
@@ -89,6 +104,16 @@ npx -p create-asaje-go-vue@latest asaje sync-railway-env ./my-app --dry-run
|
|
|
89
104
|
- runs `npm run pack:dry-run`
|
|
90
105
|
- prints the final manual npm release steps
|
|
91
106
|
|
|
107
|
+
## What `asaje update` does
|
|
108
|
+
|
|
109
|
+
- validates the target project structure
|
|
110
|
+
- reads the template repository and branch from `asaje.config.json` when available
|
|
111
|
+
- clones the latest template into a temporary directory
|
|
112
|
+
- overwrites a safe set of boilerplate-managed files such as Railway config, Dockerfiles, generated Swagger docs, and `.env.example` files
|
|
113
|
+
- supports `--include` for explicitly overwriting extra files or directories from the template, such as `admin/src/stores/session.ts`
|
|
114
|
+
- supports `--dry-run` to preview which files would be updated
|
|
115
|
+
- updates `asaje.config.json` with the template repository and branch used for the update
|
|
116
|
+
|
|
92
117
|
## What `asaje setup-railway` does
|
|
93
118
|
|
|
94
119
|
- validates the target project structure
|
|
@@ -111,6 +136,15 @@ npx -p create-asaje-go-vue@latest asaje sync-railway-env ./my-app --dry-run
|
|
|
111
136
|
- syncs variables for `api`, `realtime-gateway`, and `admin` without provisioning infra resources
|
|
112
137
|
- supports `--dry-run` to preview variable changes without applying them
|
|
113
138
|
|
|
139
|
+
## What `asaje destroy-railway` does
|
|
140
|
+
|
|
141
|
+
- validates the target project structure
|
|
142
|
+
- checks that the Railway CLI is installed and authenticated
|
|
143
|
+
- deletes either the linked Railway environment or the whole Railway project
|
|
144
|
+
- supports `--scope environment` (default) and `--scope project`
|
|
145
|
+
- supports `--dry-run` to preview the destructive action without applying it
|
|
146
|
+
- removes the local `asaje.railway.json` manifest after a successful deletion
|
|
147
|
+
|
|
114
148
|
## Useful flags
|
|
115
149
|
|
|
116
150
|
```bash
|
|
@@ -121,9 +155,12 @@ node ./bin/asaje.js start ../my-app --yes --profile frontend-only
|
|
|
121
155
|
node ./bin/asaje.js start ../my-app --yes --skip-admin --skip-worker
|
|
122
156
|
node ./bin/asaje.js doctor ../my-app
|
|
123
157
|
node ./bin/asaje.js publish .
|
|
158
|
+
node ./bin/asaje.js update ../my-app --dry-run
|
|
159
|
+
node ./bin/asaje.js update ../my-app --include admin/src/stores/session.ts --yes
|
|
124
160
|
node ./bin/asaje.js setup-railway ../my-app --yes
|
|
125
161
|
node ./bin/asaje.js setup-railway ../my-app --yes --dry-run
|
|
126
162
|
node ./bin/asaje.js sync-railway-env ../my-app --yes
|
|
163
|
+
node ./bin/asaje.js destroy-railway ../my-app --scope environment --yes
|
|
127
164
|
```
|
|
128
165
|
|
|
129
166
|
## Publish checklist
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import process from "node:process";
|
|
6
7
|
import {
|
|
@@ -29,23 +30,37 @@ const RAILWAY_SERVICE_DISCOVERY_RETRY_COUNT = 5;
|
|
|
29
30
|
const RAILWAY_APP_SERVICE_SPECS = [
|
|
30
31
|
{
|
|
31
32
|
aliases: ["api", "backend", "server"],
|
|
33
|
+
baseName: "api",
|
|
32
34
|
directory: "api",
|
|
33
35
|
key: "api",
|
|
34
|
-
serviceName: "api",
|
|
35
36
|
},
|
|
36
37
|
{
|
|
37
38
|
aliases: ["admin", "frontend", "web"],
|
|
39
|
+
baseName: "admin",
|
|
38
40
|
directory: "admin",
|
|
39
41
|
key: "admin",
|
|
40
|
-
serviceName: "admin",
|
|
41
42
|
},
|
|
42
43
|
{
|
|
43
44
|
aliases: ["realtime-gateway", "realtime"],
|
|
45
|
+
baseName: "realtime-gateway",
|
|
44
46
|
directory: "realtime-gateway",
|
|
45
47
|
key: "realtime",
|
|
46
|
-
serviceName: "realtime-gateway",
|
|
47
48
|
},
|
|
48
49
|
];
|
|
50
|
+
const SAFE_UPDATE_PATHS = [
|
|
51
|
+
"docker-compose.yml",
|
|
52
|
+
"admin/.env.example",
|
|
53
|
+
"admin/Dockerfile",
|
|
54
|
+
"admin/railway.json",
|
|
55
|
+
"admin/nginx",
|
|
56
|
+
"api/.env.example",
|
|
57
|
+
"api/Dockerfile",
|
|
58
|
+
"api/railway.json",
|
|
59
|
+
"api/docs",
|
|
60
|
+
"realtime-gateway/.env.example",
|
|
61
|
+
"realtime-gateway/Dockerfile",
|
|
62
|
+
"realtime-gateway/railway.json",
|
|
63
|
+
];
|
|
49
64
|
const ENV_FILE_SPECS = [
|
|
50
65
|
{ envPath: "admin/.env", examplePath: "admin/.env.example" },
|
|
51
66
|
{ envPath: "api/.env", examplePath: "api/.env.example" },
|
|
@@ -83,6 +98,12 @@ async function main() {
|
|
|
83
98
|
return;
|
|
84
99
|
}
|
|
85
100
|
|
|
101
|
+
if (invocation.command === "update") {
|
|
102
|
+
await runUpdate(invocation.argv);
|
|
103
|
+
outro(pc.green("Project update complete."));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
if (invocation.command === "setup-railway") {
|
|
87
108
|
await runSetupRailway(invocation.argv);
|
|
88
109
|
outro(pc.green("Railway setup complete."));
|
|
@@ -95,6 +116,12 @@ async function main() {
|
|
|
95
116
|
return;
|
|
96
117
|
}
|
|
97
118
|
|
|
119
|
+
if (invocation.command === "destroy-railway") {
|
|
120
|
+
await runDestroyRailway(invocation.argv);
|
|
121
|
+
outro(pc.green("Railway teardown complete."));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
98
125
|
await runCreate(invocation.argv);
|
|
99
126
|
outro(pc.green("Project ready."));
|
|
100
127
|
} catch (error) {
|
|
@@ -135,6 +162,10 @@ function resolveInvocation(argv) {
|
|
|
135
162
|
return { argv: rawArgs.slice(1), command: "publish", title: "asaje publish" };
|
|
136
163
|
}
|
|
137
164
|
|
|
165
|
+
if (firstArg === "update") {
|
|
166
|
+
return { argv: rawArgs.slice(1), command: "update", title: "asaje update" };
|
|
167
|
+
}
|
|
168
|
+
|
|
138
169
|
if (firstArg === "setup-railway") {
|
|
139
170
|
return { argv: rawArgs.slice(1), command: "setup-railway", title: "asaje setup-railway" };
|
|
140
171
|
}
|
|
@@ -143,6 +174,10 @@ function resolveInvocation(argv) {
|
|
|
143
174
|
return { argv: rawArgs.slice(1), command: "sync-railway-env", title: "asaje sync-railway-env" };
|
|
144
175
|
}
|
|
145
176
|
|
|
177
|
+
if (firstArg === "destroy-railway") {
|
|
178
|
+
return { argv: rawArgs.slice(1), command: "destroy-railway", title: "asaje destroy-railway" };
|
|
179
|
+
}
|
|
180
|
+
|
|
146
181
|
if (firstArg === "create") {
|
|
147
182
|
return { argv: rawArgs.slice(1), command: "create", title: "asaje create" };
|
|
148
183
|
}
|
|
@@ -162,6 +197,22 @@ function resolveInvocation(argv) {
|
|
|
162
197
|
return { argv: rawArgs.slice(1), command: "publish", title: "create-asaje-go-vue" };
|
|
163
198
|
}
|
|
164
199
|
|
|
200
|
+
if (firstArg === "update") {
|
|
201
|
+
return { argv: rawArgs.slice(1), command: "update", title: "create-asaje-go-vue" };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (firstArg === "setup-railway") {
|
|
205
|
+
return { argv: rawArgs.slice(1), command: "setup-railway", title: "create-asaje-go-vue" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (firstArg === "sync-railway-env") {
|
|
209
|
+
return { argv: rawArgs.slice(1), command: "sync-railway-env", title: "create-asaje-go-vue" };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (firstArg === "destroy-railway") {
|
|
213
|
+
return { argv: rawArgs.slice(1), command: "destroy-railway", title: "create-asaje-go-vue" };
|
|
214
|
+
}
|
|
215
|
+
|
|
165
216
|
return { argv: rawArgs, command: "create", title: "create-asaje-go-vue" };
|
|
166
217
|
}
|
|
167
218
|
|
|
@@ -172,16 +223,20 @@ function printHelp() {
|
|
|
172
223
|
console.log(`- ${pc.bold("asaje start [directory]")} start a configured project`);
|
|
173
224
|
console.log(`- ${pc.bold("asaje doctor [directory]")} check environment and project readiness`);
|
|
174
225
|
console.log(`- ${pc.bold("asaje publish")} run npm publish checklist for the CLI package`);
|
|
226
|
+
console.log(`- ${pc.bold("asaje update [directory]")} update managed boilerplate files from the template`);
|
|
175
227
|
console.log(`- ${pc.bold("asaje setup-railway [directory]")} provision Railway infrastructure for a project`);
|
|
176
228
|
console.log(`- ${pc.bold("asaje sync-railway-env [directory]")} sync Railway app variables without provisioning`);
|
|
229
|
+
console.log(`- ${pc.bold("asaje destroy-railway [directory]")} delete the linked Railway environment or project`);
|
|
177
230
|
console.log(pc.bold("\nExamples"));
|
|
178
231
|
console.log(`- ${pc.bold("npx create-asaje-go-vue my-app")}`);
|
|
179
232
|
console.log(`- ${pc.bold("node ./bin/create-asaje-go-vue.js my-app --yes")}`);
|
|
180
233
|
console.log(`- ${pc.bold("node ./bin/asaje.js start ../my-app")}`);
|
|
181
234
|
console.log(`- ${pc.bold("node ./bin/asaje.js doctor ..")}`);
|
|
182
235
|
console.log(`- ${pc.bold("node ./bin/asaje.js publish")}`);
|
|
236
|
+
console.log(`- ${pc.bold("node ./bin/asaje.js update .. --dry-run")}`);
|
|
183
237
|
console.log(`- ${pc.bold("node ./bin/asaje.js setup-railway ..")}`);
|
|
184
238
|
console.log(`- ${pc.bold("node ./bin/asaje.js sync-railway-env ..")}`);
|
|
239
|
+
console.log(`- ${pc.bold("node ./bin/asaje.js destroy-railway ..")}`);
|
|
185
240
|
}
|
|
186
241
|
|
|
187
242
|
async function runCreate(argv) {
|
|
@@ -276,6 +331,71 @@ function parseCreateArgs(argv) {
|
|
|
276
331
|
return { ...options, directory: positionals[0] };
|
|
277
332
|
}
|
|
278
333
|
|
|
334
|
+
function parseUpdateArgs(argv) {
|
|
335
|
+
const options = {
|
|
336
|
+
branch: undefined,
|
|
337
|
+
directory: ".",
|
|
338
|
+
dryRun: false,
|
|
339
|
+
include: [],
|
|
340
|
+
template: undefined,
|
|
341
|
+
yes: false,
|
|
342
|
+
};
|
|
343
|
+
const positionals = [];
|
|
344
|
+
|
|
345
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
346
|
+
const arg = argv[index];
|
|
347
|
+
|
|
348
|
+
if (arg === "--yes" || arg === "-y") {
|
|
349
|
+
options.yes = true;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (arg === "--dry-run") {
|
|
354
|
+
options.dryRun = true;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (arg === "--template") {
|
|
359
|
+
options.template = argv[index + 1] || options.template;
|
|
360
|
+
index += 1;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (arg.startsWith("--template=")) {
|
|
365
|
+
options.template = arg.split("=")[1] || options.template;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (arg === "--branch") {
|
|
370
|
+
options.branch = argv[index + 1] || options.branch;
|
|
371
|
+
index += 1;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (arg.startsWith("--branch=")) {
|
|
376
|
+
options.branch = arg.split("=")[1] || options.branch;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (arg === "--include") {
|
|
381
|
+
options.include.push(...splitCommaSeparatedPaths(argv[index + 1] || ""));
|
|
382
|
+
index += 1;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (arg.startsWith("--include=")) {
|
|
387
|
+
options.include.push(...splitCommaSeparatedPaths(arg.split("=")[1] || ""));
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
positionals.push(arg);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
options.directory = positionals[0] || options.directory;
|
|
395
|
+
options.include = uniquePaths(options.include);
|
|
396
|
+
return options;
|
|
397
|
+
}
|
|
398
|
+
|
|
279
399
|
async function collectCreateAnswers(args) {
|
|
280
400
|
const defaultDirectory = args.directory || "my-asaje-app";
|
|
281
401
|
const defaultSlug = slugify(path.basename(defaultDirectory));
|
|
@@ -610,6 +730,44 @@ async function collectCreateAnswers(args) {
|
|
|
610
730
|
});
|
|
611
731
|
}
|
|
612
732
|
|
|
733
|
+
async function collectUpdateAnswers(args) {
|
|
734
|
+
if (args.yes) {
|
|
735
|
+
return args;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const directory = await prompt(
|
|
739
|
+
text({
|
|
740
|
+
defaultValue: args.directory,
|
|
741
|
+
message: "Project directory to update?",
|
|
742
|
+
placeholder: ".",
|
|
743
|
+
validate(value) {
|
|
744
|
+
return value.trim().length === 0 ? "Project directory is required" : undefined;
|
|
745
|
+
},
|
|
746
|
+
}),
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
const include = args.include.length
|
|
750
|
+
? args.include
|
|
751
|
+
: splitCommaSeparatedPaths(
|
|
752
|
+
await prompt(
|
|
753
|
+
text({
|
|
754
|
+
defaultValue: "",
|
|
755
|
+
message: "Additional files or directories to overwrite from the template? (optional, comma-separated)",
|
|
756
|
+
placeholder: "admin/src/stores/session.ts,admin/src/services/http/session.ts",
|
|
757
|
+
}),
|
|
758
|
+
),
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
return {
|
|
762
|
+
branch: args.branch,
|
|
763
|
+
directory,
|
|
764
|
+
dryRun: args.dryRun,
|
|
765
|
+
include,
|
|
766
|
+
template: args.template,
|
|
767
|
+
yes: true,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
613
771
|
function buildCreateAnswers(input) {
|
|
614
772
|
const directory = input.directory.trim();
|
|
615
773
|
const slug = slugify(path.basename(directory));
|
|
@@ -911,10 +1069,54 @@ async function runPublish(argv) {
|
|
|
911
1069
|
console.log(`- Publish with ${pc.bold("npm publish")}`);
|
|
912
1070
|
}
|
|
913
1071
|
|
|
1072
|
+
async function runUpdate(argv) {
|
|
1073
|
+
const args = parseUpdateArgs(argv);
|
|
1074
|
+
const answers = await collectUpdateAnswers(args);
|
|
1075
|
+
const projectDir = path.resolve(process.cwd(), answers.directory);
|
|
1076
|
+
|
|
1077
|
+
await ensureProjectStructure(projectDir);
|
|
1078
|
+
|
|
1079
|
+
const projectConfig = await loadProjectConfig(projectDir);
|
|
1080
|
+
const templateRepository = answers.template || projectConfig?.template?.repository || DEFAULT_TEMPLATE;
|
|
1081
|
+
const templateBranch = answers.branch || projectConfig?.template?.branch || DEFAULT_BRANCH;
|
|
1082
|
+
const templateDir = await fs.mkdtemp(path.join(os.tmpdir(), "asaje-update-"));
|
|
1083
|
+
|
|
1084
|
+
try {
|
|
1085
|
+
console.log(pc.dim(`\nCloning template ${templateRepository}#${templateBranch}...`));
|
|
1086
|
+
await cloneTemplate(templateRepository, templateBranch, templateDir);
|
|
1087
|
+
await cleanupTemplateFiles(templateDir);
|
|
1088
|
+
|
|
1089
|
+
const selectedPaths = uniquePaths([...SAFE_UPDATE_PATHS, ...answers.include]);
|
|
1090
|
+
const summary = await applyTemplateUpdates({
|
|
1091
|
+
dryRun: answers.dryRun,
|
|
1092
|
+
projectDir,
|
|
1093
|
+
selectedPaths,
|
|
1094
|
+
templateDir,
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
if (!answers.dryRun) {
|
|
1098
|
+
await updateProjectTemplateConfig(projectDir, projectConfig, templateRepository, templateBranch);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
printUpdateSummary({
|
|
1102
|
+
branch: templateBranch,
|
|
1103
|
+
dryRun: answers.dryRun,
|
|
1104
|
+
include: answers.include,
|
|
1105
|
+
projectDir,
|
|
1106
|
+
repository: templateRepository,
|
|
1107
|
+
summary,
|
|
1108
|
+
});
|
|
1109
|
+
} finally {
|
|
1110
|
+
await fs.remove(templateDir);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
914
1114
|
async function runSetupRailway(argv) {
|
|
915
1115
|
const args = parseSetupRailwayArgs(argv);
|
|
916
1116
|
const answers = await collectSetupRailwayAnswers(args);
|
|
917
1117
|
const projectDir = path.resolve(process.cwd(), answers.directory);
|
|
1118
|
+
const projectConfig = await loadProjectConfig(projectDir);
|
|
1119
|
+
const projectSlug = resolveProjectSlug(projectDir, projectConfig);
|
|
918
1120
|
|
|
919
1121
|
await ensureProjectStructure(projectDir);
|
|
920
1122
|
await ensureRailwayCliInstalled();
|
|
@@ -991,17 +1193,18 @@ async function runSetupRailway(argv) {
|
|
|
991
1193
|
console.log(pc.bold("\nApplication services"));
|
|
992
1194
|
manifest.appServices ||= {};
|
|
993
1195
|
for (const spec of RAILWAY_APP_SERVICE_SPECS) {
|
|
1196
|
+
const serviceName = buildRailwayAppServiceName(projectSlug, spec.baseName);
|
|
994
1197
|
const serviceResult = await ensureRailwayAppService({
|
|
995
1198
|
aliases: spec.aliases,
|
|
996
1199
|
dryRun: answers.dryRun,
|
|
997
1200
|
existingServices,
|
|
998
1201
|
key: spec.key,
|
|
999
1202
|
manifest,
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1203
|
+
projectDir,
|
|
1204
|
+
railwayContext,
|
|
1205
|
+
serviceName,
|
|
1206
|
+
seedImage: spec.key === "admin" ? "nginx:1.29-alpine" : "alpine:3.22",
|
|
1207
|
+
});
|
|
1005
1208
|
appServiceSummary.push(serviceResult);
|
|
1006
1209
|
}
|
|
1007
1210
|
|
|
@@ -1010,6 +1213,7 @@ async function runSetupRailway(argv) {
|
|
|
1010
1213
|
manifest.environmentName = railwayContext.environmentName || manifest.environmentName || null;
|
|
1011
1214
|
manifest.projectId = railwayContext.projectId || manifest.projectId || null;
|
|
1012
1215
|
manifest.projectName = railwayContext.projectName || manifest.projectName || null;
|
|
1216
|
+
manifest.projectSlug = projectSlug;
|
|
1013
1217
|
manifest.updatedAt = new Date().toISOString();
|
|
1014
1218
|
|
|
1015
1219
|
const servicesAfterProvision = await discoverRailwayServices(railwayContext, projectDir);
|
|
@@ -1026,6 +1230,7 @@ async function runSetupRailway(argv) {
|
|
|
1026
1230
|
dryRun: answers.dryRun,
|
|
1027
1231
|
manifest,
|
|
1028
1232
|
projectDir,
|
|
1233
|
+
projectSlug,
|
|
1029
1234
|
railwayContext,
|
|
1030
1235
|
services: servicesAfterProvision,
|
|
1031
1236
|
});
|
|
@@ -1050,6 +1255,8 @@ async function runSyncRailwayEnv(argv) {
|
|
|
1050
1255
|
const args = parseSetupRailwayArgs(argv);
|
|
1051
1256
|
const answers = await collectSetupRailwayAnswers(args);
|
|
1052
1257
|
const projectDir = path.resolve(process.cwd(), answers.directory);
|
|
1258
|
+
const projectConfig = await loadProjectConfig(projectDir);
|
|
1259
|
+
const projectSlug = resolveProjectSlug(projectDir, projectConfig);
|
|
1053
1260
|
|
|
1054
1261
|
await ensureProjectStructure(projectDir);
|
|
1055
1262
|
await ensureRailwayCliInstalled();
|
|
@@ -1080,6 +1287,7 @@ async function runSyncRailwayEnv(argv) {
|
|
|
1080
1287
|
manifest.environmentName = railwayContext.environmentName || manifest.environmentName || null;
|
|
1081
1288
|
manifest.projectId = railwayContext.projectId || manifest.projectId || null;
|
|
1082
1289
|
manifest.projectName = railwayContext.projectName || manifest.projectName || null;
|
|
1290
|
+
manifest.projectSlug = manifest.projectSlug || projectSlug;
|
|
1083
1291
|
manifest.updatedAt = new Date().toISOString();
|
|
1084
1292
|
|
|
1085
1293
|
if (!answers.dryRun) {
|
|
@@ -1098,6 +1306,67 @@ async function runSyncRailwayEnv(argv) {
|
|
|
1098
1306
|
});
|
|
1099
1307
|
}
|
|
1100
1308
|
|
|
1309
|
+
async function runDestroyRailway(argv) {
|
|
1310
|
+
const args = parseDestroyRailwayArgs(argv);
|
|
1311
|
+
const answers = await collectDestroyRailwayAnswers(args);
|
|
1312
|
+
const projectDir = path.resolve(process.cwd(), answers.directory);
|
|
1313
|
+
|
|
1314
|
+
await ensureProjectStructure(projectDir);
|
|
1315
|
+
await ensureRailwayCliInstalled();
|
|
1316
|
+
await ensureRailwayAuthenticated(projectDir, answers.environment);
|
|
1317
|
+
|
|
1318
|
+
const railwayContext = await loadRailwayContext(projectDir, answers.environment);
|
|
1319
|
+
const targetEnvironment = answers.environment || railwayContext.environmentId || railwayContext.environmentName;
|
|
1320
|
+
const targetProject = railwayContext.projectId || railwayContext.projectName;
|
|
1321
|
+
|
|
1322
|
+
if (answers.scope === "project") {
|
|
1323
|
+
if (!targetProject) {
|
|
1324
|
+
throw new Error(`Unable to determine Railway project for ${projectDir}. Link the directory first with \`railway link\`.`);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
console.log(pc.bold("\nDestroy Railway project"));
|
|
1328
|
+
console.log(`- Project: ${pc.bold(railwayContext.projectName || railwayContext.projectId)}`);
|
|
1329
|
+
if (answers.dryRun) {
|
|
1330
|
+
console.log(`- Dry run: would delete project ${pc.bold(targetProject)}`);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const commandArgs = ["project", "delete", "--project", targetProject, "--yes"];
|
|
1335
|
+
if (answers.twoFactorCode) {
|
|
1336
|
+
commandArgs.push("--2fa-code", answers.twoFactorCode);
|
|
1337
|
+
}
|
|
1338
|
+
await runRailwayCommand(projectDir, undefined, commandArgs);
|
|
1339
|
+
} else {
|
|
1340
|
+
if (!targetEnvironment) {
|
|
1341
|
+
throw new Error(`Unable to determine Railway environment for ${projectDir}. Pass \`--environment\` or link the directory first.`);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
await ensureRailwayEnvironmentLinked(projectDir, targetEnvironment);
|
|
1345
|
+
|
|
1346
|
+
console.log(pc.bold("\nDestroy Railway environment"));
|
|
1347
|
+
console.log(`- Environment: ${pc.bold(targetEnvironment)}`);
|
|
1348
|
+
if (railwayContext.projectName || railwayContext.projectId) {
|
|
1349
|
+
console.log(`- Project: ${pc.bold(railwayContext.projectName || railwayContext.projectId)}`);
|
|
1350
|
+
}
|
|
1351
|
+
if (answers.dryRun) {
|
|
1352
|
+
console.log(`- Dry run: would delete environment ${pc.bold(targetEnvironment)}`);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
const commandArgs = ["environment", "delete", targetEnvironment, "--yes"];
|
|
1357
|
+
if (answers.twoFactorCode) {
|
|
1358
|
+
commandArgs.push("--2fa-code", answers.twoFactorCode);
|
|
1359
|
+
}
|
|
1360
|
+
await runRailwayCommand(projectDir, undefined, commandArgs);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const manifestPath = path.join(projectDir, RAILWAY_MANIFEST_FILENAME);
|
|
1364
|
+
if (await fs.pathExists(manifestPath)) {
|
|
1365
|
+
await fs.remove(manifestPath);
|
|
1366
|
+
console.log(`- Removed local ${pc.bold(RAILWAY_MANIFEST_FILENAME)} manifest`);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1101
1370
|
function parseDirectoryArgs(argv) {
|
|
1102
1371
|
return { directory: argv[0] || "." };
|
|
1103
1372
|
}
|
|
@@ -1154,6 +1423,74 @@ function parseSetupRailwayArgs(argv) {
|
|
|
1154
1423
|
return options;
|
|
1155
1424
|
}
|
|
1156
1425
|
|
|
1426
|
+
function parseDestroyRailwayArgs(argv) {
|
|
1427
|
+
const options = {
|
|
1428
|
+
directory: ".",
|
|
1429
|
+
dryRun: false,
|
|
1430
|
+
environment: undefined,
|
|
1431
|
+
scope: "environment",
|
|
1432
|
+
twoFactorCode: undefined,
|
|
1433
|
+
yes: false,
|
|
1434
|
+
};
|
|
1435
|
+
const positionals = [];
|
|
1436
|
+
|
|
1437
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1438
|
+
const arg = argv[index];
|
|
1439
|
+
|
|
1440
|
+
if (arg === "--yes" || arg === "-y") {
|
|
1441
|
+
options.yes = true;
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (arg === "--dry-run") {
|
|
1446
|
+
options.dryRun = true;
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (arg === "--environment" || arg === "-e") {
|
|
1451
|
+
options.environment = argv[index + 1] || options.environment;
|
|
1452
|
+
index += 1;
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (arg.startsWith("--environment=")) {
|
|
1457
|
+
options.environment = arg.split("=")[1] || options.environment;
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
if (arg === "--scope") {
|
|
1462
|
+
options.scope = argv[index + 1] || options.scope;
|
|
1463
|
+
index += 1;
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (arg.startsWith("--scope=")) {
|
|
1468
|
+
options.scope = arg.split("=")[1] || options.scope;
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
if (arg === "--2fa-code") {
|
|
1473
|
+
options.twoFactorCode = argv[index + 1] || options.twoFactorCode;
|
|
1474
|
+
index += 1;
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (arg.startsWith("--2fa-code=")) {
|
|
1479
|
+
options.twoFactorCode = arg.split("=")[1] || options.twoFactorCode;
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
positionals.push(arg);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
options.directory = positionals[0] || options.directory;
|
|
1487
|
+
if (!["environment", "project"].includes(options.scope)) {
|
|
1488
|
+
throw new Error("--scope must be either 'environment' or 'project'");
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
return options;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1157
1494
|
async function collectSetupRailwayAnswers(args) {
|
|
1158
1495
|
if (args.yes) {
|
|
1159
1496
|
return {
|
|
@@ -1207,6 +1544,68 @@ async function collectSetupRailwayAnswers(args) {
|
|
|
1207
1544
|
};
|
|
1208
1545
|
}
|
|
1209
1546
|
|
|
1547
|
+
async function collectDestroyRailwayAnswers(args) {
|
|
1548
|
+
if (args.yes) {
|
|
1549
|
+
return args;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
const directory = await prompt(
|
|
1553
|
+
text({
|
|
1554
|
+
defaultValue: args.directory,
|
|
1555
|
+
message: "Project directory linked to Railway?",
|
|
1556
|
+
placeholder: ".",
|
|
1557
|
+
validate(value) {
|
|
1558
|
+
return value.trim().length === 0 ? "Project directory is required" : undefined;
|
|
1559
|
+
},
|
|
1560
|
+
}),
|
|
1561
|
+
);
|
|
1562
|
+
|
|
1563
|
+
const scope = await prompt(
|
|
1564
|
+
select({
|
|
1565
|
+
initialValue: args.scope,
|
|
1566
|
+
message: "What should be deleted?",
|
|
1567
|
+
options: [
|
|
1568
|
+
{ label: "Environment", value: "environment", hint: "Delete one Railway environment and its services/resources" },
|
|
1569
|
+
{ label: "Project", value: "project", hint: "Delete the whole Railway project" },
|
|
1570
|
+
],
|
|
1571
|
+
}),
|
|
1572
|
+
);
|
|
1573
|
+
|
|
1574
|
+
let environment = args.environment;
|
|
1575
|
+
if (scope === "environment" && !environment) {
|
|
1576
|
+
environment = await prompt(
|
|
1577
|
+
text({
|
|
1578
|
+
defaultValue: "",
|
|
1579
|
+
message: "Railway environment name or ID? (leave empty for linked default)",
|
|
1580
|
+
placeholder: "production",
|
|
1581
|
+
}),
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
const confirmed = await prompt(
|
|
1586
|
+
confirm({
|
|
1587
|
+
initialValue: false,
|
|
1588
|
+
message:
|
|
1589
|
+
scope === "project"
|
|
1590
|
+
? "Delete the whole Railway project and all its environments?"
|
|
1591
|
+
: `Delete the Railway environment${environment ? ` ${environment}` : ""} and all its services/resources?`,
|
|
1592
|
+
}),
|
|
1593
|
+
);
|
|
1594
|
+
|
|
1595
|
+
if (!confirmed) {
|
|
1596
|
+
throw new Error("Railway teardown cancelled.");
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
return {
|
|
1600
|
+
directory,
|
|
1601
|
+
dryRun: args.dryRun,
|
|
1602
|
+
environment: environment?.trim() || undefined,
|
|
1603
|
+
scope,
|
|
1604
|
+
twoFactorCode: args.twoFactorCode,
|
|
1605
|
+
yes: true,
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1210
1609
|
async function ensureRailwayCliInstalled() {
|
|
1211
1610
|
const result = await checkCommand({ command: "railway", args: ["--version"], name: "railway" });
|
|
1212
1611
|
if (!result.ok) {
|
|
@@ -1250,6 +1649,7 @@ async function readRailwayManifest(projectDir) {
|
|
|
1250
1649
|
bucket: DEFAULT_RAILWAY_BUCKET,
|
|
1251
1650
|
environmentId: null,
|
|
1252
1651
|
environmentName: null,
|
|
1652
|
+
projectSlug: null,
|
|
1253
1653
|
projectId: null,
|
|
1254
1654
|
projectName: null,
|
|
1255
1655
|
resources: {},
|
|
@@ -1539,11 +1939,12 @@ async function deployRailwayAppServices(config) {
|
|
|
1539
1939
|
const summary = [];
|
|
1540
1940
|
for (const spec of RAILWAY_APP_SERVICE_SPECS) {
|
|
1541
1941
|
const manifestEntry = config.manifest.appServices?.[spec.key];
|
|
1542
|
-
const
|
|
1543
|
-
const
|
|
1942
|
+
const defaultServiceName = buildRailwayAppServiceName(config.projectSlug, spec.baseName);
|
|
1943
|
+
const service = findRailwayService(config.services, spec.aliases, manifestEntry?.serviceName || defaultServiceName);
|
|
1944
|
+
const targetServiceName = service?.name || manifestEntry?.serviceName || defaultServiceName;
|
|
1544
1945
|
|
|
1545
1946
|
if (!service && !config.dryRun) {
|
|
1546
|
-
console.log(`- ${pc.yellow(
|
|
1947
|
+
console.log(`- ${pc.yellow(defaultServiceName)} service not found, skipping deployment`);
|
|
1547
1948
|
continue;
|
|
1548
1949
|
}
|
|
1549
1950
|
|
|
@@ -1891,7 +2292,7 @@ function findRailwayService(services, aliases, preferredName) {
|
|
|
1891
2292
|
const normalizedAliases = aliases.map(normalizeRailwayServiceName);
|
|
1892
2293
|
return services.find((service) => {
|
|
1893
2294
|
const normalizedName = normalizeRailwayServiceName(service.name);
|
|
1894
|
-
return normalizedAliases.some((alias) => normalizedName === alias || normalizedName.
|
|
2295
|
+
return normalizedAliases.some((alias) => normalizedName === alias || normalizedName.endsWith(`-${alias}`));
|
|
1895
2296
|
});
|
|
1896
2297
|
}
|
|
1897
2298
|
|
|
@@ -1976,14 +2377,28 @@ function updateRailwayManifestAppServices(manifest, services) {
|
|
|
1976
2377
|
manifest.appServices ||= {};
|
|
1977
2378
|
|
|
1978
2379
|
const entries = [
|
|
1979
|
-
[
|
|
1980
|
-
|
|
2380
|
+
[
|
|
2381
|
+
"api",
|
|
2382
|
+
findRailwayService(
|
|
2383
|
+
services,
|
|
2384
|
+
["api", "backend", "server"],
|
|
2385
|
+
manifest.appServices.api?.serviceName || buildRailwayAppServiceName(manifest.projectSlug, "api"),
|
|
2386
|
+
),
|
|
2387
|
+
],
|
|
2388
|
+
[
|
|
2389
|
+
"admin",
|
|
2390
|
+
findRailwayService(
|
|
2391
|
+
services,
|
|
2392
|
+
["admin", "frontend", "web"],
|
|
2393
|
+
manifest.appServices.admin?.serviceName || buildRailwayAppServiceName(manifest.projectSlug, "admin"),
|
|
2394
|
+
),
|
|
2395
|
+
],
|
|
1981
2396
|
[
|
|
1982
2397
|
"realtime",
|
|
1983
2398
|
findRailwayService(
|
|
1984
2399
|
services,
|
|
1985
2400
|
["realtime-gateway", "realtime"],
|
|
1986
|
-
manifest.appServices.realtime?.serviceName,
|
|
2401
|
+
manifest.appServices.realtime?.serviceName || buildRailwayAppServiceName(manifest.projectSlug, "realtime-gateway"),
|
|
1987
2402
|
),
|
|
1988
2403
|
],
|
|
1989
2404
|
];
|
|
@@ -2401,6 +2816,125 @@ async function ensureProjectStructure(projectDir) {
|
|
|
2401
2816
|
}
|
|
2402
2817
|
}
|
|
2403
2818
|
|
|
2819
|
+
async function loadProjectConfig(projectDir) {
|
|
2820
|
+
const configPath = path.join(projectDir, "asaje.config.json");
|
|
2821
|
+
if (!(await fs.pathExists(configPath))) {
|
|
2822
|
+
return null;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
return fs.readJson(configPath);
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
function resolveProjectSlug(projectDir, projectConfig) {
|
|
2829
|
+
return slugify(projectConfig?.projectSlug || projectConfig?.projectName || path.basename(projectDir) || "asaje-app");
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
function buildRailwayAppServiceName(projectSlug, baseName) {
|
|
2833
|
+
const normalizedSlug = slugify(projectSlug || "asaje-app");
|
|
2834
|
+
const normalizedBaseName = slugify(baseName);
|
|
2835
|
+
return `${normalizedSlug}-${normalizedBaseName}`;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
async function updateProjectTemplateConfig(projectDir, projectConfig, templateRepository, templateBranch) {
|
|
2839
|
+
const configPath = path.join(projectDir, "asaje.config.json");
|
|
2840
|
+
const nextConfig = {
|
|
2841
|
+
...(projectConfig || {}),
|
|
2842
|
+
template: {
|
|
2843
|
+
branch: templateBranch,
|
|
2844
|
+
repository: templateRepository,
|
|
2845
|
+
},
|
|
2846
|
+
};
|
|
2847
|
+
|
|
2848
|
+
await fs.writeJson(configPath, nextConfig, { spaces: 2 });
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
async function applyTemplateUpdates(config) {
|
|
2852
|
+
const summary = {
|
|
2853
|
+
missing: [],
|
|
2854
|
+
skipped: [],
|
|
2855
|
+
updated: [],
|
|
2856
|
+
};
|
|
2857
|
+
|
|
2858
|
+
for (const relativePath of config.selectedPaths) {
|
|
2859
|
+
const sourcePath = path.join(config.templateDir, relativePath);
|
|
2860
|
+
const destinationPath = path.join(config.projectDir, relativePath);
|
|
2861
|
+
|
|
2862
|
+
if (!(await fs.pathExists(sourcePath))) {
|
|
2863
|
+
summary.missing.push(relativePath);
|
|
2864
|
+
continue;
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
const sourceStats = await fs.stat(sourcePath);
|
|
2868
|
+
if (config.dryRun) {
|
|
2869
|
+
summary.updated.push(relativePath);
|
|
2870
|
+
continue;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
if (sourceStats.isDirectory()) {
|
|
2874
|
+
await fs.remove(destinationPath);
|
|
2875
|
+
await fs.copy(sourcePath, destinationPath);
|
|
2876
|
+
} else {
|
|
2877
|
+
await fs.ensureDir(path.dirname(destinationPath));
|
|
2878
|
+
await fs.copyFile(sourcePath, destinationPath);
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
summary.updated.push(relativePath);
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
summary.skipped = config.selectedPaths.filter((relativePath) => !summary.updated.includes(relativePath) && !summary.missing.includes(relativePath));
|
|
2885
|
+
return summary;
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
function printUpdateSummary(config) {
|
|
2889
|
+
console.log(pc.bold("\nUpdate"));
|
|
2890
|
+
console.log(`- Directory: ${pc.bold(config.projectDir)}`);
|
|
2891
|
+
console.log(`- Template: ${pc.bold(`${config.repository}#${config.branch}`)}`);
|
|
2892
|
+
console.log(`- Safe paths: ${pc.bold(String(SAFE_UPDATE_PATHS.length))}`);
|
|
2893
|
+
if (config.include.length > 0) {
|
|
2894
|
+
console.log(`- Extra include: ${pc.bold(config.include.join(", "))}`);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
console.log(pc.bold("\nUpdated"));
|
|
2898
|
+
if (config.summary.updated.length === 0) {
|
|
2899
|
+
console.log("- No files selected for update");
|
|
2900
|
+
} else {
|
|
2901
|
+
for (const relativePath of config.summary.updated) {
|
|
2902
|
+
console.log(`- ${config.dryRun ? "would update" : "updated"} ${pc.bold(relativePath)}`);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
if (config.summary.missing.length > 0) {
|
|
2907
|
+
console.log(pc.bold("\nMissing In Template"));
|
|
2908
|
+
for (const relativePath of config.summary.missing) {
|
|
2909
|
+
console.log(`- ${pc.bold(relativePath)}`);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
if (config.dryRun) {
|
|
2914
|
+
console.log("- Dry run only, local files were not modified");
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
function splitCommaSeparatedPaths(value) {
|
|
2919
|
+
return value
|
|
2920
|
+
.split(",")
|
|
2921
|
+
.map((entry) => normalizeRelativePath(entry))
|
|
2922
|
+
.filter(Boolean);
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
function uniquePaths(paths) {
|
|
2926
|
+
return [...new Set(paths.map((entry) => normalizeRelativePath(entry)).filter(Boolean))];
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
function normalizeRelativePath(value) {
|
|
2930
|
+
const trimmed = String(value || "").trim();
|
|
2931
|
+
if (!trimmed) {
|
|
2932
|
+
return "";
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
return trimmed.replace(/^\.\//, "").replace(/\\/g, "/").replace(/\/$/, "");
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2404
2938
|
async function ensureEnvFiles(projectDir) {
|
|
2405
2939
|
for (const spec of ENV_FILE_SPECS) {
|
|
2406
2940
|
const envPath = path.join(projectDir, spec.envPath);
|