create-ampless 0.2.0-alpha.3 → 0.2.0-alpha.6

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/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { spinner, outro, log } from "@clack/prompts";
4
+ import { spinner as spinner2, outro, log as log3 } from "@clack/prompts";
5
5
  import { existsSync as existsSync2 } from "fs";
6
- import { resolve as resolve3 } from "path";
6
+ import { basename as basename3, resolve as resolve4 } from "path";
7
7
 
8
8
  // src/prompts.ts
9
9
  import * as p from "@clack/prompts";
@@ -191,36 +191,1359 @@ async function scaffold(sharedDir, themesRoot, destDir, opts) {
191
191
  await substituteDir(destDir, vars);
192
192
  }
193
193
 
194
- // src/index.ts
194
+ // src/args.ts
195
+ var VALID_THEMES = ["blog", "minimal", "landing", "corporate", "docs", "dads"];
196
+ var VALID_PLUGINS = ["seo", "rss", "webhook"];
197
+ var STRING_FLAGS = /* @__PURE__ */ new Set([
198
+ "--site-name",
199
+ "--themes",
200
+ "--plugins",
201
+ "--github-owner",
202
+ "--github-token",
203
+ "--aws-profile",
204
+ "--aws-region",
205
+ "--domain",
206
+ "--subdomain",
207
+ "--iam-service-role"
208
+ ]);
209
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
210
+ "--deploy",
211
+ "--github-private",
212
+ "--create-iam-role",
213
+ "--skip-confirm",
214
+ "--help",
215
+ "-h"
216
+ ]);
217
+ function parseDeployArgs(argv) {
218
+ const out = {
219
+ deploy: false,
220
+ githubPrivate: false,
221
+ createIamRole: false,
222
+ skipConfirm: false,
223
+ help: false,
224
+ unknown: []
225
+ };
226
+ for (let i = 0; i < argv.length; i++) {
227
+ const raw = argv[i];
228
+ let token = raw;
229
+ let inlineValue;
230
+ const eq = raw.indexOf("=");
231
+ if (raw.startsWith("--") && eq > 2) {
232
+ token = raw.slice(0, eq);
233
+ inlineValue = raw.slice(eq + 1);
234
+ }
235
+ if (BOOLEAN_FLAGS.has(token)) {
236
+ switch (token) {
237
+ case "--deploy":
238
+ out.deploy = true;
239
+ break;
240
+ case "--github-private":
241
+ out.githubPrivate = true;
242
+ break;
243
+ case "--create-iam-role":
244
+ out.createIamRole = true;
245
+ break;
246
+ case "--skip-confirm":
247
+ out.skipConfirm = true;
248
+ break;
249
+ case "--help":
250
+ case "-h":
251
+ out.help = true;
252
+ break;
253
+ }
254
+ continue;
255
+ }
256
+ if (STRING_FLAGS.has(token)) {
257
+ const value = inlineValue ?? argv[++i];
258
+ if (value === void 0) {
259
+ throw new Error(`Missing value for ${token}`);
260
+ }
261
+ switch (token) {
262
+ case "--site-name":
263
+ out.siteName = value;
264
+ break;
265
+ case "--themes": {
266
+ const themes = value.split(",").map((t) => t.trim()).filter(Boolean);
267
+ const invalid = themes.filter((t) => !VALID_THEMES.includes(t));
268
+ if (invalid.length > 0) {
269
+ throw new Error(
270
+ `Invalid theme(s): ${invalid.join(", ")}. Valid values: ${VALID_THEMES.join(", ")}`
271
+ );
272
+ }
273
+ out.themes = themes;
274
+ break;
275
+ }
276
+ case "--plugins": {
277
+ const plugins = value.split(",").map((p3) => p3.trim()).filter(Boolean);
278
+ const invalid = plugins.filter((p3) => !VALID_PLUGINS.includes(p3));
279
+ if (invalid.length > 0) {
280
+ throw new Error(
281
+ `Invalid plugin(s): ${invalid.join(", ")}. Valid values: ${VALID_PLUGINS.join(", ")}`
282
+ );
283
+ }
284
+ out.plugins = plugins;
285
+ break;
286
+ }
287
+ case "--github-owner":
288
+ out.githubOwner = value;
289
+ break;
290
+ case "--github-token":
291
+ out.githubToken = value;
292
+ break;
293
+ case "--aws-profile":
294
+ out.awsProfile = value;
295
+ break;
296
+ case "--aws-region":
297
+ out.awsRegion = value;
298
+ break;
299
+ case "--domain":
300
+ out.domain = value;
301
+ break;
302
+ case "--subdomain":
303
+ out.subdomain = value;
304
+ break;
305
+ case "--iam-service-role":
306
+ out.iamServiceRole = value;
307
+ break;
308
+ }
309
+ continue;
310
+ }
311
+ if (raw.startsWith("-")) {
312
+ out.unknown.push(raw);
313
+ continue;
314
+ }
315
+ if (out.projectName === void 0) {
316
+ out.projectName = raw;
317
+ } else {
318
+ out.unknown.push(raw);
319
+ }
320
+ }
321
+ return out;
322
+ }
323
+ var HELP_TEXT = `create-ampless \u2014 scaffold an ampless project
324
+
325
+ Usage:
326
+ npx create-ampless@alpha <project-name> [options]
327
+
328
+ Options:
329
+ --site-name <name> Site display name (default: "My Blog")
330
+ --themes <list> Comma-separated theme names to install
331
+ Valid: blog, minimal, landing, corporate, docs, dads
332
+ (default: blog)
333
+ --plugins <list> Comma-separated plugin names to install
334
+ Valid: seo, rss, webhook
335
+ (default: seo)
336
+ --deploy Also create GitHub repo + Amplify Hosting app and
337
+ kick off the first deploy after scaffolding
338
+ --github-owner <login> GitHub owner (user or org). Defaults to the
339
+ authenticated 'gh' user
340
+ --github-private Create a private repo (default: public)
341
+ --github-token <token> GitHub token. Falls back to GITHUB_TOKEN env,
342
+ then 'gh auth token', then an interactive prompt
343
+ --aws-profile <profile> AWS profile name to pass to the aws CLI
344
+ --aws-region <region> AWS region (defaults to aws config / env)
345
+ --domain <name> Custom domain (apex or subdomain) to attach
346
+ --subdomain <prefix> Subdomain prefix for the domain (default: apex)
347
+ --iam-service-role <arn> Existing IAM role for Amplify Hosting (must trust
348
+ amplify.amazonaws.com and have
349
+ AdministratorAccess-Amplify attached)
350
+ --create-iam-role Let create-ampless provision the Amplify Hosting
351
+ service role (idempotent; defaults to role name
352
+ AmplifyDeployBackend)
353
+ --skip-confirm Skip all interactive prompts and use defaults /
354
+ flag values (for CI / automation)
355
+ -h, --help Show this message
356
+ `;
357
+
358
+ // src/deploy-prompts.ts
359
+ import * as p2 from "@clack/prompts";
360
+ import { execa as execa3 } from "execa";
361
+
362
+ // src/deploy.ts
363
+ import { execa as execa2 } from "execa";
364
+ import { writeFile as writeFile2 } from "fs/promises";
365
+ import { basename as basename2, resolve as resolve3 } from "path";
366
+ import { spinner, log } from "@clack/prompts";
367
+ import pc2 from "picocolors";
368
+
369
+ // src/preflight.ts
370
+ import { execa } from "execa";
371
+ import { basename } from "path";
195
372
  import pc from "picocolors";
