create-ampless 0.2.0-alpha.2 → 0.2.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,6 +28,42 @@ npm run dev # http://localhost:3000
28
28
 
29
29
  Sign up at `/login` — the first registered user is automatically promoted to the `ampless-admin` Cognito group.
30
30
 
31
+ ## One-shot deploy: `--deploy`
32
+
33
+ The wizard can also take you all the way from `npx` to a live Amplify Hosting URL:
34
+
35
+ ```bash
36
+ npx create-ampless@alpha my-site --deploy
37
+ ```
38
+
39
+ That extra flag, after scaffolding, runs:
40
+
41
+ 1. `git init` + initial commit
42
+ 2. Create a GitHub repo (`gh repo create`) and push
43
+ 3. `aws amplify create-app` linked to the new repo
44
+ 4. `aws amplify create-branch main`
45
+ 5. `aws amplify start-job --job-type RELEASE`
46
+ 6. Optional `aws amplify create-domain-association` if you pass `--domain`
47
+
48
+ Anything missing on the command line gets asked interactively. For CI-friendly fully-flagged usage:
49
+
50
+ ```bash
51
+ npx create-ampless@alpha my-site --deploy \
52
+ --github-owner my-org \
53
+ --github-private \
54
+ --aws-region us-east-1 \
55
+ --domain example.com --subdomain blog \
56
+ --skip-confirm
57
+ ```
58
+
59
+ If the apex domain is hosted in Route 53 in the same AWS account, Amplify will auto-create the DNS records once ACM validates. Otherwise the CLI prints the exact CNAMEs to add at your registrar.
60
+
61
+ ### Deploy requirements
62
+
63
+ - [`gh`](https://cli.github.com/) installed and authenticated (`gh auth login`)
64
+ - [`aws`](https://aws.amazon.com/cli/) installed and configured (`aws configure`)
65
+ - A GitHub token with `repo` scope (resolved via `--github-token` → `GITHUB_TOKEN` env → `gh auth token` → interactive prompt)
66
+
31
67
  ## Requirements
32
68
 
33
69
  - Node.js >= 20
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 { resolve as resolve4 } from "path";
7
7
 
8
8
  // src/prompts.ts
9
9
  import * as p from "@clack/prompts";
@@ -191,36 +191,725 @@ 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 STRING_FLAGS = /* @__PURE__ */ new Set([
196
+ "--github-owner",
197
+ "--github-token",
198
+ "--aws-profile",
199
+ "--aws-region",
200
+ "--domain",
201
+ "--subdomain"
202
+ ]);
203
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
204
+ "--deploy",
205
+ "--github-private",
206
+ "--skip-confirm",
207
+ "--help",
208
+ "-h"
209
+ ]);
210
+ function parseDeployArgs(argv) {
211
+ const out = {
212
+ deploy: false,
213
+ githubPrivate: false,
214
+ skipConfirm: false,
215
+ help: false,
216
+ unknown: []
217
+ };
218
+ for (let i = 0; i < argv.length; i++) {
219
+ const raw = argv[i];
220
+ let token = raw;
221
+ let inlineValue;
222
+ const eq = raw.indexOf("=");
223
+ if (raw.startsWith("--") && eq > 2) {
224
+ token = raw.slice(0, eq);
225
+ inlineValue = raw.slice(eq + 1);
226
+ }
227
+ if (BOOLEAN_FLAGS.has(token)) {
228
+ switch (token) {
229
+ case "--deploy":
230
+ out.deploy = true;
231
+ break;
232
+ case "--github-private":
233
+ out.githubPrivate = true;
234
+ break;
235
+ case "--skip-confirm":
236
+ out.skipConfirm = true;
237
+ break;
238
+ case "--help":
239
+ case "-h":
240
+ out.help = true;
241
+ break;
242
+ }
243
+ continue;
244
+ }
245
+ if (STRING_FLAGS.has(token)) {
246
+ const value = inlineValue ?? argv[++i];
247
+ if (value === void 0) {
248
+ throw new Error(`Missing value for ${token}`);
249
+ }
250
+ switch (token) {
251
+ case "--github-owner":
252
+ out.githubOwner = value;
253
+ break;
254
+ case "--github-token":
255
+ out.githubToken = value;
256
+ break;
257
+ case "--aws-profile":
258
+ out.awsProfile = value;
259
+ break;
260
+ case "--aws-region":
261
+ out.awsRegion = value;
262
+ break;
263
+ case "--domain":
264
+ out.domain = value;
265
+ break;
266
+ case "--subdomain":
267
+ out.subdomain = value;
268
+ break;
269
+ }
270
+ continue;
271
+ }
272
+ if (raw.startsWith("-")) {
273
+ out.unknown.push(raw);
274
+ continue;
275
+ }
276
+ if (out.projectName === void 0) {
277
+ out.projectName = raw;
278
+ } else {
279
+ out.unknown.push(raw);
280
+ }
281
+ }
282
+ return out;
283
+ }
284
+ var HELP_TEXT = `create-ampless \u2014 scaffold an ampless project
285
+
286
+ Usage:
287
+ npx create-ampless@alpha <project-name> [options]
288
+
289
+ Options:
290
+ --deploy Also create GitHub repo + Amplify Hosting app and
291
+ kick off the first deploy after scaffolding
292
+ --github-owner <login> GitHub owner (user or org). Defaults to the
293
+ authenticated 'gh' user
294
+ --github-private Create a private repo (default: public)
295
+ --github-token <token> GitHub token. Falls back to GITHUB_TOKEN env,
296
+ then 'gh auth token', then an interactive prompt
297
+ --aws-profile <profile> AWS profile name to pass to the aws CLI
298
+ --aws-region <region> AWS region (defaults to aws config / env)
299
+ --domain <name> Custom domain (apex or subdomain) to attach
300
+ --subdomain <prefix> Subdomain prefix for the domain (default: apex)
301
+ --skip-confirm Don't ask 'proceed?' before running
302
+ -h, --help Show this message
303
+ `;
304
+
305
+ // src/deploy-prompts.ts
306
+ import * as p2 from "@clack/prompts";
307
+ import { execa as execa2 } from "execa";
308
+
309
+ // src/deploy.ts
310
+ import { execa } from "execa";
311
+ import { writeFile as writeFile2 } from "fs/promises";
312
+ import { resolve as resolve3 } from "path";
313
+ import { spinner, log } from "@clack/prompts";
195
314
  import pc from "picocolors";
