abw-react-starter 1.0.1 → 1.0.2

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.
Files changed (2) hide show
  1. package/bin/create.js +336 -58
  2. package/package.json +1 -1
package/bin/create.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { Command } from "commander";
4
4
  import inquirer from "inquirer";
@@ -35,6 +35,32 @@ async function runCapture(cmd, args, opts = {}) {
35
35
  return p.stdout?.trim();
36
36
  }
37
37
 
38
+ async function runCombined(cmd, args, opts = {}) {
39
+ try {
40
+ const result = await execa(cmd, args, {
41
+ all: true,
42
+ ...opts,
43
+ });
44
+
45
+ return {
46
+ ok: true,
47
+ code: 0,
48
+ output: result.all?.trim() || "",
49
+ stdout: result.stdout?.trim() || "",
50
+ stderr: result.stderr?.trim() || "",
51
+ };
52
+ } catch (err) {
53
+ return {
54
+ ok: false,
55
+ code: err?.exitCode ?? 1,
56
+ output: String(err?.all || err?.stderr || err?.stdout || err?.message || "").trim(),
57
+ stdout: String(err?.stdout || "").trim(),
58
+ stderr: String(err?.stderr || "").trim(),
59
+ error: err,
60
+ };
61
+ }
62
+ }
63
+
38
64
  function ensureNoTrailingSlash(url) {
39
65
  return (url || "").replace(/\/+$/, "");
40
66
  }
@@ -47,9 +73,15 @@ async function assertCmdExists(cmd, args = ["--version"]) {
47
73
  }
48
74
  }
49
75
 