373
+ var DEFAULT_AMPLIFY_ROLE_NAME = "AmplifyDeployBackend";
374
+ var AMPLIFY_BACKEND_POLICY_ARN = "arn:aws:iam::aws:policy/AdministratorAccess-Amplify";
375
+ var TRUST_POLICY_JSON = JSON.stringify({
376
+ Version: "2012-10-17",
377
+ Statement: [
378
+ {
379
+ Effect: "Allow",
380
+ Principal: { Service: "amplify.amazonaws.com" },
381
+ Action: "sts:AssumeRole"
382
+ }
383
+ ]
384
+ });
385
+ async function tryExec(cmd, args) {
386
+ try {
387
+ const r = await execa(cmd, args, { reject: false, stdio: "pipe" });
388
+ return {
389
+ exitCode: r.exitCode ?? 0,
390
+ stdout: r.stdout?.toString() ?? "",
391
+ stderr: r.stderr?.toString() ?? ""
392
+ };
393
+ } catch (err) {
394
+ const e = err;
395
+ return {
396
+ exitCode: 1,
397
+ stdout: e.stdout ?? "",
398
+ stderr: e.stderr ?? e.message ?? String(err)
399
+ };
400
+ }
401
+ }
402
+ function awsBaseArgs(opts) {
403
+ const out = [];
404
+ if (opts.awsProfile) out.push("--profile", opts.awsProfile);
405
+ if (opts.awsRegion) out.push("--region", opts.awsRegion);
406
+ return out;
407
+ }
408
+ function awsCmd(opts, extra) {
409
+ return [...awsBaseArgs(opts), ...extra, "--output", "json"];
410
+ }
411
+ function shortName(opts) {
412
+ return basename(opts.projectDir);
413
+ }
414
+ async function checkGhInstalled(problems) {
415
+ const r = await tryExec("gh", ["--version"]);
416
+ if (r.exitCode !== 0) {
417
+ problems.push({
418
+ id: "gh-not-installed",
419
+ message: "GitHub CLI (`gh`) is not installed.",
420
+ remediation: [
421
+ "Install it (macOS: `brew install gh`; other OS: https://cli.github.com/),",
422
+ "then run `gh auth login` to sign in."
423
+ ]
424
+ });
425
+ return false;
426
+ }
427
+ return true;
428
+ }
429
+ async function checkGhAuth(problems) {
430
+ const r = await tryExec("gh", ["auth", "status"]);
431
+ const combined = `${r.stdout}
432
+ ${r.stderr}`;
433
+ if (r.exitCode !== 0 || /not logged in/i.test(combined)) {
434
+ problems.push({
435
+ id: "gh-not-authenticated",
436
+ message: "GitHub CLI is not authenticated.",
437
+ remediation: ["Run `gh auth login` and choose a method that includes the `repo` scope."]
438
+ });
439
+ return false;
440
+ }
441
+ const scopesMatch = combined.match(/Token scopes?:\s*([^\n]+)/i);
442
+ if (!scopesMatch) {
443
+ problems.push({
444
+ id: "gh-scopes-unknown",
445
+ message: "Could not determine GitHub token scopes from `gh auth status`.",
446
+ remediation: ["Run `gh auth refresh -s repo` to ensure the `repo` scope is granted."]
447
+ });
448
+ return false;
449
+ }
450
+ const hasRepo = /(^|[^a-z])repo([^a-z]|$)/i.test(scopesMatch[1]);
451
+ if (!hasRepo) {
452
+ problems.push({
453
+ id: "gh-missing-repo-scope",
454
+ message: "GitHub token is missing the `repo` scope.",
455
+ remediation: ["Run `gh auth refresh -s repo` to add the `repo` scope to your token."]
456
+ });
457
+ return false;
458
+ }
459
+ return true;
460
+ }
461
+ async function checkAwsInstalled(problems) {
462
+ const r = await tryExec("aws", ["--version"]);
463
+ if (r.exitCode !== 0) {
464
+ problems.push({
465
+ id: "aws-not-installed",
466
+ message: "AWS CLI (`aws`) is not installed.",
467
+ remediation: [
468
+ "Install it (macOS: `brew install awscli`; other OS: https://aws.amazon.com/cli/),",
469
+ "then run `aws configure` to set credentials."
470
+ ]
471
+ });
472
+ return false;
473
+ }
474
+ return true;
475
+ }
476
+ async function checkAwsCreds(opts, problems) {
477
+ const r = await tryExec("aws", [...awsBaseArgs(opts), "sts", "get-caller-identity", "--output", "json"]);
478
+ if (r.exitCode !== 0) {
479
+ problems.push({
480
+ id: "aws-credentials-missing",
481
+ message: "AWS credentials are not configured (sts get-caller-identity failed).",
482
+ remediation: [
483
+ "Run `aws configure` (or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN),",
484
+ opts.awsProfile ? `or check that profile \`${opts.awsProfile}\` exists in ~/.aws/config.` : "or pass --aws-profile <name> if you use named profiles."
485
+ ]
486
+ });
487
+ return void 0;
488
+ }
489
+ try {
490
+ const parsed = JSON.parse(r.stdout);
491
+ const account = parsed.Account;
492
+ if (!account) {
493
+ problems.push({
494
+ id: "aws-identity-malformed",
495
+ message: "aws sts get-caller-identity did not return an Account field.",
496
+ remediation: ["Re-run `aws sts get-caller-identity` manually to inspect the response."]
497
+ });
498
+ return void 0;
499
+ }
500
+ let region = opts.awsRegion;
501
+ if (!region) {
502
+ const fromEnv = process.env.AWS_REGION?.trim() || process.env.AWS_DEFAULT_REGION?.trim();
503
+ if (fromEnv) region = fromEnv;
504
+ }
505
+ if (!region) {
506
+ const cfg = await tryExec("aws", [...awsBaseArgs(opts), "configure", "get", "region"]);
507
+ const trimmed = cfg.stdout.trim();
508
+ if (trimmed) region = trimmed;
509
+ }
510
+ if (!region) {
511
+ problems.push({
512
+ id: "aws-region-missing",
513
+ message: "AWS region is not set.",
514
+ remediation: [
515
+ "Pass --aws-region <name>, set AWS_REGION env var, or run `aws configure set region <name>`."
516
+ ]
517
+ });
518
+ return void 0;
519
+ }
520
+ return { account, region };
521
+ } catch (err) {
522
+ problems.push({
523
+ id: "aws-identity-parse-failed",
524
+ message: `Could not parse aws sts get-caller-identity response: ${err instanceof Error ? err.message : String(err)}`,
525
+ remediation: ["Re-run `aws sts get-caller-identity` manually to inspect the response."]
526
+ });
527
+ return void 0;
528
+ }
529
+ }
530
+ async function checkGithubRepoFree(opts, owner, problems) {
531
+ if (!owner) return;
532
+ const name = `${owner}/${shortName(opts)}`;
533
+ const r = await tryExec("gh", ["repo", "view", name]);
534
+ if (r.exitCode === 0) {
535
+ problems.push({
536
+ id: "github-repo-exists",
537
+ message: `GitHub repo ${name} already exists.`,
538
+ remediation: [
539
+ "Pick a different project name, or delete the existing repo first:",
540
+ ` gh repo delete ${name} --yes`
541
+ ]
542
+ });
543
+ }
544
+ }
545
+ async function checkAmplifyAppNameFree(opts, problems) {
546
+ const name = shortName(opts);
547
+ const r = await tryExec(
548
+ "aws",
549
+ awsCmd(opts, [
550
+ "amplify",
551
+ "list-apps",
552
+ "--query",
553
+ `apps[?name=='${name}']`
554
+ ])
555
+ );
556
+ if (r.exitCode !== 0) {
557
+ problems.push({
558
+ id: "amplify-list-apps-failed",
559
+ message: "Could not list Amplify Hosting apps to check for a name collision.",
560
+ remediation: [
561
+ "Verify your IAM identity has `amplify:ListApps` permission, then re-run.",
562
+ `Raw error: ${r.stderr.trim() || r.stdout.trim()}`
563
+ ]
564
+ });
565
+ return;
566
+ }
567
+ try {
568
+ const arr = JSON.parse(r.stdout);
569
+ if (Array.isArray(arr) && arr.length > 0) {
570
+ problems.push({
571
+ id: "amplify-app-name-taken",
572
+ message: `Amplify Hosting already has an app named "${name}" in this region.`,
573
+ remediation: [
574
+ "Pick a different project name, or delete the existing app first:",
575
+ ` aws amplify list-apps --query "apps[?name=='${name}'].appId" --output text${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}${opts.awsRegion ? ` --region ${opts.awsRegion}` : ""}`,
576
+ " aws amplify delete-app --app-id <appId>"
577
+ ]
578
+ });
579
+ }
580
+ } catch {
581
+ }
582
+ }
583
+ async function checkCdkBootstrap(opts, identity, problems) {
584
+ const r = await tryExec(
585
+ "aws",
586
+ [
587
+ ...awsBaseArgs(opts),
588
+ "ssm",
589
+ "get-parameter",
590
+ "--name",
591
+ "/cdk-bootstrap/hnb659fds/version",
592
+ "--output",
593
+ "json"
594
+ ]
595
+ );
596
+ if (r.exitCode !== 0) {
597
+ problems.push({
598
+ id: "cdk-not-bootstrapped",
599
+ message: `CDK is not bootstrapped in region ${identity.region}.`,
600
+ remediation: [
601
+ `Run: npx cdk bootstrap aws://${identity.account}/${identity.region}`
602
+ ]
603
+ });
604
+ }
605
+ }
606
+ async function checkRoute53Zone(opts, problems) {
607
+ if (!opts.domain) return;
608
+ const registrable = extractRegistrableDomain(opts.domain);
609
+ const r = await tryExec(
610
+ "aws",
611
+ [
612
+ ...awsBaseArgs(opts),
613
+ "route53",
614
+ "list-hosted-zones-by-name",
615
+ "--dns-name",
616
+ registrable,
617
+ "--max-items",
618
+ "1",
619
+ "--output",
620
+ "json"
621
+ ]
622
+ );
623
+ if (r.exitCode !== 0) {
624
+ problems.push({
625
+ id: "route53-list-failed",
626
+ message: `Could not query Route 53 for zone ${registrable}.`,
627
+ remediation: [
628
+ "Either grant `route53:ListHostedZonesByName` to your IAM identity,",
629
+ "or be prepared to add the CNAME records Amplify prints manually after deploy.",
630
+ `Raw error: ${r.stderr.trim() || r.stdout.trim()}`
631
+ ]
632
+ });
633
+ return;
634
+ }
635
+ try {
636
+ const parsed = JSON.parse(r.stdout);
637
+ const zones = parsed.HostedZones ?? [];
638
+ const found = zones.some(
639
+ (z) => z.Name && z.Name.replace(/\.$/, "") === registrable
640
+ );
641
+ if (!found) {
642
+ problems.push({
643
+ id: "route53-zone-missing",
644
+ message: `No Route 53 hosted zone for ${registrable} found in this AWS account.`,
645
+ remediation: [
646
+ "Amplify will still issue an ACM certificate, but you will need to add",
647
+ "the verification CNAMEs at your DNS provider manually. If you want",
648
+ "auto-validation, create a hosted zone first:",
649
+ ` aws route53 create-hosted-zone --name ${registrable} --caller-reference $(date +%s)${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}`
650
+ ]
651
+ });
652
+ }
653
+ } catch {
654
+ }
655
+ }
656
+ async function resolveIamServiceRole(opts, args, problems) {
657
+ if (args.explicitArn) {
658
+ const roleName = args.explicitArn.split("/").pop() ?? args.explicitArn;
659
+ const r = await tryExec(
660
+ "aws",
661
+ [
662
+ ...awsBaseArgs(opts),
663
+ "iam",
664
+ "get-role",
665
+ "--role-name",
666
+ roleName,
667
+ "--output",
668
+ "json"
669
+ ]
670
+ );
671
+ if (r.exitCode !== 0) {
672
+ problems.push({
673
+ id: "iam-service-role-not-found",
674
+ message: `IAM role ${args.explicitArn} not found or inaccessible.`,
675
+ remediation: [
676
+ "Verify the ARN is correct and the role exists:",
677
+ ` aws iam get-role --role-name ${roleName}${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}`
678
+ ]
679
+ });
680
+ return { willCreate: false };
681
+ }
682
+ return { arn: args.explicitArn, willCreate: false };
683
+ }
684
+ if (args.willCreate) {
685
+ return { willCreate: true };
686
+ }
687
+ const found = await findExistingAmplifyServiceRole(opts);
688
+ if (found) {
689
+ return { arn: found, willCreate: false };
690
+ }
691
+ problems.push({
692
+ id: "iam-service-role-missing",
693
+ message: "No Amplify Hosting service role found.",
694
+ remediation: [
695
+ "Either pass --iam-service-role <arn> pointing at an existing role,",
696
+ "or let create-ampless build one for you with --create-iam-role.",
697
+ "",
698
+ "Alternatively, create one yourself:",
699
+ ` aws iam create-role --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} \\`,
700
+ ` --assume-role-policy-document '${TRUST_POLICY_JSON}'`,
701
+ ` aws iam attach-role-policy --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} \\`,
702
+ ` --policy-arn ${AMPLIFY_BACKEND_POLICY_ARN}`,
703
+ ` # then re-run with: --iam-service-role $(aws iam get-role --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} --query Role.Arn --output text)`
704
+ ]
705
+ });
706
+ return { willCreate: false };
707
+ }
708
+ function trustPolicyAllowsAmplify(doc) {
709
+ let parsed = doc;
710
+ if (typeof doc === "string") {
711
+ try {
712
+ parsed = JSON.parse(decodeURIComponent(doc));
713
+ } catch {
714
+ try {
715
+ parsed = JSON.parse(doc);
716
+ } catch {
717
+ return false;
718
+ }
719
+ }
720
+ }
721
+ const stmts = parsed?.Statement;
722
+ const arr = Array.isArray(stmts) ? stmts : stmts ? [stmts] : [];
723
+ for (const stmt of arr) {
724
+ const s = stmt;
725
+ const svc = s.Principal?.Service;
726
+ if (typeof svc === "string" && svc === "amplify.amazonaws.com") return true;
727
+ if (Array.isArray(svc) && svc.includes("amplify.amazonaws.com")) return true;
728
+ }
729
+ return false;
730
+ }
731
+ async function findExistingAmplifyServiceRole(opts) {
732
+ let marker;
733
+ for (let page = 0; page < 20; page++) {
734
+ const args = [
735
+ ...awsBaseArgs(opts),
736
+ "iam",
737
+ "list-roles",
738
+ "--max-items",
739
+ "200",
740
+ "--output",
741
+ "json"
742
+ ];
743
+ if (marker) args.push("--starting-token", marker);
744
+ const r = await tryExec("aws", args);
745
+ if (r.exitCode !== 0) return void 0;
746
+ let parsed;
747
+ try {
748
+ parsed = JSON.parse(r.stdout);
749
+ } catch {
750
+ return void 0;
751
+ }
752
+ for (const role of parsed.Roles ?? []) {
753
+ if (!role.RoleName || !role.Arn) continue;
754
+ if (!trustPolicyAllowsAmplify(role.AssumeRolePolicyDocument)) continue;
755
+ const pr = await tryExec(
756
+ "aws",
757
+ [
758
+ ...awsBaseArgs(opts),
759
+ "iam",
760
+ "list-attached-role-policies",
761
+ "--role-name",
762
+ role.RoleName,
763
+ "--output",
764
+ "json"
765
+ ]
766
+ );
767
+ if (pr.exitCode !== 0) continue;
768
+ try {
769
+ const policies = JSON.parse(pr.stdout);
770
+ const attached = (policies.AttachedPolicies ?? []).some(
771
+ (p3) => p3.PolicyArn === AMPLIFY_BACKEND_POLICY_ARN
772
+ );
773
+ if (attached) return role.Arn;
774
+ } catch {
775
+ continue;
776
+ }
777
+ }
778
+ if (!parsed.IsTruncated) break;
779
+ marker = parsed.Marker;
780
+ if (!marker) break;
781
+ }
782
+ return void 0;
783
+ }
784
+ async function runPreflight(opts, extra = {}) {
785
+ const problems = [];
786
+ const hasGh = await checkGhInstalled(problems);
787
+ if (hasGh) await checkGhAuth(problems);
788
+ const hasAws = await checkAwsInstalled(problems);
789
+ const identity = hasAws ? await checkAwsCreds(opts, problems) : void 0;
790
+ if (hasGh && opts.githubOwner) {
791
+ await checkGithubRepoFree(opts, opts.githubOwner, problems);
792
+ }
793
+ if (identity) {
794
+ await checkAmplifyAppNameFree(opts, problems);
795
+ await checkCdkBootstrap(opts, identity, problems);
796
+ await checkRoute53Zone(opts, problems);
797
+ }
798
+ let willCreateIamRole = false;
799
+ let iamServiceRoleArn;
800
+ if (identity) {
801
+ const resolved = await resolveIamServiceRole(
802
+ opts,
803
+ { explicitArn: extra.iamServiceRoleArn, willCreate: extra.createIamRole === true },
804
+ problems
805
+ );
806
+ willCreateIamRole = resolved.willCreate;
807
+ iamServiceRoleArn = resolved.arn;
808
+ }
809
+ if (problems.length > 0) {
810
+ return { problems };
811
+ }
812
+ return {
813
+ problems,
814
+ resolution: {
815
+ iamServiceRoleArn,
816
+ willCreateIamRole,
817
+ awsAccount: identity.account,
818
+ awsRegion: identity.region
819
+ }
820
+ };
821
+ }
822
+ function formatPreflightReport(problems) {
823
+ const lines = [];
824
+ lines.push(`${pc.red("\u2717")} create-ampless --deploy: prerequisites missing`);
825
+ lines.push("");
826
+ for (const p3 of problems) {
827
+ lines.push(` ${pc.red("\u2717")} ${p3.message}`);
828
+ for (const r of p3.remediation) {
829
+ lines.push(r ? ` ${r}` : "");
830
+ }
831
+ lines.push("");
832
+ }
833
+ lines.push("Fix the items above and re-run.");
834
+ return lines.join("\n");
835
+ }
836
+ function printPreflightReport(problems) {
837
+ process.stderr.write(`${formatPreflightReport(problems)}
838
+ `);
839
+ }
840
+
841
+ // src/deploy.ts
842
+ var AMPLIFY_BUILD_SPEC = `version: 1
843
+ frontend:
844
+ phases:
845
+ preBuild:
846
+ commands:
847
+ - npm install
848
+ build:
849
+ commands:
850
+ - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
851
+ - npm run build
852
+ artifacts:
853
+ baseDirectory: .next
854
+ files:
855
+ - '**/*'
856
+ cache:
857
+ paths:
858
+ - node_modules/**/*
859
+ - .next/cache/**/*
860
+ `;
861
+ async function resolveGithubToken(explicit, env = process.env) {
862
+ if (explicit && explicit.trim()) return explicit.trim();
863
+ const envToken = env.GITHUB_TOKEN;
864
+ if (envToken && envToken.trim()) return envToken.trim();
865
+ try {
866
+ const { stdout } = await execa2("gh", ["auth", "token"], { reject: false });
867
+ const trimmed = stdout?.trim();
868
+ if (trimmed) return trimmed;
869
+ } catch {
870
+ }
871
+ return void 0;
872
+ }
873
+ function extractRegistrableDomain(domain) {
874
+ const labels = domain.replace(/\.$/, "").split(".");
875
+ if (labels.length <= 2) return labels.join(".");
876
+ const MULTI_PART_SUFFIXES = /* @__PURE__ */ new Set([
877
+ "co.uk",
878
+ "org.uk",
879
+ "me.uk",
880
+ "gov.uk",
881
+ "ac.uk",
882
+ "co.jp",
883
+ "ne.jp",
884
+ "or.jp",
885
+ "ac.jp",
886
+ "go.jp",
887
+ "ad.jp",
888
+ "gr.jp",
889
+ "lg.jp",
890
+ "com.au",
891
+ "net.au",
892
+ "org.au",
893
+ "edu.au",
894
+ "gov.au",
895
+ "co.nz",
896
+ "net.nz",
897
+ "org.nz",
898
+ "com.br",
899
+ "com.mx",
900
+ "com.cn",
901
+ "com.sg",
902
+ "com.hk",
903
+ "com.tw",
904
+ "co.kr",
905
+ "or.kr",
906
+ "co.in",
907
+ "co.id",
908
+ "co.za"
909
+ ]);
910
+ const lastTwo = labels.slice(-2).join(".");
911
+ if (MULTI_PART_SUFFIXES.has(lastTwo) && labels.length >= 3) {
912
+ return labels.slice(-3).join(".");
913
+ }
914
+ return lastTwo;
915
+ }
916
+ function splitDomain(domain, subdomain) {
917
+ const registrable = extractRegistrableDomain(domain);
918
+ if (subdomain !== void 0) {
919
+ return { registrable, subdomain };
920
+ }
921
+ if (domain === registrable) {
922
+ return { registrable, subdomain: "" };
923
+ }
924
+ const prefix = domain.slice(0, domain.length - registrable.length - 1);
925
+ return { registrable, subdomain: prefix };
926
+ }
927
+ async function run(cmd, args, opts = {}) {
928
+ const { step, ...execaOpts } = opts;
929
+ try {
930
+ const result = await execa2(cmd, args, { stdio: "pipe", ...execaOpts });
931
+ return result.stdout?.toString() ?? "";
932
+ } catch (err) {
933
+ const e = err;
934
+ const detail = (e.stderr || e.stdout || e.shortMessage || e.message || String(err)).trim();
935
+ const prefix = step ? `${step}: ` : "";
936
+ throw new Error(`${prefix}${cmd} ${args.join(" ")}
937
+ ${detail}`);
938
+ }
939
+ }
940
+ function awsArgs(opts, extra) {
941
+ const base = [];
942
+ if (opts.awsProfile) base.push("--profile", opts.awsProfile);
943
+ if (opts.awsRegion) base.push("--region", opts.awsRegion);
944
+ base.push(...extra);
945
+ base.push("--output", "json");
946
+ return base;
947
+ }
948
+ async function gitInitCommit(dir) {
949
+ await run("git", ["init", "-b", "main"], { cwd: dir, step: "git init" });
950
+ await run("git", ["add", "."], { cwd: dir, step: "git add" });
951
+ await run(
952
+ "git",
953
+ ["commit", "-m", "Initial scaffold (create-ampless)"],
954
+ { cwd: dir, step: "git commit" }
955
+ );
956
+ }
957
+ function shortName2(opts) {
958
+ return basename2(opts.projectDir);
959
+ }
960
+ async function ghRepoCreate(opts) {
961
+ const visibility = opts.githubPrivate ? "--private" : "--public";
962
+ const name = `${opts.githubOwner}/${shortName2(opts)}`;
963
+ const out = await run(
964
+ "gh",
965
+ [
966
+ "repo",
967
+ "create",
968
+ name,
969
+ "--source",
970
+ opts.projectDir,
971
+ "--push",
972
+ visibility,
973
+ "--description",
974
+ "Created by create-ampless"
975
+ ],
976
+ {
977
+ cwd: opts.projectDir,
978
+ step: "gh repo create",
979
+ env: { ...process.env, GH_TOKEN: opts.githubToken }
980
+ }
981
+ );
982
+ const match = out.match(/https:\/\/github\.com\/[^\s]+/g);
983
+ if (match && match.length > 0) return match[match.length - 1].replace(/[.,]$/, "");
984
+ return `https://github.com/${name}`;
985
+ }
986
+ async function amplifyCreateApp(opts, repoUrl, iamServiceRoleArn) {
987
+ const cmd = [
988
+ "amplify",
989
+ "create-app",
990
+ "--name",
991
+ shortName2(opts),
992
+ "--repository",
993
+ repoUrl,
994
+ "--access-token",
995
+ opts.githubToken,
996
+ "--platform",
997
+ "WEB_COMPUTE",
998
+ "--build-spec",
999
+ AMPLIFY_BUILD_SPEC
1000
+ ];
1001
+ if (iamServiceRoleArn) {
1002
+ cmd.push("--iam-service-role-arn", iamServiceRoleArn);
1003
+ }
1004
+ const out = await run(
1005
+ "aws",
1006
+ awsArgs(opts, cmd),
1007
+ { step: "aws amplify create-app" }
1008
+ );
1009
+ const parsed = JSON.parse(out);
1010
+ const appId = parsed.app?.appId;
1011
+ if (!appId) throw new Error("aws amplify create-app: missing app.appId in response");
1012
+ return {
1013
+ appId,
1014
+ defaultDomain: parsed.app?.defaultDomain ?? "amplifyapp.com"
1015
+ };
1016
+ }
1017
+ async function amplifyCreateBranch(opts, appId) {
1018
+ await run(
1019
+ "aws",
1020
+ awsArgs(opts, [
1021
+ "amplify",
1022
+ "create-branch",
1023
+ "--app-id",
1024
+ appId,
1025
+ "--branch-name",
1026
+ "main",
1027
+ "--stage",
1028
+ "PRODUCTION"
1029
+ ]),
1030
+ { step: "aws amplify create-branch" }
1031
+ );
1032
+ }
1033
+ async function amplifyStartJob(opts, appId) {
1034
+ await run(
1035
+ "aws",
1036
+ awsArgs(opts, [
1037
+ "amplify",
1038
+ "start-job",
1039
+ "--app-id",
1040
+ appId,
1041
+ "--branch-name",
1042
+ "main",
1043
+ "--job-type",
1044
+ "RELEASE"
1045
+ ]),
1046
+ { step: "aws amplify start-job" }
1047
+ );
1048
+ }
1049
+ async function findRoute53Zone(opts, registrable) {
1050
+ try {
1051
+ const out = await run(
1052
+ "aws",
1053
+ awsArgs(opts, [
1054
+ "route53",
1055
+ "list-hosted-zones-by-name",
1056
+ "--dns-name",
1057
+ registrable,
1058
+ "--max-items",
1059
+ "1"
1060
+ ]),
1061
+ { step: "aws route53 list-hosted-zones-by-name" }
1062
+ );
1063
+ const parsed = JSON.parse(out);
1064
+ const zones = parsed.HostedZones ?? [];
1065
+ for (const z of zones) {
1066
+ if (z.Name && z.Name.replace(/\.$/, "") === registrable && z.Id) {
1067
+ return z.Id.replace("/hostedzone/", "");
1068
+ }
1069
+ }
1070
+ } catch {
1071
+ }
1072
+ return void 0;
1073
+ }
1074
+ async function amplifyCreateDomain(opts, appId) {
1075
+ const { registrable, subdomain } = splitDomain(opts.domain, opts.subdomain);
1076
+ const fullName = subdomain ? `${subdomain}.${registrable}` : registrable;
1077
+ const route53Zone = await findRoute53Zone(opts, registrable);
1078
+ if (route53Zone) {
1079
+ log.info(
1080
+ `Route 53 hosted zone detected for ${pc2.cyan(registrable)} \u2014 Amplify will auto-create DNS records once ACM validates.`
1081
+ );
1082
+ }
1083
+ const out = await run(
1084
+ "aws",
1085
+ awsArgs(opts, [
1086
+ "amplify",
1087
+ "create-domain-association",
1088
+ "--app-id",
1089
+ appId,
1090
+ "--domain-name",
1091
+ registrable,
1092
+ "--sub-domain-settings",
1093
+ `prefix=${subdomain},branchName=main`
1094
+ ]),
1095
+ { step: "aws amplify create-domain-association" }
1096
+ );
1097
+ const parsed = JSON.parse(out);
1098
+ const verification = [];
1099
+ const certRecord = parsed.domainAssociation?.certificateVerificationDNSRecord;
1100
+ if (certRecord) {
1101
+ const parts = certRecord.trim().split(/\s+/);
1102
+ if (parts.length >= 3) {
1103
+ verification.push({
1104
+ cname: parts[0].replace(/\.$/, ""),
1105
+ target: parts.slice(2).join(" ").replace(/\.$/, "")
1106
+ });
1107
+ }
1108
+ }
1109
+ for (const sd of parsed.domainAssociation?.subDomains ?? []) {
1110
+ if (!sd.dnsRecord) continue;
1111
+ const parts = sd.dnsRecord.trim().split(/\s+/);
1112
+ if (parts.length >= 3) {
1113
+ const prefix = sd.subDomainSetting?.prefix ?? "";
1114
+ const cname = prefix ? `${prefix}.${registrable}` : registrable;
1115
+ verification.push({
1116
+ cname,
1117
+ target: parts.slice(2).join(" ").replace(/\.$/, "")
1118
+ });
1119
+ }
1120
+ }
1121
+ return {
1122
+ domainUrl: `https://${fullName}`,
1123
+ verification: route53Zone ? void 0 : verification.length > 0 ? verification : void 0
1124
+ };
1125
+ }
1126
+ async function provisionIamServiceRole(opts) {
1127
+ const roleName = DEFAULT_AMPLIFY_ROLE_NAME;
1128
+ const trustPolicy = JSON.stringify({
1129
+ Version: "2012-10-17",
1130
+ Statement: [
1131
+ {
1132
+ Effect: "Allow",
1133
+ Principal: { Service: "amplify.amazonaws.com" },
1134
+ Action: "sts:AssumeRole"
1135
+ }
1136
+ ]
1137
+ });
1138
+ const createArgs = [
1139
+ "iam",
1140
+ "create-role",
1141
+ "--role-name",
1142
+ roleName,
1143
+ "--assume-role-policy-document",
1144
+ trustPolicy,
1145
+ "--description",
1146
+ "Amplify Hosting service role created by create-ampless"
1147
+ ];
1148
+ try {
1149
+ await run("aws", awsArgs(opts, createArgs), { step: "aws iam create-role" });
1150
+ } catch (err) {
1151
+ const msg = err instanceof Error ? err.message : String(err);
1152
+ if (!/EntityAlreadyExists/i.test(msg) && !/already exists/i.test(msg)) {
1153
+ throw err;
1154
+ }
1155
+ }
1156
+ await run(
1157
+ "aws",
1158
+ awsArgs(opts, [
1159
+ "iam",
1160
+ "attach-role-policy",
1161
+ "--role-name",
1162
+ roleName,
1163
+ "--policy-arn",
1164
+ AMPLIFY_BACKEND_POLICY_ARN
1165
+ ]),
1166
+ { step: "aws iam attach-role-policy" }
1167
+ );
1168
+ const out = await run(
1169
+ "aws",
1170
+ awsArgs(opts, ["iam", "get-role", "--role-name", roleName]),
1171
+ { step: "aws iam get-role" }
1172
+ );
1173
+ const parsed = JSON.parse(out);
1174
+ const arn = parsed.Role?.Arn;
1175
+ if (!arn) {
1176
+ throw new Error(`aws iam get-role for ${roleName}: missing Role.Arn in response`);
1177
+ }
1178
+ return arn;
1179
+ }
1180
+ var PreflightError = class extends Error {
1181
+ result;
1182
+ constructor(result) {
1183
+ super("create-ampless --deploy: pre-flight failed");
1184
+ this.name = "PreflightError";
1185
+ this.result = result;
1186
+ }
1187
+ };
1188
+ async function runDeploy(opts) {
1189
+ const pre = await runPreflight(opts, {
1190
+ iamServiceRoleArn: opts.iamServiceRoleArn,
1191
+ createIamRole: opts.createIamRole === true
1192
+ });
1193
+ if (pre.problems.length > 0) {
1194
+ printPreflightReport(pre.problems);
1195
+ throw new PreflightError(pre);
1196
+ }
1197
+ const resolution = pre.resolution;
1198
+ let serviceRoleArn = resolution.iamServiceRoleArn;
1199
+ if (resolution.willCreateIamRole && !serviceRoleArn) {
1200
+ const s0 = spinner();
1201
+ s0.start(`Provisioning IAM service role ${DEFAULT_AMPLIFY_ROLE_NAME}`);
1202
+ try {
1203
+ serviceRoleArn = await provisionIamServiceRole(opts);
1204
+ s0.stop(`IAM role: ${serviceRoleArn}`);
1205
+ } catch (err) {
1206
+ s0.stop("IAM role provisioning: failed");
1207
+ throw new Error(
1208
+ `Failed to provision IAM service role: ${err instanceof Error ? err.message : String(err)}`
1209
+ );
1210
+ }
1211
+ }
1212
+ await writeFile2(resolve3(opts.projectDir, "amplify.yml"), AMPLIFY_BUILD_SPEC, "utf-8");
1213
+ const created = {};
1214
+ const fail = (step, cause) => {
1215
+ const msg = cause instanceof Error ? cause.message : String(cause);
1216
+ const cleanup = [];
1217
+ if (created.appId) {
1218
+ cleanup.push(
1219
+ ` - Amplify app: ${created.appId} (delete: aws amplify delete-app --app-id ${created.appId}${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}${opts.awsRegion ? ` --region ${opts.awsRegion}` : ""})`
1220
+ );
1221
+ }
1222
+ if (created.repoUrl) {
1223
+ cleanup.push(
1224
+ ` - GitHub repo: ${created.repoUrl} (delete: gh repo delete ${opts.githubOwner}/${shortName2(opts)} --yes)`
1225
+ );
1226
+ }
1227
+ const cleanupBlock = cleanup.length > 0 ? `
1228
+ Created so far:
1229
+ ${cleanup.join("\n")}
1230
+
1231
+ Re-run after cleaning up.` : "";
1232
+ throw new Error(`Deploy failed at: ${step}
1233
+ ${msg}${cleanupBlock}`);
1234
+ };
1235
+ let s = spinner();
1236
+ s.start("git init + initial commit");
1237
+ try {
1238
+ await gitInitCommit(opts.projectDir);
1239
+ s.stop("git: committed initial scaffold");
1240
+ } catch (err) {
1241
+ s.stop("git: failed");
1242
+ fail("git init / commit", err);
1243
+ }
1244
+ s = spinner();
1245
+ s.start("Creating GitHub repo + pushing");
1246
+ try {
1247
+ created.repoUrl = await ghRepoCreate(opts);
1248
+ s.stop(`GitHub: ${created.repoUrl}`);
1249
+ } catch (err) {
1250
+ s.stop("GitHub: failed");
1251
+ fail("gh repo create", err);
1252
+ }
1253
+ let app;
1254
+ s = spinner();
1255
+ s.start("Creating Amplify Hosting app");
1256
+ try {
1257
+ app = await amplifyCreateApp(opts, created.repoUrl, serviceRoleArn);
1258
+ created.appId = app.appId;
1259
+ s.stop(`Amplify app: ${app.appId}`);
1260
+ } catch (err) {
1261
+ s.stop("Amplify create-app: failed");
1262
+ return fail("aws amplify create-app", err);
1263
+ }
1264
+ s = spinner();
1265
+ s.start("Creating main branch");
1266
+ try {
1267
+ await amplifyCreateBranch(opts, app.appId);
1268
+ s.stop("Amplify branch: main");
1269
+ } catch (err) {
1270
+ s.stop("Amplify create-branch: failed");
1271
+ fail("aws amplify create-branch", err);
1272
+ }
1273
+ s = spinner();
1274
+ s.start("Starting first deployment");
1275
+ try {
1276
+ await amplifyStartJob(opts, app.appId);
1277
+ s.stop("Amplify: first job started");
1278
+ } catch (err) {
1279
+ s.stop("Amplify start-job: failed");
1280
+ fail("aws amplify start-job", err);
1281
+ }
1282
+ const amplifyAppUrl = `https://main.${app.defaultDomain}`;
1283
+ const result = {
1284
+ githubRepoUrl: created.repoUrl,
1285
+ amplifyAppId: app.appId,
1286
+ amplifyAppUrl
1287
+ };
1288
+ if (opts.domain) {
1289
+ s = spinner();
1290
+ s.start(`Associating custom domain ${opts.domain}`);
1291
+ try {
1292
+ const dom = await amplifyCreateDomain(opts, app.appId);
1293
+ result.domainUrl = dom.domainUrl;
1294
+ result.domainVerification = dom.verification;
1295
+ s.stop(`Custom domain queued: ${opts.domain}`);
1296
+ } catch (err) {
1297
+ s.stop("Custom domain: failed");
1298
+ const msg = err instanceof Error ? err.message : String(err);
1299
+ log.warn(`Custom domain step failed:
1300
+ ${msg}`);
1301
+ }
1302
+ }
1303
+ return result;
1304
+ }
1305
+
1306
+ // src/deploy-prompts.ts
1307
+ async function detectGithubLogin() {
1308
+ try {
1309
+ const { stdout } = await execa3("gh", ["api", "user", "--jq", ".login"], { reject: false });
1310
+ const trimmed = stdout?.trim();
1311
+ return trimmed || void 0;
1312
+ } catch {
1313
+ return void 0;
1314
+ }
1315
+ }
1316
+ async function detectAwsRegion() {
1317
+ if (process.env.AWS_REGION?.trim()) return process.env.AWS_REGION.trim();
1318
+ if (process.env.AWS_DEFAULT_REGION?.trim()) return process.env.AWS_DEFAULT_REGION.trim();
1319
+ try {
1320
+ const { stdout } = await execa3("aws", ["configure", "get", "region"], { reject: false });
1321
+ const trimmed = stdout?.trim();
1322
+ return trimmed || void 0;
1323
+ } catch {
1324
+ return void 0;
1325
+ }
1326
+ }
1327
+ async function gatherDeployOptions(args, projectDir, projectName) {
1328
+ p2.log.info("Configuring deploy (GitHub + Amplify Hosting)");
1329
+ let githubOwner = args.githubOwner;
1330
+ if (!githubOwner) {
1331
+ const detected = await detectGithubLogin();
1332
+ const answer = await p2.text({
1333
+ message: "GitHub owner (user or org)",
1334
+ placeholder: detected ?? "your-github-username",
1335
+ defaultValue: detected ?? "",
1336
+ validate: (v) => !v?.trim() ? "GitHub owner is required" : void 0
1337
+ });
1338
+ if (p2.isCancel(answer)) {
1339
+ p2.cancel("Cancelled.");
1340
+ return null;
1341
+ }
1342
+ githubOwner = answer.trim();
1343
+ }
1344
+ let githubPrivate = args.githubPrivate;
1345
+ if (!args.githubPrivate) {
1346
+ const choice = await p2.select({
1347
+ message: "Repository visibility",
1348
+ options: [
1349
+ { value: "public", label: "Public" },
1350
+ { value: "private", label: "Private" }
1351
+ ],
1352
+ initialValue: "public"
1353
+ });
1354
+ if (p2.isCancel(choice)) {
1355
+ p2.cancel("Cancelled.");
1356
+ return null;
1357
+ }
1358
+ githubPrivate = choice === "private";
1359
+ }
1360
+ let githubToken = await resolveGithubToken(args.githubToken);
1361
+ if (!githubToken) {
1362
+ const answer = await p2.password({
1363
+ message: "GitHub token (needs `repo` scope) \u2014 or run `gh auth login` first",
1364
+ validate: (v) => !v?.trim() ? "Token is required" : void 0
1365
+ });
1366
+ if (p2.isCancel(answer)) {
1367
+ p2.cancel("Cancelled.");
1368
+ return null;
1369
+ }
1370
+ githubToken = answer.trim();
1371
+ }
1372
+ let awsRegion = args.awsRegion;
1373
+ if (!awsRegion) {
1374
+ const detected = await detectAwsRegion();
1375
+ const answer = await p2.text({
1376
+ message: "AWS region",
1377
+ placeholder: detected ?? "us-east-1",
1378
+ defaultValue: detected ?? "us-east-1"
1379
+ });
1380
+ if (p2.isCancel(answer)) {
1381
+ p2.cancel("Cancelled.");
1382
+ return null;
1383
+ }
1384
+ awsRegion = answer.trim() || detected || "us-east-1";
1385
+ }
1386
+ let domain = args.domain;
1387
+ let subdomain = args.subdomain;
1388
+ if (!domain) {
1389
+ const wantDomain = await p2.confirm({
1390
+ message: "Attach a custom domain?",
1391
+ initialValue: false
1392
+ });
1393
+ if (p2.isCancel(wantDomain)) {
1394
+ p2.cancel("Cancelled.");
1395
+ return null;
1396
+ }
1397
+ if (wantDomain) {
1398
+ const dom = await p2.text({
1399
+ message: "Domain (apex or subdomain, e.g. example.com or blog.example.com)",
1400
+ validate: (v) => {
1401
+ if (!v?.trim()) return "Domain is required";
1402
+ if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(v.trim())) return "Looks invalid";
1403
+ }
1404
+ });
1405
+ if (p2.isCancel(dom)) {
1406
+ p2.cancel("Cancelled.");
1407
+ return null;
1408
+ }
1409
+ domain = dom.trim();
1410
+ if (!subdomain) {
1411
+ const sub = await p2.text({
1412
+ message: "Subdomain prefix (leave blank for apex)",
1413
+ placeholder: "",
1414
+ defaultValue: ""
1415
+ });
1416
+ if (p2.isCancel(sub)) {
1417
+ p2.cancel("Cancelled.");
1418
+ return null;
1419
+ }
1420
+ subdomain = sub.trim() || void 0;
1421
+ }
1422
+ }
1423
+ }
1424
+ if (!args.skipConfirm) {
1425
+ const proceed = await p2.confirm({
1426
+ message: `Proceed?
1427
+ GitHub: ${githubOwner}/${projectName} (${githubPrivate ? "private" : "public"})
1428
+ Region: ${awsRegion}` + (domain ? `
1429
+ Domain: ${subdomain ? `${subdomain}.` : ""}${domain}` : ""),
1430
+ initialValue: true
1431
+ });
1432
+ if (p2.isCancel(proceed) || !proceed) {
1433
+ p2.cancel("Cancelled.");
1434
+ return null;
1435
+ }
1436
+ }
1437
+ return {
1438
+ projectDir,
1439
+ projectName,
1440
+ githubOwner,
1441
+ githubPrivate,
1442
+ githubToken,
1443
+ awsProfile: args.awsProfile,
1444
+ awsRegion,
1445
+ domain,
1446
+ subdomain,
1447
+ iamServiceRoleArn: args.iamServiceRole,
1448
+ createIamRole: args.createIamRole,
1449
+ skipConfirm: args.skipConfirm
1450
+ };
1451
+ }
1452
+
1453
+ // src/index.ts
1454
+ import pc3 from "picocolors";
1455
+ function buildNonInteractiveOpts(args) {
1456
+ const projectName = args.projectName ?? (() => {
1457
+ const b = basename3(process.cwd());
1458
+ return b && b !== "/" ? b : "my-ampless-site";
1459
+ })();
1460
+ const siteName = args.siteName ?? "My Blog";
1461
+ const themes = args.themes ?? ["blog"];
1462
+ const plugins = args.plugins ?? ["seo"];
1463
+ return {
1464
+ projectName,
1465
+ siteName,
1466
+ themes,
1467
+ defaultTheme: themes[0],
1468
+ plugins
1469
+ };
1470
+ }
196
1471
  async function main() {
197
- const argProjectName = process.argv[2];
198
- const opts = await runPrompts(argProjectName);
1472
+ const args = parseDeployArgs(process.argv.slice(2));
1473
+ if (args.help) {
1474
+ process.stdout.write(HELP_TEXT);
1475
+ return;
1476
+ }
1477
+ for (const flag of args.unknown) {
1478
+ log3.warn(`Unknown argument ignored: ${flag}`);
1479
+ }
1480
+ let opts;
1481
+ if (args.skipConfirm) {
1482
+ opts = buildNonInteractiveOpts(args);
1483
+ } else {
1484
+ opts = await runPrompts(args.projectName);
1485
+ }
199
1486
  if (!opts) return;
200
- const destDir = resolve3(process.cwd(), opts.projectName);
1487
+ const destDir = resolve4(process.cwd(), opts.projectName);
201
1488
  if (existsSync2(destDir)) {
202
- log.error(`Directory already exists: ${destDir}`);
1489
+ log3.error(`Directory already exists: ${destDir}`);
203
1490
  process.exit(1);
204
1491
  }
205
1492
  const sharedDir = sharedTemplateDir();
206
- const s = spinner();
1493
+ const s = spinner2();
207
1494
  s.start("Scaffolding project...");
208
1495
  try {
209
1496
  await scaffold(sharedDir, templatesDir, destDir, opts);
210
1497
  s.stop("Done!");
211
1498
  } catch (err) {
212
1499
  s.stop("Failed.");
213
- log.error(String(err));
1500
+ log3.error(String(err));
214
1501
  process.exit(1);
215
1502
  }
1503
+ if (args.deploy) {
1504
+ const deployOpts = await gatherDeployOptions(args, destDir, opts.projectName);
1505
+ if (!deployOpts) return;
1506
+ try {
1507
+ const result = await runDeploy(deployOpts);
1508
+ const lines = [
1509
+ `${pc3.green("\u2714")} Project deployed`,
1510
+ ``,
1511
+ ` GitHub: ${pc3.cyan(result.githubRepoUrl)}`,
1512
+ ` Amplify app: ${pc3.cyan(result.amplifyAppId)}`,
1513
+ ` Amplify URL: ${pc3.cyan(result.amplifyAppUrl)}`
1514
+ ];
1515
+ if (result.domainUrl) {
1516
+ lines.push(` Custom domain: ${pc3.cyan(result.domainUrl)}`);
1517
+ }
1518
+ if (result.domainVerification && result.domainVerification.length > 0) {
1519
+ lines.push("", ` ${pc3.bold("Add these DNS records to verify the domain:")}`);
1520
+ for (const v of result.domainVerification) {
1521
+ lines.push(` ${v.cname} CNAME ${v.target}`);
1522
+ }
1523
+ }
1524
+ lines.push(
1525
+ "",
1526
+ ` First build is now running in Amplify Hosting.`,
1527
+ ` Watch it at ${pc3.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
1528
+ );
1529
+ outro(lines.join("\n"));
1530
+ } catch (err) {
1531
+ if (err instanceof PreflightError) {
1532
+ process.exit(1);
1533
+ }
1534
+ log3.error(err instanceof Error ? err.message : String(err));
1535
+ process.exit(1);
1536
+ }
1537
+ return;
1538
+ }
216
1539
  outro(
217
- `${pc.green("\u2714")} Project created at ${pc.bold(opts.projectName)}
1540
+ `${pc3.green("\u2714")} Project created at ${pc3.bold(opts.projectName)}
218
1541
 
219
1542
  Next steps:
220
- ${pc.cyan("cd")} ${opts.projectName}
221
- ${pc.cyan("npm install")}
222
- ${pc.cyan("npx ampx sandbox")} ${pc.dim("# start Amplify backend")}
223
- ${pc.cyan("npm run dev")} ${pc.dim("# start Next.js")}`
1543
+ ${pc3.cyan("cd")} ${opts.projectName}
1544
+ ${pc3.cyan("npm install")}
1545
+ ${pc3.cyan("npx ampx sandbox")} ${pc3.dim("# start Amplify backend")}
1546
+ ${pc3.cyan("npm run dev")} ${pc3.dim("# start Next.js")}`
224
1547
  );
225
1548
  }
226
1549
  main().catch((err) => {