315
+ var AMPLIFY_BUILD_SPEC = `version: 1
316
+ frontend:
317
+ phases:
318
+ preBuild:
319
+ commands:
320
+ - npm ci
321
+ build:
322
+ commands:
323
+ - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
324
+ - npm run build
325
+ artifacts:
326
+ baseDirectory: .next
327
+ files:
328
+ - '**/*'
329
+ cache:
330
+ paths:
331
+ - node_modules/**/*
332
+ - .next/cache/**/*
333
+ `;
334
+ async function resolveGithubToken(explicit, env = process.env) {
335
+ if (explicit && explicit.trim()) return explicit.trim();
336
+ const envToken = env.GITHUB_TOKEN;
337
+ if (envToken && envToken.trim()) return envToken.trim();
338
+ try {
339
+ const { stdout } = await execa("gh", ["auth", "token"], { reject: false });
340
+ const trimmed = stdout?.trim();
341
+ if (trimmed) return trimmed;
342
+ } catch {
343
+ }
344
+ return void 0;
345
+ }
346
+ function extractRegistrableDomain(domain) {
347
+ const labels = domain.replace(/\.$/, "").split(".");
348
+ if (labels.length <= 2) return labels.join(".");
349
+ const MULTI_PART_SUFFIXES = /* @__PURE__ */ new Set([
350
+ "co.uk",
351
+ "org.uk",
352
+ "me.uk",
353
+ "gov.uk",
354
+ "ac.uk",
355
+ "co.jp",
356
+ "ne.jp",
357
+ "or.jp",
358
+ "ac.jp",
359
+ "go.jp",
360
+ "ad.jp",
361
+ "gr.jp",
362
+ "lg.jp",
363
+ "com.au",
364
+ "net.au",
365
+ "org.au",
366
+ "edu.au",
367
+ "gov.au",
368
+ "co.nz",
369
+ "net.nz",
370
+ "org.nz",
371
+ "com.br",
372
+ "com.mx",
373
+ "com.cn",
374
+ "com.sg",
375
+ "com.hk",
376
+ "com.tw",
377
+ "co.kr",
378
+ "or.kr",
379
+ "co.in",
380
+ "co.id",
381
+ "co.za"
382
+ ]);
383
+ const lastTwo = labels.slice(-2).join(".");
384
+ if (MULTI_PART_SUFFIXES.has(lastTwo) && labels.length >= 3) {
385
+ return labels.slice(-3).join(".");
386
+ }
387
+ return lastTwo;
388
+ }
389
+ function splitDomain(domain, subdomain) {
390
+ const registrable = extractRegistrableDomain(domain);
391
+ if (subdomain !== void 0) {
392
+ return { registrable, subdomain };
393
+ }
394
+ if (domain === registrable) {
395
+ return { registrable, subdomain: "" };
396
+ }
397
+ const prefix = domain.slice(0, domain.length - registrable.length - 1);
398
+ return { registrable, subdomain: prefix };
399
+ }
400
+ async function run(cmd, args, opts = {}) {
401
+ const { step, ...execaOpts } = opts;
402
+ try {
403
+ const result = await execa(cmd, args, { stdio: "pipe", ...execaOpts });
404
+ return result.stdout?.toString() ?? "";
405
+ } catch (err) {
406
+ const e = err;
407
+ const detail = (e.stderr || e.stdout || e.shortMessage || e.message || String(err)).trim();
408
+ const prefix = step ? `${step}: ` : "";
409
+ throw new Error(`${prefix}${cmd} ${args.join(" ")}
410
+ ${detail}`);
411
+ }
412
+ }
413
+ async function ensureCommandExists(cmd) {
414
+ try {
415
+ await execa(cmd, ["--version"], { stdio: "ignore" });
416
+ } catch {
417
+ const hint = cmd === "gh" ? "Install with `brew install gh` (or see https://cli.github.com/) then run `gh auth login`." : "Install with `brew install awscli` (or see https://aws.amazon.com/cli/) then run `aws configure`.";
418
+ throw new Error(`Required command not found: ${cmd}
419
+ ${hint}`);
420
+ }
421
+ }
422
+ function awsArgs(opts, extra) {
423
+ const base = [];
424
+ if (opts.awsProfile) base.push("--profile", opts.awsProfile);
425
+ if (opts.awsRegion) base.push("--region", opts.awsRegion);
426
+ base.push(...extra);
427
+ base.push("--output", "json");
428
+ return base;
429
+ }
430
+ async function gitInitCommit(dir) {
431
+ await run("git", ["init", "-b", "main"], { cwd: dir, step: "git init" });
432
+ await run("git", ["add", "."], { cwd: dir, step: "git add" });
433
+ await run(
434
+ "git",
435
+ ["commit", "-m", "Initial scaffold (create-ampless)"],
436
+ { cwd: dir, step: "git commit" }
437
+ );
438
+ }
439
+ async function ghRepoCreate(opts) {
440
+ const visibility = opts.githubPrivate ? "--private" : "--public";
441
+ const name = `${opts.githubOwner}/${opts.projectName}`;
442
+ const out = await run(
443
+ "gh",
444
+ [
445
+ "repo",
446
+ "create",
447
+ name,
448
+ "--source",
449
+ opts.projectDir,
450
+ "--push",
451
+ visibility,
452
+ "--description",
453
+ "Created by create-ampless"
454
+ ],
455
+ {
456
+ cwd: opts.projectDir,
457
+ step: "gh repo create",
458
+ env: { ...process.env, GH_TOKEN: opts.githubToken }
459
+ }
460
+ );
461
+ const match = out.match(/https:\/\/github\.com\/[^\s]+/g);
462
+ if (match && match.length > 0) return match[match.length - 1].replace(/[.,]$/, "");
463
+ return `https://github.com/${name}`;
464
+ }
465
+ async function amplifyCreateApp(opts, repoUrl) {
466
+ const out = await run(
467
+ "aws",
468
+ awsArgs(opts, [
469
+ "amplify",
470
+ "create-app",
471
+ "--name",
472
+ opts.projectName,
473
+ "--repository",
474
+ repoUrl,
475
+ "--access-token",
476
+ opts.githubToken,
477
+ "--platform",
478
+ "WEB_COMPUTE",
479
+ "--build-spec",
480
+ AMPLIFY_BUILD_SPEC
481
+ ]),
482
+ { step: "aws amplify create-app" }
483
+ );
484
+ const parsed = JSON.parse(out);
485
+ const appId = parsed.app?.appId;
486
+ if (!appId) throw new Error("aws amplify create-app: missing app.appId in response");
487
+ return {
488
+ appId,
489
+ defaultDomain: parsed.app?.defaultDomain ?? "amplifyapp.com"
490
+ };
491
+ }
492
+ async function amplifyCreateBranch(opts, appId) {
493
+ await run(
494
+ "aws",
495
+ awsArgs(opts, [
496
+ "amplify",
497
+ "create-branch",
498
+ "--app-id",
499
+ appId,
500
+ "--branch-name",
501
+ "main",
502
+ "--stage",
503
+ "PRODUCTION"
504
+ ]),
505
+ { step: "aws amplify create-branch" }
506
+ );
507
+ }
508
+ async function amplifyStartJob(opts, appId) {
509
+ await run(
510
+ "aws",
511
+ awsArgs(opts, [
512
+ "amplify",
513
+ "start-job",
514
+ "--app-id",
515
+ appId,
516
+ "--branch-name",
517
+ "main",
518
+ "--job-type",
519
+ "RELEASE"
520
+ ]),
521
+ { step: "aws amplify start-job" }
522
+ );
523
+ }
524
+ async function findRoute53Zone(opts, registrable) {
525
+ try {
526
+ const out = await run(
527
+ "aws",
528
+ awsArgs(opts, [
529
+ "route53",
530
+ "list-hosted-zones-by-name",
531
+ "--dns-name",
532
+ registrable,
533
+ "--max-items",
534
+ "1"
535
+ ]),
536
+ { step: "aws route53 list-hosted-zones-by-name" }
537
+ );
538
+ const parsed = JSON.parse(out);
539
+ const zones = parsed.HostedZones ?? [];
540
+ for (const z of zones) {
541
+ if (z.Name && z.Name.replace(/\.$/, "") === registrable && z.Id) {
542
+ return z.Id.replace("/hostedzone/", "");
543
+ }
544
+ }
545
+ } catch {
546
+ }
547
+ return void 0;
548
+ }
549
+ async function amplifyCreateDomain(opts, appId) {
550
+ const { registrable, subdomain } = splitDomain(opts.domain, opts.subdomain);
551
+ const fullName = subdomain ? `${subdomain}.${registrable}` : registrable;
552
+ const route53Zone = await findRoute53Zone(opts, registrable);
553
+ if (route53Zone) {
554
+ log.info(
555
+ `Route 53 hosted zone detected for ${pc.cyan(registrable)} \u2014 Amplify will auto-create DNS records once ACM validates.`
556
+ );
557
+ }
558
+ const out = await run(
559
+ "aws",
560
+ awsArgs(opts, [
561
+ "amplify",
562
+ "create-domain-association",
563
+ "--app-id",
564
+ appId,
565
+ "--domain-name",
566
+ registrable,
567
+ "--sub-domain-settings",
568
+ `prefix=${subdomain},branchName=main`
569
+ ]),
570
+ { step: "aws amplify create-domain-association" }
571
+ );
572
+ const parsed = JSON.parse(out);
573
+ const verification = [];
574
+ const certRecord = parsed.domainAssociation?.certificateVerificationDNSRecord;
575
+ if (certRecord) {
576
+ const parts = certRecord.trim().split(/\s+/);
577
+ if (parts.length >= 3) {
578
+ verification.push({
579
+ cname: parts[0].replace(/\.$/, ""),
580
+ target: parts.slice(2).join(" ").replace(/\.$/, "")
581
+ });
582
+ }
583
+ }
584
+ for (const sd of parsed.domainAssociation?.subDomains ?? []) {
585
+ if (!sd.dnsRecord) continue;
586
+ const parts = sd.dnsRecord.trim().split(/\s+/);
587
+ if (parts.length >= 3) {
588
+ const prefix = sd.subDomainSetting?.prefix ?? "";
589
+ const cname = prefix ? `${prefix}.${registrable}` : registrable;
590
+ verification.push({
591
+ cname,
592
+ target: parts.slice(2).join(" ").replace(/\.$/, "")
593
+ });
594
+ }
595
+ }
596
+ return {
597
+ domainUrl: `https://${fullName}`,
598
+ verification: route53Zone ? void 0 : verification.length > 0 ? verification : void 0
599
+ };
600
+ }
601
+ async function runDeploy(opts) {
602
+ await ensureCommandExists("gh");
603
+ await ensureCommandExists("aws");
604
+ await writeFile2(resolve3(opts.projectDir, "amplify.yml"), AMPLIFY_BUILD_SPEC, "utf-8");
605
+ const created = {};
606
+ const fail = (step, cause) => {
607
+ const msg = cause instanceof Error ? cause.message : String(cause);
608
+ const cleanup = [];
609
+ if (created.appId) {
610
+ cleanup.push(
611
+ ` - Amplify app: ${created.appId} (delete: aws amplify delete-app --app-id ${created.appId}${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}${opts.awsRegion ? ` --region ${opts.awsRegion}` : ""})`
612
+ );
613
+ }
614
+ if (created.repoUrl) {
615
+ cleanup.push(
616
+ ` - GitHub repo: ${created.repoUrl} (delete: gh repo delete ${opts.githubOwner}/${opts.projectName} --yes)`
617
+ );
618
+ }
619
+ const cleanupBlock = cleanup.length > 0 ? `
620
+ Created so far:
621
+ ${cleanup.join("\n")}
622
+
623
+ Re-run after cleaning up.` : "";
624
+ throw new Error(`Deploy failed at: ${step}
625
+ ${msg}${cleanupBlock}`);
626
+ };
627
+ let s = spinner();
628
+ s.start("git init + initial commit");
629
+ try {
630
+ await gitInitCommit(opts.projectDir);
631
+ s.stop("git: committed initial scaffold");
632
+ } catch (err) {
633
+ s.stop("git: failed");
634
+ fail("git init / commit", err);
635
+ }
636
+ s = spinner();
637
+ s.start("Creating GitHub repo + pushing");
638
+ try {
639
+ created.repoUrl = await ghRepoCreate(opts);
640
+ s.stop(`GitHub: ${created.repoUrl}`);
641
+ } catch (err) {
642
+ s.stop("GitHub: failed");
643
+ fail("gh repo create", err);
644
+ }
645
+ let app;
646
+ s = spinner();
647
+ s.start("Creating Amplify Hosting app");
648
+ try {
649
+ app = await amplifyCreateApp(opts, created.repoUrl);
650
+ created.appId = app.appId;
651
+ s.stop(`Amplify app: ${app.appId}`);
652
+ } catch (err) {
653
+ s.stop("Amplify create-app: failed");
654
+ return fail("aws amplify create-app", err);
655
+ }
656
+ s = spinner();
657
+ s.start("Creating main branch");
658
+ try {
659
+ await amplifyCreateBranch(opts, app.appId);
660
+ s.stop("Amplify branch: main");
661
+ } catch (err) {
662
+ s.stop("Amplify create-branch: failed");
663
+ fail("aws amplify create-branch", err);
664
+ }
665
+ s = spinner();
666
+ s.start("Starting first deployment");
667
+ try {
668
+ await amplifyStartJob(opts, app.appId);
669
+ s.stop("Amplify: first job started");
670
+ } catch (err) {
671
+ s.stop("Amplify start-job: failed");
672
+ fail("aws amplify start-job", err);
673
+ }
674
+ const amplifyAppUrl = `https://main.${app.appId}.${app.defaultDomain}`;
675
+ const result = {
676
+ githubRepoUrl: created.repoUrl,
677
+ amplifyAppId: app.appId,
678
+ amplifyAppUrl
679
+ };
680
+ if (opts.domain) {
681
+ s = spinner();
682
+ s.start(`Associating custom domain ${opts.domain}`);
683
+ try {
684
+ const dom = await amplifyCreateDomain(opts, app.appId);
685
+ result.domainUrl = dom.domainUrl;
686
+ result.domainVerification = dom.verification;
687
+ s.stop(`Custom domain queued: ${opts.domain}`);
688
+ } catch (err) {
689
+ s.stop("Custom domain: failed");
690
+ const msg = err instanceof Error ? err.message : String(err);
691
+ log.warn(`Custom domain step failed:
692
+ ${msg}`);
693
+ }
694
+ }
695
+ return result;
696
+ }
697
+
698
+ // src/deploy-prompts.ts
699
+ async function detectGithubLogin() {
700
+ try {
701
+ const { stdout } = await execa2("gh", ["api", "user", "--jq", ".login"], { reject: false });
702
+ const trimmed = stdout?.trim();
703
+ return trimmed || void 0;
704
+ } catch {
705
+ return void 0;
706
+ }
707
+ }
708
+ async function detectAwsRegion() {
709
+ if (process.env.AWS_REGION?.trim()) return process.env.AWS_REGION.trim();
710
+ if (process.env.AWS_DEFAULT_REGION?.trim()) return process.env.AWS_DEFAULT_REGION.trim();
711
+ try {
712
+ const { stdout } = await execa2("aws", ["configure", "get", "region"], { reject: false });
713
+ const trimmed = stdout?.trim();
714
+ return trimmed || void 0;
715
+ } catch {
716
+ return void 0;
717
+ }
718
+ }
719
+ async function gatherDeployOptions(args, projectDir, projectName) {
720
+ p2.log.info("Configuring deploy (GitHub + Amplify Hosting)");
721
+ let githubOwner = args.githubOwner;
722
+ if (!githubOwner) {
723
+ const detected = await detectGithubLogin();
724
+ const answer = await p2.text({
725
+ message: "GitHub owner (user or org)",
726
+ placeholder: detected ?? "your-github-username",
727
+ defaultValue: detected ?? "",
728
+ validate: (v) => !v?.trim() ? "GitHub owner is required" : void 0
729
+ });
730
+ if (p2.isCancel(answer)) {
731
+ p2.cancel("Cancelled.");
732
+ return null;
733
+ }
734
+ githubOwner = answer.trim();
735
+ }
736
+ let githubPrivate = args.githubPrivate;
737
+ if (!args.githubPrivate) {
738
+ const choice = await p2.select({
739
+ message: "Repository visibility",
740
+ options: [
741
+ { value: "public", label: "Public" },
742
+ { value: "private", label: "Private" }
743
+ ],
744
+ initialValue: "public"
745
+ });
746
+ if (p2.isCancel(choice)) {
747
+ p2.cancel("Cancelled.");
748
+ return null;
749
+ }
750
+ githubPrivate = choice === "private";
751
+ }
752
+ let githubToken = await resolveGithubToken(args.githubToken);
753
+ if (!githubToken) {
754
+ const answer = await p2.password({
755
+ message: "GitHub token (needs `repo` scope) \u2014 or run `gh auth login` first",
756
+ validate: (v) => !v?.trim() ? "Token is required" : void 0
757
+ });
758
+ if (p2.isCancel(answer)) {
759
+ p2.cancel("Cancelled.");
760
+ return null;
761
+ }
762
+ githubToken = answer.trim();
763
+ }
764
+ let awsRegion = args.awsRegion;
765
+ if (!awsRegion) {
766
+ const detected = await detectAwsRegion();
767
+ const answer = await p2.text({
768
+ message: "AWS region",
769
+ placeholder: detected ?? "us-east-1",
770
+ defaultValue: detected ?? "us-east-1"
771
+ });
772
+ if (p2.isCancel(answer)) {
773
+ p2.cancel("Cancelled.");
774
+ return null;
775
+ }
776
+ awsRegion = answer.trim() || detected || "us-east-1";
777
+ }
778
+ let domain = args.domain;
779
+ let subdomain = args.subdomain;
780
+ if (!domain) {
781
+ const wantDomain = await p2.confirm({
782
+ message: "Attach a custom domain?",
783
+ initialValue: false
784
+ });
785
+ if (p2.isCancel(wantDomain)) {
786
+ p2.cancel("Cancelled.");
787
+ return null;
788
+ }
789
+ if (wantDomain) {
790
+ const dom = await p2.text({
791
+ message: "Domain (apex or subdomain, e.g. example.com or blog.example.com)",
792
+ validate: (v) => {
793
+ if (!v?.trim()) return "Domain is required";
794
+ if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(v.trim())) return "Looks invalid";
795
+ }
796
+ });
797
+ if (p2.isCancel(dom)) {
798
+ p2.cancel("Cancelled.");
799
+ return null;
800
+ }
801
+ domain = dom.trim();
802
+ if (!subdomain) {
803
+ const sub = await p2.text({
804
+ message: "Subdomain prefix (leave blank for apex)",
805
+ placeholder: "",
806
+ defaultValue: ""
807
+ });
808
+ if (p2.isCancel(sub)) {
809
+ p2.cancel("Cancelled.");
810
+ return null;
811
+ }
812
+ subdomain = sub.trim() || void 0;
813
+ }
814
+ }
815
+ }
816
+ if (!args.skipConfirm) {
817
+ const proceed = await p2.confirm({
818
+ message: `Proceed?
819
+ GitHub: ${githubOwner}/${projectName} (${githubPrivate ? "private" : "public"})
820
+ Region: ${awsRegion}` + (domain ? `
821
+ Domain: ${subdomain ? `${subdomain}.` : ""}${domain}` : ""),
822
+ initialValue: true
823
+ });
824
+ if (p2.isCancel(proceed) || !proceed) {
825
+ p2.cancel("Cancelled.");
826
+ return null;
827
+ }
828
+ }
829
+ return {
830
+ projectDir,
831
+ projectName,
832
+ githubOwner,
833
+ githubPrivate,
834
+ githubToken,
835
+ awsProfile: args.awsProfile,
836
+ awsRegion,
837
+ domain,
838
+ subdomain,
839
+ skipConfirm: args.skipConfirm
840
+ };
841
+ }
842
+
843
+ // src/index.ts
844
+ import pc2 from "picocolors";
196
845
  async function main() {
197
- const argProjectName = process.argv[2];
198
- const opts = await runPrompts(argProjectName);
846
+ const args = parseDeployArgs(process.argv.slice(2));
847
+ if (args.help) {
848
+ process.stdout.write(HELP_TEXT);
849
+ return;
850
+ }
851
+ for (const flag of args.unknown) {
852
+ log3.warn(`Unknown argument ignored: ${flag}`);
853
+ }
854
+ const opts = await runPrompts(args.projectName);
199
855
  if (!opts) return;
200
- const destDir = resolve3(process.cwd(), opts.projectName);
856
+ const destDir = resolve4(process.cwd(), opts.projectName);
201
857
  if (existsSync2(destDir)) {
202
- log.error(`Directory already exists: ${destDir}`);
858
+ log3.error(`Directory already exists: ${destDir}`);
203
859
  process.exit(1);
204
860
  }
205
861
  const sharedDir = sharedTemplateDir();
206
- const s = spinner();
862
+ const s = spinner2();
207
863
  s.start("Scaffolding project...");
208
864
  try {
209
865
  await scaffold(sharedDir, templatesDir, destDir, opts);
210
866
  s.stop("Done!");
211
867
  } catch (err) {
212
868
  s.stop("Failed.");
213
- log.error(String(err));
869
+ log3.error(String(err));
214
870
  process.exit(1);
215
871
  }
872
+ if (args.deploy) {
873
+ const deployOpts = await gatherDeployOptions(args, destDir, opts.projectName);
874
+ if (!deployOpts) return;
875
+ try {
876
+ const result = await runDeploy(deployOpts);
877
+ const lines = [
878
+ `${pc2.green("\u2714")} Project deployed`,
879
+ ``,
880
+ ` GitHub: ${pc2.cyan(result.githubRepoUrl)}`,
881
+ ` Amplify app: ${pc2.cyan(result.amplifyAppId)}`,
882
+ ` Amplify URL: ${pc2.cyan(result.amplifyAppUrl)}`
883
+ ];
884
+ if (result.domainUrl) {
885
+ lines.push(` Custom domain: ${pc2.cyan(result.domainUrl)}`);
886
+ }
887
+ if (result.domainVerification && result.domainVerification.length > 0) {
888
+ lines.push("", ` ${pc2.bold("Add these DNS records to verify the domain:")}`);
889
+ for (const v of result.domainVerification) {
890
+ lines.push(` ${v.cname} CNAME ${v.target}`);
891
+ }
892
+ }
893
+ lines.push(
894
+ "",
895
+ ` First build is now running in Amplify Hosting.`,
896
+ ` Watch it at ${pc2.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
897
+ );
898
+ outro(lines.join("\n"));
899
+ } catch (err) {
900
+ log3.error(err instanceof Error ? err.message : String(err));
901
+ process.exit(1);
902
+ }
903
+ return;
904
+ }
216
905
  outro(
217
- `${pc.green("\u2714")} Project created at ${pc.bold(opts.projectName)}
906
+ `${pc2.green("\u2714")} Project created at ${pc2.bold(opts.projectName)}
218
907
 
219
908
  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")}`
909
+ ${pc2.cyan("cd")} ${opts.projectName}
910
+ ${pc2.cyan("npm install")}
911
+ ${pc2.cyan("npx ampx sandbox")} ${pc2.dim("# start Amplify backend")}
912
+ ${pc2.cyan("npm run dev")} ${pc2.dim("# start Next.js")}`
224
913
  );
225
914
  }
226
915
  main().catch((err) => {
@@ -0,0 +1,18 @@
1
+ version: 1
2
+ frontend:
3
+ phases:
4
+ preBuild:
5
+ commands:
6
+ - npm ci
7
+ build:
8
+ commands:
9
+ - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
10
+ - npm run build
11
+ artifacts:
12
+ baseDirectory: .next
13
+ files:
14
+ - '**/*'
15
+ cache:
16
+ paths:
17
+ - node_modules/**/*
18
+ - .next/cache/**/*
@@ -1,9 +1,20 @@
1
- // Back-compat shim. The Amplify SDK is now configured by the admin's
2
- // `<AdminProviders>` bootstrap (mounted by the layout factory in
3
- // `@ampless/admin/pages`), so most call sites no longer need to call
4
- // `configureAmplify()` themselves. Kept as a no-op so any lingering
5
- // `import '@/lib/amplify'` side-effect imports stay safe.
1
+ // Client-side Amplify SDK setup. Imported as a side-effect from
2
+ // `app/providers.tsx` so it runs on every page load (public + admin +
3
+ // login). `<AdminProviders>` (mounted by the admin layout factory)
4
+ // also performs this configuration, but it only runs for routes under
5
+ // the (admin) group; `/login` is a top-level route outside that group
6
+ // and would otherwise hit "Auth UserPool not configured" because no
7
+ // AdminProviders has bootstrapped the SDK yet.
8
+ //
9
+ // `Amplify.configure` is idempotent — calling it once at the root and
10
+ // again inside AdminProviders is harmless.
11
+
12
+ import { Amplify } from 'aws-amplify'
13
+ import outputs from '../amplify_outputs.json'
14
+
15
+ Amplify.configure(outputs, { ssr: true })
6
16
 
7
17
  export function configureAmplify() {
8
- // intentionally empty admin bootstrap handles this
18
+ // module-level side effect above already ran; keep this as a no-op
19
+ // for callers that still import it as a function for back-compat.
9
20
  }
@@ -24,9 +24,9 @@
24
24
  "@ampless/plugin-rss": "^0.2.0-alpha.1",
25
25
  "@ampless/plugin-seo": "^0.2.0-alpha.1",
26
26
  "@ampless/plugin-webhook": "^0.2.0-alpha.1",
27
- "@ampless/admin": "^0.2.0-alpha.1",
27
+ "@ampless/admin": "^0.2.0-alpha.5",
28
28
  "@ampless/backend": "^0.2.0-alpha.1",
29
- "@ampless/runtime": "^0.2.0-alpha.1",
29
+ "@ampless/runtime": "^0.2.0-alpha.2",
30
30
  "@digital-go-jp/tailwind-theme-plugin": "^0.3.4",
31
31
  "ampless": "^0.2.0-alpha.1",
32
32
  "aws-amplify": "^6.10.0",
@@ -10,7 +10,24 @@
10
10
  // forwarding, multi-site Cache-Control override.
11
11
 
12
12
  import cmsConfig from './cms.config'
13
- import { createAmplessMiddleware, defaultMatcherConfig } from '@ampless/runtime/middleware'
13
+ import { createAmplessMiddleware } from '@ampless/runtime/middleware'
14
14
 
15
15
  export const proxy = createAmplessMiddleware({ cmsConfig })
16
- export const config = defaultMatcherConfig
16
+
17
+ // Next.js 16's Turbopack requires `config` to be a statically
18
+ // analysable object literal — referencing an imported variable
19
+ // (e.g. `defaultMatcherConfig` from @ampless/runtime/middleware)
20
+ // causes a build error:
21
+ // "Next.js can't recognize the exported `config` field in route.
22
+ // It needs to be a static object."
23
+ //
24
+ // So we inline the matcher here. If you change it, keep it in sync
25
+ // with `defaultMatcherConfig` documented in @ampless/runtime/middleware.
26
+ // The matcher excludes admin / api / login / static assets /
27
+ // amplify_outputs.json so the public-site proxy doesn't rewrite
28
+ // legitimate non-blog routes.
29
+ export const config = {
30
+ matcher: [
31
+ '/((?!admin|api|login|_next/static|_next/image|favicon\\.ico|amplify_outputs\\.json).*)',
32
+ ],
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ampless",
3
- "version": "0.2.0-alpha.2",
3
+ "version": "0.2.0-alpha.5",
4
4
  "description": "Create a new ampless project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,6 +23,7 @@
23
23
  "bugs": "https://github.com/heavymoons/ampless/issues",
24
24
  "dependencies": {
25
25
  "@clack/prompts": "^1.4.0",
26
+ "execa": "^9.6.1",
26
27
  "picocolors": "^1.1.1"
27
28
  },
28
29
  "keywords": [