abw-react-starter 1.0.1 → 1.0.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.
Files changed (2) hide show
  1. package/bin/create.js +360 -59
  2. package/package.json +1 -1
package/bin/create.js CHANGED
@@ -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,8 +160,47 @@ 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
+
190
+ async function commitAllIfNeeded(cwd, message) {
191
+ await run("git", ["add", "."], { cwd });
192
+
193
+ const status = await runCapture("git", ["status", "--porcelain"], { cwd });
194
+ if (!status) {
195
+ console.log("ℹ️ Nada novo para commitar");
196
+ return;
197
+ }
198
+
199
+ await run("git", ["commit", "-m", message], { cwd });
200
+ }
201
+
107
202
  /* =========================
108
- Heroku Login (melhorado)
203
+ Heroku Login
109
204
  ========================= */
110
205
 
111
206
  async function isHerokuLoggedIn() {
@@ -146,7 +241,8 @@ async function ensureHerokuLoginOrSkip() {
146
241
  ========================= */
147
242
 
148
243
  async function writeManifest(projectDir, data) {
149
- await fs.writeJson(path.join(projectDir, MANIFEST_NAME), data, {
244
+ const current = (await readManifest(projectDir)) || {};
245
+ await fs.writeJson(path.join(projectDir, MANIFEST_NAME), { ...current, ...data }, {
150
246
  spaces: 2,
151
247
  });
152
248
  }
@@ -179,7 +275,7 @@ async function initGitRepo(cwd, message) {
179
275
  }
180
276
 
181
277
  /* =========================
182
- 🆕 S3 Setup
278
+ S3 Setup
183
279
  ========================= */
184
280
 
185
281
  async function configureBackendS3(projectDir, aws) {
@@ -190,6 +286,7 @@ async function configureBackendS3(projectDir, aws) {
190
286
 
191
287
  const bucketUrl = `https://${aws.bucket}.s3.${aws.region}.amazonaws.com`;
192
288
 
289
+ console.log("\n=== BACKEND: configurar middlewares.ts ===\n");
193
290
  await fs.writeFile(
194
291
  path.join(beDir, "config", "middlewares.ts"),
195
292
  `export default [
@@ -218,6 +315,7 @@ async function configureBackendS3(projectDir, aws) {
218
315
  `
219
316
  );
220
317
 
318
+ console.log("\n=== BACKEND: configurar plugins.ts ===\n");
221
319
  await fs.writeFile(
222
320
  path.join(beDir, "config", "plugins.ts"),
223
321
  `export default ({ env }) => ({
@@ -248,14 +346,37 @@ async function configureBackendS3(projectDir, aws) {
248
346
  `
249
347
  );
250
348
 
349
+ console.log("\n=== BACKEND: configurar server.ts ===\n");
251
350
  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}
351
+ path.join(beDir, "config", "server.ts"),
352
+ `import type { Core } from '@strapi/strapi';
353
+
354
+ const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => {
355
+ return {
356
+ host: env('HOST', '0.0.0.0'),
357
+ port: env.int('PORT', 1337),
358
+ app: {
359
+ keys: env.array('APP_KEYS'),
360
+ },
361
+ transfer: {
362
+ remote: {
363
+ enabled: true,
364
+ },
365
+ },
366
+ };
367
+ };
368
+
369
+ export default config;
257
370
  `
258
371
  );
372
+
373
+ console.log("\n=== BACKEND: atualizar .env ===\n");
374
+ await mergeEnvFile(path.join(beDir, ".env"), {
375
+ AWS_ACCESS_KEY_ID: aws.key,
376
+ AWS_ACCESS_SECRET: aws.secret,
377
+ AWS_REGION: aws.region,
378
+ AWS_BUCKET: aws.bucket,
379
+ });
259
380
  }
260
381
 
261
382
  /* =========================
@@ -286,6 +407,7 @@ async function createBackendLocal(projectDir) {
286
407
  cwd: path.join(projectDir, "backend"),
287
408
  });
288
409
 
410
+ console.log("\n=== BACKEND: git init/commit ===\n");
289
411
  await initGitRepo(path.join(projectDir, "backend"), "Init Strapi backend");
290
412
  }
291
413
 
@@ -300,6 +422,31 @@ async function createFrontendLocal(projectDir, nodeVersion) {
300
422
  });
301
423
 
302
424
  const feDir = path.join(projectDir, "frontend");
425
+
426
+ const nextConfigTs = path.join(feDir, "next.config.ts");
427
+ const nextConfigJs = path.join(feDir, "next.config.js");
428
+
429
+ if (await fs.pathExists(nextConfigTs)) {
430
+ await fs.remove(nextConfigTs);
431
+ await fs.writeFile(
432
+ nextConfigJs,
433
+ `/** @type {import('next').NextConfig} */
434
+ const nextConfig = {};
435
+
436
+ module.exports = nextConfig;
437
+ `
438
+ );
439
+ } else if (!(await fs.pathExists(nextConfigJs))) {
440
+ await fs.writeFile(
441
+ nextConfigJs,
442
+ `/** @type {import('next').NextConfig} */
443
+ const nextConfig = {};
444
+
445
+ module.exports = nextConfig;
446
+ `
447
+ );
448
+ }
449
+
303
450
  const pkgPath = path.join(feDir, "package.json");
304
451
  const pkg = await fs.readJson(pkgPath);
305
452
 
@@ -307,10 +454,17 @@ async function createFrontendLocal(projectDir, nodeVersion) {
307
454
  pkg.engines.node = nodeVersion;
308
455
 
309
456
  pkg.scripts = pkg.scripts || {};
457
+ pkg.scripts.build = pkg.scripts.build || "next build";
310
458
  pkg.scripts.start = "next start -p $PORT";
311
459
 
312
460
  await fs.writeJson(pkgPath, pkg, { spaces: 2 });
313
461
 
462
+ const envLocalPath = path.join(feDir, ".env.local");
463
+ if (!(await fs.pathExists(envLocalPath))) {
464
+ await fs.writeFile(envLocalPath, "NEXT_PUBLIC_STRAPI_URL=http://localhost:1337\n");
465
+ }
466
+
467
+ console.log("\n=== FRONTEND: git init/commit ===\n");
314
468
  await initGitRepo(feDir, "Init Next frontend");
315
469
  }
316
470
 
@@ -331,6 +485,24 @@ async function deployBackendHeroku({ projectDir, backendApp, region }) {
331
485
  await run("heroku", ["git:remote", "-a", finalBackendApp], { cwd: beDir });
332
486
  await run("heroku", ["addons:create", "heroku-postgresql", "-a", finalBackendApp], { cwd: beDir });
333
487
 
488
+ console.log("\n=== BACKEND: configurar env vars Strapi ===\n");
489
+ const appKeys = [randHex(16), randHex(16), randHex(16), randHex(16)].join(",");
490
+ const apiTokenSalt = randHex(16);
491
+ const adminJwtSecret = randHex(24);
492
+ const jwtSecret = randHex(24);
493
+
494
+ await run("heroku", ["config:set", `NODE_ENV=production`, "-a", finalBackendApp], { cwd: beDir });
495
+ await run("heroku", ["config:set", `APP_KEYS=${appKeys}`, "-a", finalBackendApp], { cwd: beDir });
496
+ await run("heroku", ["config:set", `API_TOKEN_SALT=${apiTokenSalt}`, "-a", finalBackendApp], { cwd: beDir });
497
+ await run("heroku", ["config:set", `ADMIN_JWT_SECRET=${adminJwtSecret}`, "-a", finalBackendApp], { cwd: beDir });
498
+ await run("heroku", ["config:set", `JWT_SECRET=${jwtSecret}`, "-a", finalBackendApp], { cwd: beDir });
499
+
500
+ await run("heroku", ["config:set", `DATABASE_CLIENT=postgres`, "-a", finalBackendApp], { cwd: beDir });
501
+ await run("heroku", ["config:set", `DATABASE_SSL=true`, "-a", finalBackendApp], { cwd: beDir });
502
+ await run("heroku", ["config:set", `DATABASE_SSL_REJECT_UNAUTHORIZED=false`, "-a", finalBackendApp], {
503
+ cwd: beDir,
504
+ });
505
+
334
506
  const envPath = path.join(beDir, ".env");
335
507
 
336
508
  if (await fs.pathExists(envPath)) {
@@ -349,13 +521,21 @@ async function deployBackendHeroku({ projectDir, backendApp, region }) {
349
521
  await run("heroku", ["config:set", `AWS_REGION=${awsRegion}`, "-a", finalBackendApp], { cwd: beDir });
350
522
  }
351
523
  if (awsAccessKeyId) {
352
- await run("heroku", ["config:set", `AWS_ACCESS_KEY_ID=${awsAccessKeyId}`, "-a", finalBackendApp], { cwd: beDir });
524
+ await run("heroku", ["config:set", `AWS_ACCESS_KEY_ID=${awsAccessKeyId}`, "-a", finalBackendApp], {
525
+ cwd: beDir,
526
+ });
353
527
  }
354
528
  if (awsAccessSecret) {
355
- await run("heroku", ["config:set", `AWS_ACCESS_SECRET=${awsAccessSecret}`, "-a", finalBackendApp], { cwd: beDir });
529
+ await run("heroku", ["config:set", `AWS_ACCESS_SECRET=${awsAccessSecret}`, "-a", finalBackendApp], {
530
+ cwd: beDir,
531
+ });
356
532
  }
357
533
  }
358
534
 
535
+ console.log("\n=== BACKEND: garantir commit antes do deploy ===\n");
536
+ await commitAllIfNeeded(beDir, "Pre-deploy commit");
537
+
538
+ console.log("\n=== BACKEND: deploy ===\n");
359
539
  await run("git", ["push", "heroku", "main"], { cwd: beDir });
360
540
 
361
541
  const info = await getHerokuAppInfo(finalBackendApp, beDir);
@@ -387,10 +567,15 @@ async function deployFrontendHeroku({ projectDir, frontendApp, region, backendWe
387
567
 
388
568
  await run("heroku", ["git:remote", "-a", finalFrontendApp], { cwd: feDir });
389
569
 
570
+ console.log("\n=== FRONTEND: configurar env var ===\n");
390
571
  await run("heroku", ["config:set", `NEXT_PUBLIC_STRAPI_URL=${backendWebUrl}`, "-a", finalFrontendApp], {
391
572
  cwd: feDir,
392
573
  });
393
574
 
575
+ console.log("\n=== FRONTEND: garantir commit antes do deploy ===\n");
576
+ await commitAllIfNeeded(feDir, "Pre-deploy commit");
577
+
578
+ console.log("\n=== FRONTEND: deploy ===\n");
394
579
  await run("git", ["push", "heroku", "main"], { cwd: feDir });
395
580
 
396
581
  const info = await getHerokuAppInfo(finalFrontendApp, feDir);
@@ -422,7 +607,6 @@ async function doCreate(projectDirArg, opts) {
422
607
  const region = (opts.region || "eu").toLowerCase();
423
608
  const nodeVersion = opts.node || "20.x";
424
609
 
425
- // S3
426
610
  const { useS3 } = await inquirer.prompt([
427
611
  { type: "confirm", name: "useS3", message: "Usar S3?", default: true },
428
612
  ]);
@@ -432,9 +616,9 @@ async function doCreate(projectDirArg, opts) {
432
616
  if (useS3) {
433
617
  aws = await inquirer.prompt([
434
618
  { name: "bucket", message: "Bucket:" },
435
- { name: "region", default: "eu-north-1" },
619
+ { name: "region", message: "Region:", default: "eu-north-1" },
436
620
  { name: "key", message: "Access Key:" },
437
- { type: "password", name: "secret", message: "Secret:" },
621
+ { type: "password", name: "secret", message: "Secret:", mask: "*" },
438
622
  ]);
439
623
  }
440
624
 
@@ -456,7 +640,12 @@ async function doCreate(projectDirArg, opts) {
456
640
 
457
641
  await createBackendLocal(projectDir);
458
642
 
459
- if (useS3) await configureBackendS3(projectDir, aws);
643
+ if (useS3 && aws) {
644
+ await configureBackendS3(projectDir, aws);
645
+
646
+ console.log("\n=== BACKEND: commit configs S3 ===\n");
647
+ await commitAllIfNeeded(path.join(projectDir, "backend"), "Configure S3 upload");
648
+ }
460
649
 
461
650
  await createFrontendLocal(projectDir, nodeVersion);
462
651
 
@@ -467,31 +656,93 @@ async function doCreate(projectDirArg, opts) {
467
656
  backendApp,
468
657
  frontendApp,
469
658
  useS3,
470
- awsBucket: aws?.bucket,
471
- awsRegion: aws?.region,
659
+ awsBucket: aws?.bucket || null,
660
+ awsRegion: aws?.region || null,
472
661
  createdAt: new Date().toISOString(),
662
+ deployedAt: null,
663
+ backendUrl: null,
664
+ frontendUrl: null,
473
665
  });
474
666
 
475
- if (!deployNow) return;
667
+ if (!deployNow) {
668
+ console.log("\n✅ Projeto criado localmente (sem Heroku).");
669
+ console.log(`📁 Pasta: ${projectDir}`);
670
+ console.log("\nPara correr local:");
671
+ console.log("- Backend: cd backend && npm run develop");
672
+ console.log("- Frontend: cd frontend && npm run dev");
673
+ console.log("\nQuando quiseres publicar:");
674
+ console.log(`- abw-react-starter publish "${projectDir}"`);
675
+ return;
676
+ }
677
+
678
+ const publishAnswers = await inquirer.prompt([
679
+ {
680
+ type: "input",
681
+ name: "backendApp",
682
+ message: "Nome da app Heroku do backend (Strapi):",
683
+ default: backendApp,
684
+ validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
685
+ },
686
+ {
687
+ type: "input",
688
+ name: "frontendApp",
689
+ message: "Nome da app Heroku do frontend (Next):",
690
+ default: frontendApp,
691
+ validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
692
+ },
693
+ {
694
+ type: "confirm",
695
+ name: "confirm",
696
+ message: "Confirmas criar 2 apps Heroku e fazer deploy?",
697
+ default: true,
698
+ },
699
+ ]);
700
+
701
+ if (!publishAnswers.confirm) {
702
+ console.log("\n✅ Projeto criado localmente (não publicaste no Heroku).");
703
+ console.log(`Quando quiseres publicar: abw-react-starter publish "${projectDir}"`);
704
+ return;
705
+ }
706
+
707
+ backendApp = publishAnswers.backendApp;
708
+ frontendApp = publishAnswers.frontendApp;
709
+
710
+ await writeManifest(projectDir, {
711
+ backendApp,
712
+ frontendApp,
713
+ });
476
714
 
477
715
  const canPublish = await ensureHerokuLoginOrSkip();
478
- if (!canPublish) return;
479
716
 
480
- await doPublish(projectDir);
717
+ if (!canPublish) {
718
+ console.log("\n✅ Projeto criado localmente (sem publicar no Heroku).");
719
+ console.log(`📁 Pasta: ${projectDir}`);
720
+ console.log("\nPara publicar mais tarde:");
721
+ console.log(`- abw-react-starter publish "${projectDir}"`);
722
+ return;
723
+ }
724
+
725
+ await doPublish(projectDir, { skipHerokuLoginCheck: true });
481
726
  }
482
727
 
483
728
  /* =========================
484
729
  Publish
485
730
  ========================= */
486
731
 
487
- async function doPublish(projectDirArg) {
732
+ async function doPublish(projectDirArg, options = {}) {
733
+ const { skipHerokuLoginCheck = false } = options;
488
734
  const projectDir = resolveProjectDir(projectDirArg);
489
735
 
490
736
  await assertCmdExists("git");
491
737
  await assertCmdExists("heroku");
492
738
 
493
- const canPublish = await ensureHerokuLoginOrSkip();
494
- if (!canPublish) return;
739
+ if (!skipHerokuLoginCheck) {
740
+ const canPublish = await ensureHerokuLoginOrSkip();
741
+ if (!canPublish) {
742
+ console.log("\nℹ️ Publicação cancelada. O projeto continua localmente sem deploy no Heroku.");
743
+ return;
744
+ }
745
+ }
495
746
 
496
747
  const manifest = await readManifest(projectDir);
497
748
  if (!manifest) {
@@ -500,21 +751,59 @@ async function doPublish(projectDirArg) {
500
751
  );
501
752
  }
502
753
 
754
+ const region = (manifest.region || "eu").toLowerCase();
755
+ const backendDir = path.join(projectDir, "backend");
756
+ const frontendDir = path.join(projectDir, "frontend");
757
+
758
+ if (!(await fs.pathExists(backendDir))) throw new Error("Não encontrei a pasta backend.");
759
+ if (!(await fs.pathExists(frontendDir))) throw new Error("Não encontrei a pasta frontend.");
760
+
761
+ let backendApp = manifest.backendApp;
762
+ let frontendApp = manifest.frontendApp;
763
+
764
+ if (!backendApp || !frontendApp) {
765
+ const projectBase = path.basename(projectDir);
766
+ const a = await inquirer.prompt([
767
+ {
768
+ type: "input",
769
+ name: "backendApp",
770
+ message: "Nome da app Heroku do backend (Strapi):",
771
+ default: backendApp || `${projectBase}-api`,
772
+ validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
773
+ },
774
+ {
775
+ type: "input",
776
+ name: "frontendApp",
777
+ message: "Nome da app Heroku do frontend (Next):",
778
+ default: frontendApp || `${projectBase}-web`,
779
+ validate: (v) => (v?.length >= 3 ? true : "Nome muito curto"),
780
+ },
781
+ ]);
782
+
783
+ backendApp = a.backendApp;
784
+ frontendApp = a.frontendApp;
785
+
786
+ await writeManifest(projectDir, {
787
+ backendApp,
788
+ frontendApp,
789
+ });
790
+ }
791
+
503
792
  const backendResult = await deployBackendHeroku({
504
793
  projectDir,
505
- backendApp: manifest.backendApp,
506
- region: manifest.region,
794
+ backendApp,
795
+ region,
507
796
  });
508
797
 
509
798
  const frontendResult = await deployFrontendHeroku({
510
799
  projectDir,
511
- frontendApp: manifest.frontendApp,
512
- region: manifest.region,
800
+ frontendApp,
801
+ region,
513
802
  backendWebUrl: backendResult.backendWebUrl,
514
803
  });
515
804
 
516
805
  await writeManifest(projectDir, {
517
- ...manifest,
806
+ deployedAt: new Date().toISOString(),
518
807
  backendApp: backendResult.backendApp,
519
808
  frontendApp: frontendResult.frontendApp,
520
809
  backendUrl: backendResult.backendWebUrl,
@@ -522,45 +811,57 @@ async function doPublish(projectDirArg) {
522
811
  });
523
812
 
524
813
  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);
814
+ console.log(`Backend app: ${backendResult.backendApp}`);
815
+ console.log(`Backend (Strapi): ${backendResult.backendWebUrl}`);
816
+ console.log(`Admin: ${backendResult.backendWebUrl}/admin`);
817
+ console.log(`Frontend app: ${frontendResult.frontendApp}`);
818
+ console.log(`Frontend (Next): ${frontendResult.frontendWebUrl}`);
819
+ console.log("\nNotas:");
820
+ console.log("- Content types normalmente crias localmente e depois fazes push para o backend Heroku.");
821
+ console.log("- Em produção, dá permissões em Public role para expor endpoints.");
529
822
  }
530
823
 
531
824
  /* =========================
532
- CLI (INALTERADO)
825
+ CLI
533
826
  ========================= */
534
827
 
535
828
  program.name("abw-react-starter");
536
829
 
830
+ // Comando create (default)
537
831
  program
538
832
  .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")
833
+ .argument("<projectDir>", "Diretório do projeto (será criado)")
834
+ .option("--backend-app <name>", "Nome da app Heroku do backend (Strapi)")
835
+ .option("--frontend-app <name>", "Nome da app Heroku do frontend (Next)")
836
+ .option("--node <version>", "Versão Node para Heroku (ex: 20.x)", "20.x")
837
+ .option("--region <region>", "Heroku region (eu | us)", "eu")
544
838
  .action(async (projectDirArg, opts) => {
545
839
  await doCreate(projectDirArg, opts);
546
840
  });
547
841
 
842
+ // Para compatibilidade: se chamarem sem "create", trata como create
548
843
  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");
844
+ .argument("[projectDir]", "Diretório do projeto (será criado)")
845
+ .option("--backend-app <name>", "Nome da app Heroku do backend (Strapi)")
846
+ .option("--frontend-app <name>", "Nome da app Heroku do frontend (Next)")
847
+ .option("--node <version>", "Versão Node para Heroku (ex: 20.x)", "20.x")
848
+ .option("--region <region>", "Heroku region (eu | us)", "eu");
554
849
 
850
+ // Comando publish
555
851
  program
556
852
  .command("publish")
557
- .argument("[projectDir]")
853
+ .argument("[projectDir]", "Diretório do projeto (default: .)")
854
+ .description("Publicar um projeto existente no Heroku")
558
855
  .action(async (projectDirArg) => {
559
856
  await doPublish(projectDirArg || ".");
560
857
  });
561
858
 
859
+ // Se executarem sem subcomando, faz create
562
860
  program.action(async (projectDirArg, opts) => {
563
- if (!projectDirArg) return program.help();
861
+ if (!projectDirArg) {
862
+ program.help();
863
+ return;
864
+ }
564
865
  await doCreate(projectDirArg, opts);
565
866
  });
566
867
 
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.3",
4
4
  "description": "CLI para criar projetos Strapi + Next + Heroku",
5
5
  "bin": {
6
6
  "abw-react-starter": "./bin/create.js"