create-asaje-go-vue 0.2.6 → 0.2.8

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 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 {
@@ -46,6 +47,20 @@ const RAILWAY_APP_SERVICE_SPECS = [
46
47
  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,6 +1069,48 @@ 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);
@@ -1098,6 +1298,67 @@ async function runSyncRailwayEnv(argv) {
1098
1298
  });
1099
1299
  }
1100
1300
 
1301
+ async function runDestroyRailway(argv) {
1302
+ const args = parseDestroyRailwayArgs(argv);
1303
+ const answers = await collectDestroyRailwayAnswers(args);
1304
+ const projectDir = path.resolve(process.cwd(), answers.directory);
1305
+
1306
+ await ensureProjectStructure(projectDir);
1307
+ await ensureRailwayCliInstalled();
1308
+ await ensureRailwayAuthenticated(projectDir, answers.environment);
1309
+
1310
+ const railwayContext = await loadRailwayContext(projectDir, answers.environment);
1311
+ const targetEnvironment = answers.environment || railwayContext.environmentId || railwayContext.environmentName;
1312
+ const targetProject = railwayContext.projectId || railwayContext.projectName;
1313
+
1314
+ if (answers.scope === "project") {
1315
+ if (!targetProject) {
1316
+ throw new Error(`Unable to determine Railway project for ${projectDir}. Link the directory first with \`railway link\`.`);
1317
+ }
1318
+
1319
+ console.log(pc.bold("\nDestroy Railway project"));
1320
+ console.log(`- Project: ${pc.bold(railwayContext.projectName || railwayContext.projectId)}`);
1321
+ if (answers.dryRun) {
1322
+ console.log(`- Dry run: would delete project ${pc.bold(targetProject)}`);
1323
+ return;
1324
+ }
1325
+
1326
+ const commandArgs = ["project", "delete", "--project", targetProject, "--yes"];
1327
+ if (answers.twoFactorCode) {
1328
+ commandArgs.push("--2fa-code", answers.twoFactorCode);
1329
+ }
1330
+ await runRailwayCommand(projectDir, undefined, commandArgs);
1331
+ } else {
1332
+ if (!targetEnvironment) {
1333
+ throw new Error(`Unable to determine Railway environment for ${projectDir}. Pass \`--environment\` or link the directory first.`);
1334
+ }
1335
+
1336
+ await ensureRailwayEnvironmentLinked(projectDir, targetEnvironment);
1337
+
1338
+ console.log(pc.bold("\nDestroy Railway environment"));
1339
+ console.log(`- Environment: ${pc.bold(targetEnvironment)}`);
1340
+ if (railwayContext.projectName || railwayContext.projectId) {
1341
+ console.log(`- Project: ${pc.bold(railwayContext.projectName || railwayContext.projectId)}`);
1342
+ }
1343
+ if (answers.dryRun) {
1344
+ console.log(`- Dry run: would delete environment ${pc.bold(targetEnvironment)}`);
1345
+ return;
1346
+ }
1347
+
1348
+ const commandArgs = ["environment", "delete", targetEnvironment, "--yes"];
1349
+ if (answers.twoFactorCode) {
1350
+ commandArgs.push("--2fa-code", answers.twoFactorCode);
1351
+ }
1352
+ await runRailwayCommand(projectDir, undefined, commandArgs);
1353
+ }
1354
+
1355
+ const manifestPath = path.join(projectDir, RAILWAY_MANIFEST_FILENAME);
1356
+ if (await fs.pathExists(manifestPath)) {
1357
+ await fs.remove(manifestPath);
1358
+ console.log(`- Removed local ${pc.bold(RAILWAY_MANIFEST_FILENAME)} manifest`);
1359
+ }
1360
+ }
1361
+
1101
1362
  function parseDirectoryArgs(argv) {
1102
1363
  return { directory: argv[0] || "." };
1103
1364
  }
@@ -1154,6 +1415,74 @@ function parseSetupRailwayArgs(argv) {
1154
1415
  return options;
1155
1416
  }
1156
1417
 