50
- function looksLikeHerokuNameTakenError(err) {
51
- const msg = String(err?.stderr || err?.shortMessage || err?.message || "").toLowerCase();
52
- return msg.includes("name") && (msg.includes("taken") || msg.includes("already exists"));
76
+ function looksLikeHerokuNameTakenError(output) {
77
+ const msg = String(output || "").toLowerCase();
78
+ return (
79
+ msg.includes("name") &&
80
+ (msg.includes("taken") ||
81
+ msg.includes("already taken") ||
82
+ msg.includes("already exists") ||
83
+ msg.includes("invalid_params"))
84
+ );
53
85
  }
54
86
 
55
87
  async function getHerokuAppInfo(appName, cwd) {
@@ -57,6 +89,14 @@ async function getHerokuAppInfo(appName, cwd) {
57
89
  return JSON.parse(raw || "{}");
58
90
  }
59
91
 
92
+ async function tryCreateHerokuApp(appName, region, cwd) {
93
+ const args = ["create"];
94
+ if (appName) args.push(appName);
95
+ args.push("--region", region);
96
+
97
+ return runCombined("heroku", args, { cwd });
98
+ }
99
+
60
100
  async function createHerokuAppWithFallback({ cwd, requestedName, region, label }) {
61
101
  const attempts = [];
62
102
 
@@ -67,24 +107,40 @@ async function createHerokuAppWithFallback({ cwd, requestedName, region, label }
67
107
  }
68
108
 
69
109
  for (const candidate of attempts) {
70
- try {
71
- console.log(`\n=== ${label}: tentar criar app Heroku "${candidate}" ===\n`);
72
- await run("heroku", ["create", candidate, "--region", region], { cwd });
110
+ console.log(`\n=== ${label}: tentar criar app Heroku "${candidate}" ===\n`);
111
+
112
+ const res = await tryCreateHerokuApp(candidate, region, cwd);
113
+
114
+ if (res.output) {
115
+ console.log(res.output);
116
+ }
117
+
118
+ if (res.ok) {
73
119
  return candidate;
74
- } catch (err) {
75
- if (looksLikeHerokuNameTakenError(err)) {
76
- console.log(`⚠️ Nome já ocupado: ${candidate}`);
77
- continue;
78
- }
79
- throw err;
80
120
  }
121
+
122
+ if (looksLikeHerokuNameTakenError(res.output)) {
123
+ console.log(`⚠️ Nome já ocupado: ${candidate}`);
124
+ continue;
125
+ }
126
+
127
+ throw res.error || new Error(`Falha ao criar app Heroku (${label}).`);
81
128
  }
82
129
 
83
130
  console.log(`\n=== ${label}: a criar app Heroku com nome automático ===\n`);
84
- await run("heroku", ["create", "--region", region], { cwd });
85
131
 
86
- const whoRaw = await runCapture("heroku", ["apps", "--json"], { cwd });
87
- const apps = JSON.parse(whoRaw || "[]");
132
+ const autoRes = await tryCreateHerokuApp(null, region, cwd);
133
+
134
+ if (autoRes.output) {
135
+ console.log(autoRes.output);
136
+ }
137
+
138
+ if (!autoRes.ok) {
139
+ throw autoRes.error || new Error(`Falha ao criar app Heroku com nome automático (${label}).`);
140
+ }
141
+
142
+ const appsRaw = await runCapture("heroku", ["apps", "--json"], { cwd });
143
+ const apps = JSON.parse(appsRaw || "[]");
88
144
 
89
145
  if (!Array.isArray(apps) || apps.length === 0) {
90
146
  throw new Error(`Não consegui descobrir o nome final da app Heroku (${label}).`);
@@ -104,6 +160,33 @@ async function createHerokuAppWithFallback({ cwd, requestedName, region, label }
104
160
  return latest;
105
161
  }
106
162
 
163
+ function upsertEnvValue(content, key, value) {
164
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
165
+ const regex = new RegExp(`^${escapedKey}=.*$`, "m");
166
+ const line = `${key}=${value}`;
167
+
168
+ if (regex.test(content)) {
169
+ return content.replace(regex, line);
170
+ }
171
+
172
+ const trimmed = content.replace(/\s*$/, "");
173
+ return trimmed ? `${trimmed}\n${line}\n` : `${line}\n`;
174
+ }
175
+
176
+ async function mergeEnvFile(filePath, values) {
177
+ let content = "";
178
+
179
+ if (await fs.pathExists(filePath)) {
180
+ content = await fs.readFile(filePath, "utf8");
181
+ }
182
+
183
+ for (const [key, value] of Object.entries(values)) {
184
+ content = upsertEnvValue(content, key, value ?? "");
185
+ }
186
+
187
+ await fs.writeFile(filePath, content);
188
+ }
189
+
107
190
  /* =========================
108
191
  Heroku Login (melhorado)
109
192
  ========================= */
@@ -146,7 +229,8 @@ async function ensureHerokuLoginOrSkip() {
146
229
  ========================= */
147
230
 
148
231
  async function writeManifest(projectDir, data) {
149
- await fs.writeJson(path.join(projectDir, MANIFEST_NAME), data, {
232
+ const current = (await readManifest(projectDir)) || {};
233
+ await fs.writeJson(path.join(projectDir, MANIFEST_NAME), { ...current, ...data }, {
150
234
  spaces: 2,
151
235
  });
152
236
  }
@@ -190,6 +274,7 @@ async function configureBackendS3(projectDir, aws) {
190
274
 
191
275
  const bucketUrl = `https://${aws.bucket}.s3.${aws.region}.amazonaws.com`;
192
276
 
277
+ console.log("\n=== BACKEND: configurar middlewares.ts ===\n");
193
278
  await fs.writeFile(
194
279
  path.join(beDir, "config", "middlewares.ts"),
195
280
  `export default [
@@ -218,6 +303,7 @@ async function configureBackendS3(projectDir, aws) {
218
303
  `
219
304
  );
220
305
 
306
+ console.log("\n=== BACKEND: configurar plugins.ts ===\n");
221
307
  await fs.writeFile(
222
308
  path.join(beDir, "config", "plugins.ts"),
223
309
  `export default ({ env }) => ({
@@ -248,14 +334,37 @@ async function configureBackendS3(projectDir, aws) {
248
334
  `
249
335
  );
250
336
 
337
+ console.log("\n=== BACKEND: configurar server.ts ===\n");
251
338
  await fs.writeFile(
252
- path.join(beDir, ".env"),
253
- `AWS_ACCESS_KEY_ID=${aws.key}
254
- AWS_ACCESS_SECRET=${aws.secret}
255
- AWS_REGION=${aws.region}
256
- AWS_BUCKET=${aws.bucket}
339
+ path.join(beDir, "config", "server.ts"),
340
+ `import type { Core } from '@strapi/strapi';
341
+
342
+ const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => {
343
+ return {
344
+ host: env('HOST', '0.0.0.0'),
345
+ port: env.int('PORT', 1337),
346
+ app: {
347
+ keys: env.array('APP_KEYS'),
348
+ },
349
+ transfer: {
350
+ remote: {
351
+ enabled: true,
352
+ },
353
+ },
354
+ };
355
+ };
356
+
357
+ export default config;
257
358
  `
258
359
  );
360
+
361
+ console.log("\n=== BACKEND: atualizar .env ===\n");
362
+ await mergeEnvFile(path.join(beDir, ".env"), {
363
+ AWS_ACCESS_KEY_ID: aws.key,
364
+ AWS_ACCESS_SECRET: aws.secret,
365
+ AWS_REGION: aws.region,
366
+ AWS_BUCKET: aws.bucket,
367
+ });
259
368
  }
260
369
 
261
370
  /* =========================
@@ -300,6 +409,31 @@ async function createFrontendLocal(projectDir, nodeVersion) {
300
409
  });
301
410
 
302
411
  const feDir = path.join(projectDir, "frontend");
412
+
413
+ const nextConfigTs = path.join(feDir, "next.config.ts");
414
+ const nextConfigJs = path.join(feDir, "next.config.js");
415
+
416
+ if (await fs.pathExists(nextConfigTs)) {
417
+ await fs.remove(nextConfigTs);
418
+ await fs.writeFile(
419
+ nextConfigJs,
420
+ `/** @type {import('next').NextConfig} */
421
+ const nextConfig = {};
422
+
423
+ module.exports = nextConfig;
424
+ `
425
+ );
426
+ } else if (!(await fs.pathExists(nextConfigJs))) {
427
+ await fs.writeFile(
428
+ nextConfigJs,
429
+ `/** @type {import('next').NextConfig} */
430
+ const nextConfig = {};
431
+
432
+ module.exports = nextConfig;
433
+ `
434
+ );
435
+ }
436
+
303
437
  const pkgPath = path.join(feDir, "package.json");
304
438
  const pkg = await fs.readJson(pkgPath);
305
439
 
@@ -307,10 +441,16 @@ async function createFrontendLocal(projectDir, nodeVersion) {
307
441
  pkg.engines.node = nodeVersion;
308
442
 
309
443
  pkg.scripts = pkg.scripts || {};
444
+ pkg.scripts.build = pkg.scripts.build || "next build";
310
445
  pkg.scripts.start = "next start -p $PORT";
311
446
 
312
447
  await fs.writeJson(pkgPath, pkg, { spaces: 2 });
313
448
 
449
+ const envLocalPath = path.join(feDir, ".env.local");
450
+ if (!(await fs.pathExists(envLocalPath))) {
451
+ await fs.writeFile(envLocalPath, "NEXT_PUBLIC_STRAPI_URL=http://localhost:1337\n");
452
+ }
453
+
314
454
  await initGitRepo(feDir, "Init Next frontend");
315
455
  }
316
456
 
@@ -331,6 +471,24 @@ async function deployBackendHeroku({ projectDir, backendApp, region }) {
331
471
  await run("heroku", ["git:remote", "-a", finalBackendApp], { cwd: beDir });
332
472
  await run("heroku", ["addons:create", "heroku-postgresql", "-a", finalBackendApp], { cwd: beDir });
333
473
 
474
+ console.log("\n=== BACKEND: configurar env vars Strapi ===\n");
475
+ const appKeys = [randHex(16), randHex(16), randHex(16), randHex(16)].join(",");
476
+ const apiTokenSalt = randHex(16);
477
+ const adminJwtSecret = randHex(24);
478
+ const jwtSecret = randHex(24);
479
+
480
+ await run("heroku", ["config:set", `NODE_ENV=production`, "-a", finalBackendApp], { cwd: beDir });
481
+ await run("heroku", ["config:set", `APP_KEYS=${appKeys}`, "-a", finalBackendApp], { cwd: beDir });
482
+ await run("heroku", ["config:set", `API_TOKEN_SALT=${apiTokenSalt}`, "-a", finalBackendApp], { cwd: beDir });
483
+ await run("heroku", ["config:set", `ADMIN_JWT_SECRET=${adminJwtSecret}`, "-a", finalBackendApp], { cwd: beDir });
484
+ await run("heroku", ["config:set", `JWT_SECRET=${jwtSecret}`, "-a", finalBackendApp], { cwd: beDir });
485
+
486
+ await run("heroku", ["config:set", `DATABASE_CLIENT=postgres`, "-a", finalBackendApp], { cwd: beDir });
487
+ await run("heroku", ["config:set", `DATABASE_SSL=true`, "-a", finalBackendApp], { cwd: beDir });
488
+ await run("heroku", ["config:set", `DATABASE_SSL_REJECT_UNAUTHORIZED=false`, "-a", finalBackendApp], {
489
+ cwd: beDir,
490
+ });
491
+
334
492
  const envPath = path.join(beDir, ".env");
335
493
 
336
494
  if (await fs.pathExists(envPath)) {
@@ -349,13 +507,18 @@ async function deployBackendHeroku({ projectDir, backendApp, region }) {
349
507
  await run("heroku", ["config:set", `AWS_REGION=${awsRegion}`, "-a", finalBackendApp], { cwd: beDir });
350
508
  }
351
509
  if (awsAccessKeyId) {
352
- await run("heroku", ["config:set", `AWS_ACCESS_KEY_ID=${awsAccessKeyId}`, "-a", finalBackendApp], { cwd: beDir });
510
+ await run("heroku", ["config:set", `AWS_ACCESS_KEY_ID=${awsAccessKeyId}`, "-a", finalBackendApp], {
511
+ cwd: beDir,
512
+ });
353
513
  }
354
514
  if (awsAccessSecret) {
355
- await run("heroku", ["config:set", `AWS_ACCESS_SECRET=${awsAccessSecret}`, "-a", finalBackendApp], { cwd: beDir });
515
+ await run("heroku", ["config:set", `AWS_ACCESS_SECRET=${awsAccessSecret}`, "-a", finalBackendApp], {
516
+ cwd: beDir,
517
+ });
356
518
  }
357
519
  }
358
520
 
521
+ console.log("\n=== BACKEND: deploy ===\n");
359
522
  await run("git", ["push", "heroku", "main"], { cwd: beDir });
360
523
 
361
524
  const info = await getHerokuAppInfo(finalBackendApp, beDir);
@@ -387,10 +550,12 @@ async function deployFrontendHeroku({ projectDir, frontendApp, region, backendWe
387
550
 
388
551
  await run("heroku", ["git:remote", "-a", finalFrontendApp], { cwd: feDir });
389
552
 
553
+ console.log("\n=== FRONTEND: configurar env var ===\n");
390
554
  await run("heroku", ["config:set", `NEXT_PUBLIC_STRAPI_URL=${backendWebUrl}`, "-a", finalFrontendApp], {
391
555
  cwd: feDir,
392
556
  });
393
557
 
558
+ console.log("\n=== FRONTEND: deploy ===\n");
394
559
  await run("git", ["push", "heroku", "main"], { cwd: feDir });
395
560
 
396
561
  const info = await getHerokuAppInfo(finalFrontendApp, feDir);
@@ -422,7 +587,6 @@ async function doCreate(projectDirArg, opts) {
422
587
  const region = (opts.region || "eu").toLowerCase();
423
588
  const nodeVersion = opts.node || "20.x";
424
589
 
425
- // S3
426
590
  const { useS3 } = await inquirer.prompt([
427
591
  { type: "confirm", name: "useS3", message: "Usar S3?", default: true },
428
592
  ]);
@@ -432,9 +596,9 @@ async function doCreate(projectDirArg, opts) {
432
596
  if (useS3) {
433
597
  aws = await inquirer.prompt([
434
598
  { name: "bucket", message: "Bucket:" },
435
- { name: "region", default: "eu-north-1" },
599
+ { name: "region", message: "Region:", default: "eu-north-1" },
436
600
  { name: "key", message: "Access Key:" },
437
- { type: "password", name: "secret", message: "Secret:" },
601
+ { type: "password", name: "secret", message: "Secret:", mask: "*" },
438
602
  ]);
439
603
  }
440
604
 
@@ -456,7 +620,9 @@ async function doCreate(projectDirArg, opts) {
456
620
 
457
621
  await createBackendLocal(projectDir);
458
622
 
459
- if (useS3) await configureBackendS3(projectDir, aws);
623
+ if (useS3 && aws) {
624
+ await configureBackendS3(projectDir, aws);
625
+ }
460
626
 
461
627
  await createFrontendLocal(projectDir, nodeVersion);
462
628
 
@@ -467,31 +633,93 @@ async function doCreate(projectDirArg, opts) {
467
633
  backendApp,
468
634
  frontendApp,
469
635
  useS3,
470
- awsBucket: aws?.bucket,
471
- awsRegion: aws?.region,
636
+ awsBucket: aws?.bucket || null,
637
+ awsRegion: aws?.region || null,
472
638
  createdAt: new Date().toISOString(),
639
+ deployedAt: null,
640
+ backendUrl: null,
641
+ frontendUrl: null,
473
642
  });
474
643
 
475
- if (!deployNow) return;
644
+ if (!deployNow) {
645
+ console.log("\n✅ Projeto criado localmente (sem Heroku).");
646
+ console.log(`📁 Pasta: ${projectDir}`);
647
+ console.log("\nPara correr local:");
648
+ console.log("- Backend: cd backend && npm run develop");
649
+ console.log("- Frontend: cd frontend && npm run dev");
650
+ console.log("\nQuando quiseres publicar:");
651
+ console.log(`- abw-react-starter publish "${projectDir}"`);
652
+ return;
653
+ }
654
+
655
+ const publishAnswers = await inquirer.prompt([
656
+ {
657
+ type: "input",
658
+ name: "backendApp",
659
+ message: "Nome da app Heroku do backend (Strapi):",
660
+ default: backendApp,
661
+ validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
662
+ },
663
+ {
664
+ type: "input",
665
+ name: "frontendApp",
666
+ message: "Nome da app Heroku do frontend (Next):",
667
+ default: frontendApp,
668
+ validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
669
+ },
670
+ {
671
+ type: "confirm",
672
+ name: "confirm",
673
+ message: "Confirmas criar 2 apps Heroku e fazer deploy?",
674
+ default: true,
675
+ },
676
+ ]);
677
+
678
+ if (!publishAnswers.confirm) {
679
+ console.log("\n✅ Projeto criado localmente (não publicaste no Heroku).");
680
+ console.log(`Quando quiseres publicar: abw-react-starter publish "${projectDir}"`);
681
+ return;
682
+ }
683
+
684
+ backendApp = publishAnswers.backendApp;
685
+ frontendApp = publishAnswers.frontendApp;
686
+
687
+ await writeManifest(projectDir, {
688
+ backendApp,
689
+ frontendApp,
690
+ });
476
691
 
477
692
  const canPublish = await ensureHerokuLoginOrSkip();
478
- if (!canPublish) return;
479
693
 
480
- await doPublish(projectDir);
694
+ if (!canPublish) {
695
+ console.log("\n✅ Projeto criado localmente (sem publicar no Heroku).");
696
+ console.log(`📁 Pasta: ${projectDir}`);
697
+ console.log("\nPara publicar mais tarde:");
698
+ console.log(`- abw-react-starter publish "${projectDir}"`);
699
+ return;
700
+ }
701
+
702
+ await doPublish(projectDir, { skipHerokuLoginCheck: true });
481
703
  }
482
704
 
483
705
  /* =========================
484
706
  Publish
485
707
  ========================= */
486
708
 
487
- async function doPublish(projectDirArg) {
709
+ async function doPublish(projectDirArg, options = {}) {
710
+ const { skipHerokuLoginCheck = false } = options;
488
711
  const projectDir = resolveProjectDir(projectDirArg);
489
712
 
490
713
  await assertCmdExists("git");
491
714
  await assertCmdExists("heroku");
492
715
 
493
- const canPublish = await ensureHerokuLoginOrSkip();
494
- if (!canPublish) return;
716
+ if (!skipHerokuLoginCheck) {
717
+ const canPublish = await ensureHerokuLoginOrSkip();
718
+ if (!canPublish) {
719
+ console.log("\nℹ️ Publicação cancelada. O projeto continua localmente sem deploy no Heroku.");
720
+ return;
721
+ }
722
+ }
495
723
 
496
724
  const manifest = await readManifest(projectDir);
497
725
  if (!manifest) {
@@ -500,21 +728,59 @@ async function doPublish(projectDirArg) {
500
728
  );
501
729
  }
502
730
 
731
+ const region = (manifest.region || "eu").toLowerCase();
732
+ const backendDir = path.join(projectDir, "backend");
733
+ const frontendDir = path.join(projectDir, "frontend");
734
+
735
+ if (!(await fs.pathExists(backendDir))) throw new Error("Não encontrei a pasta backend.");
736
+ if (!(await fs.pathExists(frontendDir))) throw new Error("Não encontrei a pasta frontend.");
737
+
738
+ let backendApp = manifest.backendApp;
739
+ let frontendApp = manifest.frontendApp;
740
+
741
+ if (!backendApp || !frontendApp) {
742
+ const projectBase = path.basename(projectDir);
743
+ const a = await inquirer.prompt([
744
+ {
745
+ type: "input",
746
+ name: "backendApp",
747
+ message: "Nome da app Heroku do backend (Strapi):",
748
+ default: backendApp || `${projectBase}-api`,
749
+ validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
750
+ },
751
+ {
752
+ type: "input",
753
+ name: "frontendApp",
754
+ message: "Nome da app Heroku do frontend (Next):",
755
+ default: frontendApp || `${projectBase}-web`,
756
+ validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
757
+ },
758
+ ]);
759
+
760
+ backendApp = a.backendApp;
761
+ frontendApp = a.frontendApp;
762
+
763
+ await writeManifest(projectDir, {
764
+ backendApp,
765
+ frontendApp,
766
+ });
767
+ }
768
+
503
769
  const backendResult = await deployBackendHeroku({
504
770
  projectDir,
505
- backendApp: manifest.backendApp,
506
- region: manifest.region,
771
+ backendApp,
772
+ region,
507
773
  });
508
774
 
509
775
  const frontendResult = await deployFrontendHeroku({
510
776
  projectDir,
511
- frontendApp: manifest.frontendApp,
512
- region: manifest.region,
777
+ frontendApp,
778
+ region,
513
779
  backendWebUrl: backendResult.backendWebUrl,
514
780
  });
515
781
 
516
782
  await writeManifest(projectDir, {
517
- ...manifest,
783
+ deployedAt: new Date().toISOString(),
518
784
  backendApp: backendResult.backendApp,
519
785
  frontendApp: frontendResult.frontendApp,
520
786
  backendUrl: backendResult.backendWebUrl,
@@ -522,45 +788,57 @@ async function doPublish(projectDirArg) {
522
788
  });
523
789
 
524
790
  console.log("\n✅ Tudo pronto!");
525
- console.log("Backend app:", backendResult.backendApp);
526
- console.log("Backend:", backendResult.backendWebUrl);
527
- console.log("Frontend app:", frontendResult.frontendApp);
528
- console.log("Frontend:", frontendResult.frontendWebUrl);
791
+ console.log(`Backend app: ${backendResult.backendApp}`);
792
+ console.log(`Backend (Strapi): ${backendResult.backendWebUrl}`);
793
+ console.log(`Admin: ${backendResult.backendWebUrl}/admin`);
794
+ console.log(`Frontend app: ${frontendResult.frontendApp}`);
795
+ console.log(`Frontend (Next): ${frontendResult.frontendWebUrl}`);
796
+ console.log("\nNotas:");
797
+ console.log("- Content types normalmente crias localmente e depois fazes push para o backend Heroku.");
798
+ console.log("- Em produção, dá permissões em Public role para expor endpoints.");
529
799
  }
530
800
 
531
801
  /* =========================
532
- CLI (INALTERADO)
802
+ CLI
533
803
  ========================= */
534
804
 
535
805
  program.name("abw-react-starter");
536
806
 
807
+ // Comando create (default)
537
808
  program
538
809
  .command("create")
539
- .argument("<projectDir>")
540
- .option("--backend-app <name>")
541
- .option("--frontend-app <name>")
542
- .option("--node <version>", "20.x")
543
- .option("--region <region>", "eu")
810
+ .argument("<projectDir>", "Diretório do projeto (será criado)")
811
+ .option("--backend-app <name>", "Nome da app Heroku do backend (Strapi)")
812
+ .option("--frontend-app <name>", "Nome da app Heroku do frontend (Next)")
813
+ .option("--node <version>", "Versão Node para Heroku (ex: 20.x)", "20.x")
814
+ .option("--region <region>", "Heroku region (eu | us)", "eu")
544
815
  .action(async (projectDirArg, opts) => {
545
816
  await doCreate(projectDirArg, opts);
546
817
  });
547
818
 
819
+ // Para compatibilidade: se chamarem sem "create", trata como create
548
820
  program
549
- .argument("[projectDir]")
550
- .option("--backend-app <name>")
551
- .option("--frontend-app <name>")
552
- .option("--node <version>", "20.x")
553
- .option("--region <region>", "eu");
821
+ .argument("[projectDir]", "Diretório do projeto (será criado)")
822
+ .option("--backend-app <name>", "Nome da app Heroku do backend (Strapi)")
823
+ .option("--frontend-app <name>", "Nome da app Heroku do frontend (Next)")
824
+ .option("--node <version>", "Versão Node para Heroku (ex: 20.x)", "20.x")
825
+ .option("--region <region>", "Heroku region (eu | us)", "eu");
554
826
 
827
+ // Comando publish
555
828
  program
556
829
  .command("publish")
557
- .argument("[projectDir]")
830
+ .argument("[projectDir]", "Diretório do projeto (default: .)")
831
+ .description("Publicar um projeto existente no Heroku")
558
832
  .action(async (projectDirArg) => {
559
833
  await doPublish(projectDirArg || ".");
560
834
  });
561
835
 
836
+ // Se executarem sem subcomando, faz create
562
837
  program.action(async (projectDirArg, opts) => {
563
- if (!projectDirArg) return program.help();
838
+ if (!projectDirArg) {
839
+ program.help();
840
+ return;
841
+ }
564
842
  await doCreate(projectDirArg, opts);
565
843
  });
566
844
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abw-react-starter",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "CLI para criar projetos Strapi + Next + Heroku",
5
5
  "bin": {
6
6
  "abw-react-starter": "./bin/create.js"