1418
+ function parseDestroyRailwayArgs(argv) {
1419
+ const options = {
1420
+ directory: ".",
1421
+ dryRun: false,
1422
+ environment: undefined,
1423
+ scope: "environment",
1424
+ twoFactorCode: undefined,
1425
+ yes: false,
1426
+ };
1427
+ const positionals = [];
1428
+
1429
+ for (let index = 0; index < argv.length; index += 1) {
1430
+ const arg = argv[index];
1431
+
1432
+ if (arg === "--yes" || arg === "-y") {
1433
+ options.yes = true;
1434
+ continue;
1435
+ }
1436
+
1437
+ if (arg === "--dry-run") {
1438
+ options.dryRun = true;
1439
+ continue;
1440
+ }
1441
+
1442
+ if (arg === "--environment" || arg === "-e") {
1443
+ options.environment = argv[index + 1] || options.environment;
1444
+ index += 1;
1445
+ continue;
1446
+ }
1447
+
1448
+ if (arg.startsWith("--environment=")) {
1449
+ options.environment = arg.split("=")[1] || options.environment;
1450
+ continue;
1451
+ }
1452
+
1453
+ if (arg === "--scope") {
1454
+ options.scope = argv[index + 1] || options.scope;
1455
+ index += 1;
1456
+ continue;
1457
+ }
1458
+
1459
+ if (arg.startsWith("--scope=")) {
1460
+ options.scope = arg.split("=")[1] || options.scope;
1461
+ continue;
1462
+ }
1463
+
1464
+ if (arg === "--2fa-code") {
1465
+ options.twoFactorCode = argv[index + 1] || options.twoFactorCode;
1466
+ index += 1;
1467
+ continue;
1468
+ }
1469
+
1470
+ if (arg.startsWith("--2fa-code=")) {
1471
+ options.twoFactorCode = arg.split("=")[1] || options.twoFactorCode;
1472
+ continue;
1473
+ }
1474
+
1475
+ positionals.push(arg);
1476
+ }
1477
+
1478
+ options.directory = positionals[0] || options.directory;
1479
+ if (!["environment", "project"].includes(options.scope)) {
1480
+ throw new Error("--scope must be either 'environment' or 'project'");
1481
+ }
1482
+
1483
+ return options;
1484
+ }
1485
+
1157
1486
  async function collectSetupRailwayAnswers(args) {
1158
1487
  if (args.yes) {
1159
1488
  return {
@@ -1207,6 +1536,68 @@ async function collectSetupRailwayAnswers(args) {
1207
1536
  };
1208
1537
  }
1209
1538
 
1539
+ async function collectDestroyRailwayAnswers(args) {
1540
+ if (args.yes) {
1541
+ return args;
1542
+ }
1543
+
1544
+ const directory = await prompt(
1545
+ text({
1546
+ defaultValue: args.directory,
1547
+ message: "Project directory linked to Railway?",
1548
+ placeholder: ".",
1549
+ validate(value) {
1550
+ return value.trim().length === 0 ? "Project directory is required" : undefined;
1551
+ },
1552
+ }),
1553
+ );
1554
+
1555
+ const scope = await prompt(
1556
+ select({
1557
+ initialValue: args.scope,
1558
+ message: "What should be deleted?",
1559
+ options: [
1560
+ { label: "Environment", value: "environment", hint: "Delete one Railway environment and its services/resources" },
1561
+ { label: "Project", value: "project", hint: "Delete the whole Railway project" },
1562
+ ],
1563
+ }),
1564
+ );
1565
+
1566
+ let environment = args.environment;
1567
+ if (scope === "environment" && !environment) {
1568
+ environment = await prompt(
1569
+ text({
1570
+ defaultValue: "",
1571
+ message: "Railway environment name or ID? (leave empty for linked default)",
1572
+ placeholder: "production",
1573
+ }),
1574
+ );
1575
+ }
1576
+
1577
+ const confirmed = await prompt(
1578
+ confirm({
1579
+ initialValue: false,
1580
+ message:
1581
+ scope === "project"
1582
+ ? "Delete the whole Railway project and all its environments?"
1583
+ : `Delete the Railway environment${environment ? ` ${environment}` : ""} and all its services/resources?`,
1584
+ }),
1585
+ );
1586
+
1587
+ if (!confirmed) {
1588
+ throw new Error("Railway teardown cancelled.");
1589
+ }
1590
+
1591
+ return {
1592
+ directory,
1593
+ dryRun: args.dryRun,
1594
+ environment: environment?.trim() || undefined,
1595
+ scope,
1596
+ twoFactorCode: args.twoFactorCode,
1597
+ yes: true,
1598
+ };
1599
+ }
1600
+
1210
1601
  async function ensureRailwayCliInstalled() {
1211
1602
  const result = await checkCommand({ command: "railway", args: ["--version"], name: "railway" });
1212
1603
  if (!result.ok) {
@@ -1504,6 +1895,8 @@ async function ensureRailwayAppService(config) {
1504
1895
  config.serviceName,
1505
1896
  "--image",
1506
1897
  config.seedImage || "alpine:3.22",
1898
+ "--variables",
1899
+ `ASAJE_BOOTSTRAP_SERVICE=${config.serviceName}`,
1507
1900
  ]);
1508
1901
  }
1509
1902
 
@@ -2399,6 +2792,115 @@ async function ensureProjectStructure(projectDir) {
2399
2792
  }
2400
2793
  }
2401
2794
 
2795
+ async function loadProjectConfig(projectDir) {
2796
+ const configPath = path.join(projectDir, "asaje.config.json");
2797
+ if (!(await fs.pathExists(configPath))) {
2798
+ return null;
2799
+ }
2800
+
2801
+ return fs.readJson(configPath);
2802
+ }
2803
+
2804
+ async function updateProjectTemplateConfig(projectDir, projectConfig, templateRepository, templateBranch) {
2805
+ const configPath = path.join(projectDir, "asaje.config.json");
2806
+ const nextConfig = {
2807
+ ...(projectConfig || {}),
2808
+ template: {
2809
+ branch: templateBranch,
2810
+ repository: templateRepository,
2811
+ },
2812
+ };
2813
+
2814
+ await fs.writeJson(configPath, nextConfig, { spaces: 2 });
2815
+ }
2816
+
2817
+ async function applyTemplateUpdates(config) {
2818
+ const summary = {
2819
+ missing: [],
2820
+ skipped: [],
2821
+ updated: [],
2822
+ };
2823
+
2824
+ for (const relativePath of config.selectedPaths) {
2825
+ const sourcePath = path.join(config.templateDir, relativePath);
2826
+ const destinationPath = path.join(config.projectDir, relativePath);
2827
+
2828
+ if (!(await fs.pathExists(sourcePath))) {
2829
+ summary.missing.push(relativePath);
2830
+ continue;
2831
+ }
2832
+
2833
+ const sourceStats = await fs.stat(sourcePath);
2834
+ if (config.dryRun) {
2835
+ summary.updated.push(relativePath);
2836
+ continue;
2837
+ }
2838
+
2839
+ if (sourceStats.isDirectory()) {
2840
+ await fs.remove(destinationPath);
2841
+ await fs.copy(sourcePath, destinationPath);
2842
+ } else {
2843
+ await fs.ensureDir(path.dirname(destinationPath));
2844
+ await fs.copyFile(sourcePath, destinationPath);
2845
+ }
2846
+
2847
+ summary.updated.push(relativePath);
2848
+ }
2849
+
2850
+ summary.skipped = config.selectedPaths.filter((relativePath) => !summary.updated.includes(relativePath) && !summary.missing.includes(relativePath));
2851
+ return summary;
2852
+ }
2853
+
2854
+ function printUpdateSummary(config) {
2855
+ console.log(pc.bold("\nUpdate"));
2856
+ console.log(`- Directory: ${pc.bold(config.projectDir)}`);
2857
+ console.log(`- Template: ${pc.bold(`${config.repository}#${config.branch}`)}`);
2858
+ console.log(`- Safe paths: ${pc.bold(String(SAFE_UPDATE_PATHS.length))}`);
2859
+ if (config.include.length > 0) {
2860
+ console.log(`- Extra include: ${pc.bold(config.include.join(", "))}`);
2861
+ }
2862
+
2863
+ console.log(pc.bold("\nUpdated"));
2864
+ if (config.summary.updated.length === 0) {
2865
+ console.log("- No files selected for update");
2866
+ } else {
2867
+ for (const relativePath of config.summary.updated) {
2868
+ console.log(`- ${config.dryRun ? "would update" : "updated"} ${pc.bold(relativePath)}`);
2869
+ }
2870
+ }
2871
+
2872
+ if (config.summary.missing.length > 0) {
2873
+ console.log(pc.bold("\nMissing In Template"));
2874
+ for (const relativePath of config.summary.missing) {
2875
+ console.log(`- ${pc.bold(relativePath)}`);
2876
+ }
2877
+ }
2878
+
2879
+ if (config.dryRun) {
2880
+ console.log("- Dry run only, local files were not modified");
2881
+ }
2882
+ }
2883
+
2884
+ function splitCommaSeparatedPaths(value) {
2885
+ return value
2886
+ .split(",")
2887
+ .map((entry) => normalizeRelativePath(entry))
2888
+ .filter(Boolean);
2889
+ }
2890
+
2891
+ function uniquePaths(paths) {
2892
+ return [...new Set(paths.map((entry) => normalizeRelativePath(entry)).filter(Boolean))];
2893
+ }
2894
+
2895
+ function normalizeRelativePath(value) {
2896
+ const trimmed = String(value || "").trim();
2897
+ if (!trimmed) {
2898
+ return "";
2899
+ }
2900
+
2901
+ return trimmed.replace(/^\.\//, "").replace(/\\/g, "/").replace(/\/$/, "");
2902
+ }
2903
+
2402
2904
  async function ensureEnvFiles(projectDir) {
2403
2905
  for (const spec of ENV_FILE_SPECS) {
2404
2906
  const envPath = path.join(projectDir, spec.envPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-asaje-go-vue",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "CLI to scaffold, configure, and run the Asaje Go + Vue boilerplate",
5
5
  "type": "module",
6
6
  "bin": {