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

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
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { spinner as spinner2, outro, log as log3 } from "@clack/prompts";
5
- import { existsSync as existsSync2 } from "fs";
6
- import { resolve as resolve4 } from "path";
5
+ import { existsSync as existsSync4 } from "fs";
6
+ import { basename as basename3, resolve as resolve5 } from "path";
7
7
 
8
8
  // src/prompts.ts
9
9
  import * as p from "@clack/prompts";
@@ -192,17 +192,25 @@ async function scaffold(sharedDir, themesRoot, destDir, opts) {
192
192
  }
193
193
 
194
194
  // src/args.ts
195
+ var VALID_THEMES = ["blog", "minimal", "landing", "corporate", "docs", "dads"];
196
+ var VALID_PLUGINS = ["seo", "rss", "webhook"];
195
197
  var STRING_FLAGS = /* @__PURE__ */ new Set([
198
+ "--site-name",
199
+ "--themes",
200
+ "--plugins",
196
201
  "--github-owner",
197
202
  "--github-token",
198
203
  "--aws-profile",
199
204
  "--aws-region",
200
205
  "--domain",
201
- "--subdomain"
206
+ "--subdomain",
207
+ "--iam-service-role"
202
208
  ]);
203
209
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
204
210
  "--deploy",
211
+ "--mount",
205
212
  "--github-private",
213
+ "--create-iam-role",
206
214
  "--skip-confirm",
207
215
  "--help",
208
216
  "-h"
@@ -210,7 +218,9 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
210
218
  function parseDeployArgs(argv) {
211
219
  const out = {
212
220
  deploy: false,
221
+ mount: false,
213
222
  githubPrivate: false,
223
+ createIamRole: false,
214
224
  skipConfirm: false,
215
225
  help: false,
216
226
  unknown: []
@@ -229,9 +239,15 @@ function parseDeployArgs(argv) {
229
239
  case "--deploy":
230
240
  out.deploy = true;
231
241
  break;
242
+ case "--mount":
243
+ out.mount = true;
244
+ break;
232
245
  case "--github-private":
233
246
  out.githubPrivate = true;
234
247
  break;
248
+ case "--create-iam-role":
249
+ out.createIamRole = true;
250
+ break;
235
251
  case "--skip-confirm":
236
252
  out.skipConfirm = true;
237
253
  break;
@@ -248,6 +264,31 @@ function parseDeployArgs(argv) {
248
264
  throw new Error(`Missing value for ${token}`);
249
265
  }
250
266
  switch (token) {
267
+ case "--site-name":
268
+ out.siteName = value;
269
+ break;
270
+ case "--themes": {
271
+ const themes = value.split(",").map((t) => t.trim()).filter(Boolean);
272
+ const invalid = themes.filter((t) => !VALID_THEMES.includes(t));
273
+ if (invalid.length > 0) {
274
+ throw new Error(
275
+ `Invalid theme(s): ${invalid.join(", ")}. Valid values: ${VALID_THEMES.join(", ")}`
276
+ );
277
+ }
278
+ out.themes = themes;
279
+ break;
280
+ }
281
+ case "--plugins": {
282
+ const plugins = value.split(",").map((p3) => p3.trim()).filter(Boolean);
283
+ const invalid = plugins.filter((p3) => !VALID_PLUGINS.includes(p3));
284
+ if (invalid.length > 0) {
285
+ throw new Error(
286
+ `Invalid plugin(s): ${invalid.join(", ")}. Valid values: ${VALID_PLUGINS.join(", ")}`
287
+ );
288
+ }
289
+ out.plugins = plugins;
290
+ break;
291
+ }
251
292
  case "--github-owner":
252
293
  out.githubOwner = value;
253
294
  break;
@@ -266,6 +307,9 @@ function parseDeployArgs(argv) {
266
307
  case "--subdomain":
267
308
  out.subdomain = value;
268
309
  break;
310
+ case "--iam-service-role":
311
+ out.iamServiceRole = value;
312
+ break;
269
313
  }
270
314
  continue;
271
315
  }
@@ -285,10 +329,24 @@ var HELP_TEXT = `create-ampless \u2014 scaffold an ampless project
285
329
 
286
330
  Usage:
287
331
  npx create-ampless@alpha <project-name> [options]
332
+ npx create-ampless@alpha --mount [options] # in an existing project dir
288
333
 
289
334
  Options:
335
+ --site-name <name> Site display name (default: "My Blog")
336
+ --themes <list> Comma-separated theme names to install
337
+ Valid: blog, minimal, landing, corporate, docs, dads
338
+ (default: blog)
339
+ --plugins <list> Comma-separated plugin names to install
340
+ Valid: seo, rss, webhook
341
+ (default: seo)
290
342
  --deploy Also create GitHub repo + Amplify Hosting app and
291
343
  kick off the first deploy after scaffolding
344
+ --mount Skip scaffolding and mount the CURRENT directory
345
+ onto a new GitHub repo + Amplify Hosting app.
346
+ Use after you've scaffolded and tested locally
347
+ with 'npx ampx sandbox' and now want to publish.
348
+ Implies --deploy. Scaffold flags (--site-name,
349
+ --themes, --plugins) are ignored.
292
350
  --github-owner <login> GitHub owner (user or org). Defaults to the
293
351
  authenticated 'gh' user
294
352
  --github-private Create a private repo (default: public)
@@ -298,26 +356,557 @@ Options:
298
356
  --aws-region <region> AWS region (defaults to aws config / env)
299
357
  --domain <name> Custom domain (apex or subdomain) to attach
300
358
  --subdomain <prefix> Subdomain prefix for the domain (default: apex)
301
- --skip-confirm Don't ask 'proceed?' before running
359
+ --iam-service-role <arn> Existing IAM role for Amplify Hosting (must trust
360
+ amplify.amazonaws.com and have
361
+ AdministratorAccess-Amplify attached)
362
+ --create-iam-role Let create-ampless provision the Amplify Hosting
363
+ service role (idempotent; defaults to role name
364
+ AmplifyDeployBackend)
365
+ --skip-confirm Skip all interactive prompts and use defaults /
366
+ flag values (for CI / automation)
302
367
  -h, --help Show this message
303
368
  `;
304
369
 
305
370
  // src/deploy-prompts.ts
306
371
  import * as p2 from "@clack/prompts";
307
- import { execa as execa2 } from "execa";
372
+ import { execa as execa3 } from "execa";
308
373
 
309
374
  // src/deploy.ts
310
- import { execa } from "execa";
375
+ import { execa as execa2 } from "execa";
376
+ import { existsSync as existsSync3 } from "fs";
311
377
  import { writeFile as writeFile2 } from "fs/promises";
312
- import { resolve as resolve3 } from "path";
378
+ import { basename as basename2, resolve as resolve4 } from "path";
313
379
  import { spinner, log } from "@clack/prompts";
380
+ import pc2 from "picocolors";
381
+
382
+ // src/preflight.ts
383
+ import { execa } from "execa";
384
+ import { basename } from "path";
314
385
  import pc from "picocolors";
386
+ var DEFAULT_AMPLIFY_ROLE_NAME = "AmplifyDeployBackend";
387
+ var AMPLIFY_BACKEND_POLICY_ARN = "arn:aws:iam::aws:policy/AdministratorAccess-Amplify";
388
+ var TRUST_POLICY_JSON = JSON.stringify({
389
+ Version: "2012-10-17",
390
+ Statement: [
391
+ {
392
+ Effect: "Allow",
393
+ Principal: { Service: "amplify.amazonaws.com" },
394
+ Action: "sts:AssumeRole"
395
+ }
396
+ ]
397
+ });
398
+ async function tryExec(cmd, args) {
399
+ try {
400
+ const r = await execa(cmd, args, { reject: false, stdio: "pipe" });
401
+ return {
402
+ exitCode: r.exitCode ?? 0,
403
+ stdout: r.stdout?.toString() ?? "",
404
+ stderr: r.stderr?.toString() ?? ""
405
+ };
406
+ } catch (err) {
407
+ const e = err;
408
+ return {
409
+ exitCode: 1,
410
+ stdout: e.stdout ?? "",
411
+ stderr: e.stderr ?? e.message ?? String(err)
412
+ };
413
+ }
414
+ }
415
+ function awsBaseArgs(opts) {
416
+ const out = [];
417
+ if (opts.awsProfile) out.push("--profile", opts.awsProfile);
418
+ if (opts.awsRegion) out.push("--region", opts.awsRegion);
419
+ return out;
420
+ }
421
+ function awsCmd(opts, extra) {
422
+ return [...awsBaseArgs(opts), ...extra, "--output", "json"];
423
+ }
424
+ function shortName(opts) {
425
+ return basename(opts.projectDir);
426
+ }
427
+ async function checkGhInstalled(problems) {
428
+ const r = await tryExec("gh", ["--version"]);
429
+ if (r.exitCode !== 0) {
430
+ problems.push({
431
+ id: "gh-not-installed",
432
+ message: "GitHub CLI (`gh`) is not installed.",
433
+ remediation: [
434
+ "Install it (macOS: `brew install gh`; other OS: https://cli.github.com/),",
435
+ "then run `gh auth login` to sign in."
436
+ ]
437
+ });
438
+ return false;
439
+ }
440
+ return true;
441
+ }
442
+ async function checkGhAuth(problems) {
443
+ const r = await tryExec("gh", ["auth", "status"]);
444
+ const combined = `${r.stdout}
445
+ ${r.stderr}`;
446
+ if (r.exitCode !== 0 || /not logged in/i.test(combined)) {
447
+ problems.push({
448
+ id: "gh-not-authenticated",
449
+ message: "GitHub CLI is not authenticated.",
450
+ remediation: ["Run `gh auth login` and choose a method that includes the `repo` scope."]
451
+ });
452
+ return false;
453
+ }
454
+ const scopesMatch = combined.match(/Token scopes?:\s*([^\n]+)/i);
455
+ if (!scopesMatch) {
456
+ problems.push({
457
+ id: "gh-scopes-unknown",
458
+ message: "Could not determine GitHub token scopes from `gh auth status`.",
459
+ remediation: ["Run `gh auth refresh -s repo` to ensure the `repo` scope is granted."]
460
+ });
461
+ return false;
462
+ }
463
+ const hasRepo = /(^|[^a-z])repo([^a-z]|$)/i.test(scopesMatch[1]);
464
+ if (!hasRepo) {
465
+ problems.push({
466
+ id: "gh-missing-repo-scope",
467
+ message: "GitHub token is missing the `repo` scope.",
468
+ remediation: ["Run `gh auth refresh -s repo` to add the `repo` scope to your token."]
469
+ });
470
+ return false;
471
+ }
472
+ return true;
473
+ }
474
+ async function checkAwsInstalled(problems) {
475
+ const r = await tryExec("aws", ["--version"]);
476
+ if (r.exitCode !== 0) {
477
+ problems.push({
478
+ id: "aws-not-installed",
479
+ message: "AWS CLI (`aws`) is not installed.",
480
+ remediation: [
481
+ "Install it (macOS: `brew install awscli`; other OS: https://aws.amazon.com/cli/),",
482
+ "then run `aws configure` to set credentials."
483
+ ]
484
+ });
485
+ return false;
486
+ }
487
+ return true;
488
+ }
489
+ async function checkAwsCreds(opts, problems) {
490
+ const r = await tryExec("aws", [...awsBaseArgs(opts), "sts", "get-caller-identity", "--output", "json"]);
491
+ if (r.exitCode !== 0) {
492
+ problems.push({
493
+ id: "aws-credentials-missing",
494
+ message: "AWS credentials are not configured (sts get-caller-identity failed).",
495
+ remediation: [
496
+ "Run `aws configure` (or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN),",
497
+ opts.awsProfile ? `or check that profile \`${opts.awsProfile}\` exists in ~/.aws/config.` : "or pass --aws-profile <name> if you use named profiles."
498
+ ]
499
+ });
500
+ return void 0;
501
+ }
502
+ try {
503
+ const parsed = JSON.parse(r.stdout);
504
+ const account = parsed.Account;
505
+ if (!account) {
506
+ problems.push({
507
+ id: "aws-identity-malformed",
508
+ message: "aws sts get-caller-identity did not return an Account field.",
509
+ remediation: ["Re-run `aws sts get-caller-identity` manually to inspect the response."]
510
+ });
511
+ return void 0;
512
+ }
513
+ let region = opts.awsRegion;
514
+ if (!region) {
515
+ const fromEnv = process.env.AWS_REGION?.trim() || process.env.AWS_DEFAULT_REGION?.trim();
516
+ if (fromEnv) region = fromEnv;
517
+ }
518
+ if (!region) {
519
+ const cfg = await tryExec("aws", [...awsBaseArgs(opts), "configure", "get", "region"]);
520
+ const trimmed = cfg.stdout.trim();
521
+ if (trimmed) region = trimmed;
522
+ }
523
+ if (!region) {
524
+ problems.push({
525
+ id: "aws-region-missing",
526
+ message: "AWS region is not set.",
527
+ remediation: [
528
+ "Pass --aws-region <name>, set AWS_REGION env var, or run `aws configure set region <name>`."
529
+ ]
530
+ });
531
+ return void 0;
532
+ }
533
+ return { account, region };
534
+ } catch (err) {
535
+ problems.push({
536
+ id: "aws-identity-parse-failed",
537
+ message: `Could not parse aws sts get-caller-identity response: ${err instanceof Error ? err.message : String(err)}`,
538
+ remediation: ["Re-run `aws sts get-caller-identity` manually to inspect the response."]
539
+ });
540
+ return void 0;
541
+ }
542
+ }
543
+ async function checkGithubRepoFree(opts, owner, problems) {
544
+ if (!owner) return;
545
+ const name = `${owner}/${shortName(opts)}`;
546
+ const r = await tryExec("gh", ["repo", "view", name]);
547
+ if (r.exitCode === 0) {
548
+ problems.push({
549
+ id: "github-repo-exists",
550
+ message: `GitHub repo ${name} already exists.`,
551
+ remediation: [
552
+ "Pick a different project name, or delete the existing repo first:",
553
+ ` gh repo delete ${name} --yes`
554
+ ]
555
+ });
556
+ }
557
+ }
558
+ async function checkAmplifyAppNameFree(opts, problems) {
559
+ const name = shortName(opts);
560
+ const r = await tryExec(
561
+ "aws",
562
+ awsCmd(opts, [
563
+ "amplify",
564
+ "list-apps",
565
+ "--query",
566
+ `apps[?name=='${name}']`
567
+ ])
568
+ );
569
+ if (r.exitCode !== 0) {
570
+ problems.push({
571
+ id: "amplify-list-apps-failed",
572
+ message: "Could not list Amplify Hosting apps to check for a name collision.",
573
+ remediation: [
574
+ "Verify your IAM identity has `amplify:ListApps` permission, then re-run.",
575
+ `Raw error: ${r.stderr.trim() || r.stdout.trim()}`
576
+ ]
577
+ });
578
+ return;
579
+ }
580
+ try {
581
+ const arr = JSON.parse(r.stdout);
582
+ if (Array.isArray(arr) && arr.length > 0) {
583
+ problems.push({
584
+ id: "amplify-app-name-taken",
585
+ message: `Amplify Hosting already has an app named "${name}" in this region.`,
586
+ remediation: [
587
+ "Pick a different project name, or delete the existing app first:",
588
+ ` aws amplify list-apps --query "apps[?name=='${name}'].appId" --output text${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}${opts.awsRegion ? ` --region ${opts.awsRegion}` : ""}`,
589
+ " aws amplify delete-app --app-id <appId>"
590
+ ]
591
+ });
592
+ }
593
+ } catch {
594
+ }
595
+ }
596
+ async function checkCdkBootstrap(opts, identity, problems) {
597
+ const r = await tryExec(
598
+ "aws",
599
+ [
600
+ ...awsBaseArgs(opts),
601
+ "ssm",
602
+ "get-parameter",
603
+ "--name",
604
+ "/cdk-bootstrap/hnb659fds/version",
605
+ "--output",
606
+ "json"
607
+ ]
608
+ );
609
+ if (r.exitCode !== 0) {
610
+ problems.push({
611
+ id: "cdk-not-bootstrapped",
612
+ message: `CDK is not bootstrapped in region ${identity.region}.`,
613
+ remediation: [
614
+ `Run: npx cdk bootstrap aws://${identity.account}/${identity.region}`
615
+ ]
616
+ });
617
+ }
618
+ }
619
+ async function checkRoute53Zone(opts, problems) {
620
+ if (!opts.domain) return;
621
+ const registrable = extractRegistrableDomain(opts.domain);
622
+ const r = await tryExec(
623
+ "aws",
624
+ [
625
+ ...awsBaseArgs(opts),
626
+ "route53",
627
+ "list-hosted-zones-by-name",
628
+ "--dns-name",
629
+ registrable,
630
+ "--max-items",
631
+ "1",
632
+ "--output",
633
+ "json"
634
+ ]
635
+ );
636
+ if (r.exitCode !== 0) {
637
+ problems.push({
638
+ id: "route53-list-failed",
639
+ message: `Could not query Route 53 for zone ${registrable}.`,
640
+ remediation: [
641
+ "Either grant `route53:ListHostedZonesByName` to your IAM identity,",
642
+ "or be prepared to add the CNAME records Amplify prints manually after deploy.",
643
+ `Raw error: ${r.stderr.trim() || r.stdout.trim()}`
644
+ ]
645
+ });
646
+ return;
647
+ }
648
+ try {
649
+ const parsed = JSON.parse(r.stdout);
650
+ const zones = parsed.HostedZones ?? [];
651
+ const found = zones.some(
652
+ (z) => z.Name && z.Name.replace(/\.$/, "") === registrable
653
+ );
654
+ if (!found) {
655
+ problems.push({
656
+ id: "route53-zone-missing",
657
+ message: `No Route 53 hosted zone for ${registrable} found in this AWS account.`,
658
+ remediation: [
659
+ "Amplify will still issue an ACM certificate, but you will need to add",
660
+ "the verification CNAMEs at your DNS provider manually. If you want",
661
+ "auto-validation, create a hosted zone first:",
662
+ ` aws route53 create-hosted-zone --name ${registrable} --caller-reference $(date +%s)${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}`
663
+ ]
664
+ });
665
+ }
666
+ } catch {
667
+ }
668
+ }
669
+ async function resolveIamServiceRole(opts, args, problems) {
670
+ if (args.explicitArn) {
671
+ const roleName = args.explicitArn.split("/").pop() ?? args.explicitArn;
672
+ const r = await tryExec(
673
+ "aws",
674
+ [
675
+ ...awsBaseArgs(opts),
676
+ "iam",
677
+ "get-role",
678
+ "--role-name",
679
+ roleName,
680
+ "--output",
681
+ "json"
682
+ ]
683
+ );
684
+ if (r.exitCode !== 0) {
685
+ problems.push({
686
+ id: "iam-service-role-not-found",
687
+ message: `IAM role ${args.explicitArn} not found or inaccessible.`,
688
+ remediation: [
689
+ "Verify the ARN is correct and the role exists:",
690
+ ` aws iam get-role --role-name ${roleName}${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}`
691
+ ]
692
+ });
693
+ return { willCreate: false };
694
+ }
695
+ return { arn: args.explicitArn, willCreate: false };
696
+ }
697
+ if (args.willCreate) {
698
+ return { willCreate: true };
699
+ }
700
+ const found = await findExistingAmplifyServiceRole(opts);
701
+ if (found) {
702
+ return { arn: found, willCreate: false };
703
+ }
704
+ problems.push({
705
+ id: "iam-service-role-missing",
706
+ message: "No Amplify Hosting service role found.",
707
+ remediation: [
708
+ "Either pass --iam-service-role <arn> pointing at an existing role,",
709
+ "or let create-ampless build one for you with --create-iam-role.",
710
+ "",
711
+ "Alternatively, create one yourself:",
712
+ ` aws iam create-role --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} \\`,
713
+ ` --assume-role-policy-document '${TRUST_POLICY_JSON}'`,
714
+ ` aws iam attach-role-policy --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} \\`,
715
+ ` --policy-arn ${AMPLIFY_BACKEND_POLICY_ARN}`,
716
+ ` # then re-run with: --iam-service-role $(aws iam get-role --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} --query Role.Arn --output text)`
717
+ ]
718
+ });
719
+ return { willCreate: false };
720
+ }
721
+ function trustPolicyAllowsAmplify(doc) {
722
+ let parsed = doc;
723
+ if (typeof doc === "string") {
724
+ try {
725
+ parsed = JSON.parse(decodeURIComponent(doc));
726
+ } catch {
727
+ try {
728
+ parsed = JSON.parse(doc);
729
+ } catch {
730
+ return false;
731
+ }
732
+ }
733
+ }
734
+ const stmts = parsed?.Statement;
735
+ const arr = Array.isArray(stmts) ? stmts : stmts ? [stmts] : [];
736
+ for (const stmt of arr) {
737
+ const s = stmt;
738
+ const svc = s.Principal?.Service;
739
+ if (typeof svc === "string" && svc === "amplify.amazonaws.com") return true;
740
+ if (Array.isArray(svc) && svc.includes("amplify.amazonaws.com")) return true;
741
+ }
742
+ return false;
743
+ }
744
+ async function findExistingAmplifyServiceRole(opts) {
745
+ let marker;
746
+ for (let page = 0; page < 20; page++) {
747
+ const args = [
748
+ ...awsBaseArgs(opts),
749
+ "iam",
750
+ "list-roles",
751
+ "--max-items",
752
+ "200",
753
+ "--output",
754
+ "json"
755
+ ];
756
+ if (marker) args.push("--starting-token", marker);
757
+ const r = await tryExec("aws", args);
758
+ if (r.exitCode !== 0) return void 0;
759
+ let parsed;
760
+ try {
761
+ parsed = JSON.parse(r.stdout);
762
+ } catch {
763
+ return void 0;
764
+ }
765
+ for (const role of parsed.Roles ?? []) {
766
+ if (!role.RoleName || !role.Arn) continue;
767
+ if (!trustPolicyAllowsAmplify(role.AssumeRolePolicyDocument)) continue;
768
+ const pr = await tryExec(
769
+ "aws",
770
+ [
771
+ ...awsBaseArgs(opts),
772
+ "iam",
773
+ "list-attached-role-policies",
774
+ "--role-name",
775
+ role.RoleName,
776
+ "--output",
777
+ "json"
778
+ ]
779
+ );
780
+ if (pr.exitCode !== 0) continue;
781
+ try {
782
+ const policies = JSON.parse(pr.stdout);
783
+ const attached = (policies.AttachedPolicies ?? []).some(
784
+ (p3) => p3.PolicyArn === AMPLIFY_BACKEND_POLICY_ARN
785
+ );
786
+ if (attached) return role.Arn;
787
+ } catch {
788
+ continue;
789
+ }
790
+ }
791
+ if (!parsed.IsTruncated) break;
792
+ marker = parsed.Marker;
793
+ if (!marker) break;
794
+ }
795
+ return void 0;
796
+ }
797
+ async function runPreflight(opts, extra = {}) {
798
+ const problems = [];
799
+ const hasGh = await checkGhInstalled(problems);
800
+ if (hasGh) await checkGhAuth(problems);
801
+ const hasAws = await checkAwsInstalled(problems);
802
+ const identity = hasAws ? await checkAwsCreds(opts, problems) : void 0;
803
+ if (hasGh && opts.githubOwner && !extra.mountMode) {
804
+ await checkGithubRepoFree(opts, opts.githubOwner, problems);
805
+ }
806
+ if (identity) {
807
+ await checkAmplifyAppNameFree(opts, problems);
808
+ await checkCdkBootstrap(opts, identity, problems);
809
+ await checkRoute53Zone(opts, problems);
810
+ }
811
+ let willCreateIamRole = false;
812
+ let iamServiceRoleArn;
813
+ if (identity) {
814
+ const resolved = await resolveIamServiceRole(
815
+ opts,
816
+ { explicitArn: extra.iamServiceRoleArn, willCreate: extra.createIamRole === true },
817
+ problems
818
+ );
819
+ willCreateIamRole = resolved.willCreate;
820
+ iamServiceRoleArn = resolved.arn;
821
+ }
822
+ if (problems.length > 0) {
823
+ return { problems };
824
+ }
825
+ return {
826
+ problems,
827
+ resolution: {
828
+ iamServiceRoleArn,
829
+ willCreateIamRole,
830
+ awsAccount: identity.account,
831
+ awsRegion: identity.region
832
+ }
833
+ };
834
+ }
835
+ function formatPreflightReport(problems) {
836
+ const lines = [];
837
+ lines.push(`${pc.red("\u2717")} create-ampless --deploy: prerequisites missing`);
838
+ lines.push("");
839
+ for (const p3 of problems) {
840
+ lines.push(` ${pc.red("\u2717")} ${p3.message}`);
841
+ for (const r of p3.remediation) {
842
+ lines.push(r ? ` ${r}` : "");
843
+ }
844
+ lines.push("");
845
+ }
846
+ lines.push("Fix the items above and re-run.");
847
+ return lines.join("\n");
848
+ }
849
+ function printPreflightReport(problems) {
850
+ process.stderr.write(`${formatPreflightReport(problems)}
851
+ `);
852
+ }
853
+
854
+ // src/mount.ts
855
+ import { existsSync as existsSync2 } from "fs";
856
+ import { resolve as resolve3 } from "path";
857
+ function validateMountableProject(destDir) {
858
+ const required = ["package.json", "cms.config.ts"];
859
+ for (const f of required) {
860
+ if (!existsSync2(resolve3(destDir, f))) {
861
+ return `Not an ampless project (missing ${f} in ${destDir})`;
862
+ }
863
+ }
864
+ const amplifyFiles = ["amplify/backend.ts", "amplify/data/resource.ts"];
865
+ if (!amplifyFiles.some((f) => existsSync2(resolve3(destDir, f)))) {
866
+ return `Not an ampless project (missing amplify/ in ${destDir})`;
867
+ }
868
+ return null;
869
+ }
870
+ var MOUNT_DEFAULT_GITIGNORE = `# Dependencies
871
+ node_modules/
872
+
873
+ # Next.js
874
+ .next/
875
+ .amplify/
876
+
877
+ # Amplify outputs (regenerated on every deploy / sandbox)
878
+ amplify_outputs.json
879
+
880
+ # Env / OS noise
881
+ .env
882
+ .env.local
883
+ .env.*.local
884
+ .DS_Store
885
+
886
+ # Logs
887
+ npm-debug.log*
888
+ yarn-debug.log*
889
+ yarn-error.log*
890
+ `;
891
+ function originPointsAt(origin, owner, name) {
892
+ const candidates = [
893
+ `https://github.com/${owner}/${name}`,
894
+ `https://github.com/${owner}/${name}.git`,
895
+ `git@github.com:${owner}/${name}`,
896
+ `git@github.com:${owner}/${name}.git`,
897
+ `ssh://git@github.com/${owner}/${name}`,
898
+ `ssh://git@github.com/${owner}/${name}.git`
899
+ ];
900
+ return candidates.includes(origin);
901
+ }
902
+
903
+ // src/deploy.ts
315
904
  var AMPLIFY_BUILD_SPEC = `version: 1
316
905
  frontend:
317
906
  phases:
318
907
  preBuild:
319
908
  commands:
320
- - npm ci
909
+ - npm install
321
910
  build:
322
911
  commands:
323
912
  - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
@@ -336,7 +925,7 @@ async function resolveGithubToken(explicit, env = process.env) {
336
925
  const envToken = env.GITHUB_TOKEN;
337
926
  if (envToken && envToken.trim()) return envToken.trim();
338
927
  try {
339
- const { stdout } = await execa("gh", ["auth", "token"], { reject: false });
928
+ const { stdout } = await execa2("gh", ["auth", "token"], { reject: false });
340
929
  const trimmed = stdout?.trim();
341
930
  if (trimmed) return trimmed;
342
931
  } catch {
@@ -400,7 +989,7 @@ function splitDomain(domain, subdomain) {
400
989
  async function run(cmd, args, opts = {}) {
401
990
  const { step, ...execaOpts } = opts;
402
991
  try {
403
- const result = await execa(cmd, args, { stdio: "pipe", ...execaOpts });
992
+ const result = await execa2(cmd, args, { stdio: "pipe", ...execaOpts });
404
993
  return result.stdout?.toString() ?? "";
405
994
  } catch (err) {
406
995
  const e = err;
@@ -410,15 +999,6 @@ async function run(cmd, args, opts = {}) {
410
999
  ${detail}`);
411
1000
  }
412
1001
  }
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
1002
  function awsArgs(opts, extra) {
423
1003
  const base = [];
424
1004
  if (opts.awsProfile) base.push("--profile", opts.awsProfile);
@@ -427,24 +1007,108 @@ function awsArgs(opts, extra) {
427
1007
  base.push("--output", "json");
428
1008
  return base;
429
1009
  }
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
- );
1010
+ async function isGitRepo(dir) {
1011
+ const r = await execa2("git", ["rev-parse", "--git-dir"], { cwd: dir, reject: false });
1012
+ return r.exitCode === 0;
1013
+ }
1014
+ async function isDirty(dir) {
1015
+ const r = await execa2("git", ["status", "--porcelain"], { cwd: dir, reject: false });
1016
+ return (r.stdout?.toString() ?? "").trim().length > 0;
1017
+ }
1018
+ async function currentBranch(dir) {
1019
+ const r = await execa2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1020
+ cwd: dir,
1021
+ reject: false
1022
+ });
1023
+ if (r.exitCode !== 0) return null;
1024
+ const name = r.stdout?.toString().trim();
1025
+ return name && name !== "HEAD" ? name : null;
1026
+ }
1027
+ async function ensureGitignore(dir) {
1028
+ const target = resolve4(dir, ".gitignore");
1029
+ if (existsSync3(target)) return;
1030
+ await writeFile2(target, MOUNT_DEFAULT_GITIGNORE, "utf-8");
1031
+ }
1032
+ async function gitInitOrReuse(dir, mount) {
1033
+ const repo = await isGitRepo(dir);
1034
+ if (!repo) {
1035
+ await run("git", ["init", "-b", "main"], { cwd: dir, step: "git init" });
1036
+ }
1037
+ const dirty = await isDirty(dir);
1038
+ if (dirty) {
1039
+ await run("git", ["add", "."], { cwd: dir, step: "git add" });
1040
+ const message = mount && repo ? "Prepare for create-ampless --mount" : "Initial scaffold (create-ampless)";
1041
+ await run("git", ["commit", "-m", message], { cwd: dir, step: "git commit" });
1042
+ }
1043
+ if (mount) {
1044
+ const branch = await currentBranch(dir);
1045
+ if (branch && branch !== "main") {
1046
+ log.warn(
1047
+ `Current branch is "${branch}" \u2014 Amplify Hosting will be wired to the "main" branch. Push from "main" later, or rename your branch with \`git branch -m main\`.`
1048
+ );
1049
+ }
1050
+ }
1051
+ }
1052
+ function shortName2(opts) {
1053
+ return basename2(opts.projectDir);
1054
+ }
1055
+ async function ghRepoExists(name, token) {
1056
+ const r = await execa2("gh", ["repo", "view", name], {
1057
+ reject: false,
1058
+ env: { ...process.env, GH_TOKEN: token }
1059
+ });
1060
+ return r.exitCode === 0;
1061
+ }
1062
+ async function getOriginUrl(dir) {
1063
+ const r = await execa2("git", ["remote", "get-url", "origin"], {
1064
+ cwd: dir,
1065
+ reject: false
1066
+ });
1067
+ if (r.exitCode !== 0) return null;
1068
+ const url = r.stdout?.toString().trim();
1069
+ return url ? url : null;
1070
+ }
1071
+ function repoHttpsUrl(owner, name) {
1072
+ return `https://github.com/${owner}/${name}`;
438
1073
  }
439
1074
  async function ghRepoCreate(opts) {
1075
+ const owner = opts.githubOwner;
1076
+ const name = shortName2(opts);
1077
+ const fullName = `${owner}/${name}`;
440
1078
  const visibility = opts.githubPrivate ? "--private" : "--public";
441
- const name = `${opts.githubOwner}/${opts.projectName}`;
1079
+ const exists = await ghRepoExists(fullName, opts.githubToken);
1080
+ if (exists) {
1081
+ const origin = await getOriginUrl(opts.projectDir);
1082
+ if (origin && !originPointsAt(origin, owner, name)) {
1083
+ throw new Error(
1084
+ `Existing 'origin' remote points at ${origin}, but --mount expects ${fullName}.
1085
+ Remove or rename the existing remote (\`git remote remove origin\`) and re-run.`
1086
+ );
1087
+ }
1088
+ if (!origin) {
1089
+ await run(
1090
+ "git",
1091
+ ["remote", "add", "origin", `https://github.com/${fullName}.git`],
1092
+ { cwd: opts.projectDir, step: "git remote add origin" }
1093
+ );
1094
+ }
1095
+ await run(
1096
+ "git",
1097
+ ["push", "-u", "origin", "HEAD:main"],
1098
+ {
1099
+ cwd: opts.projectDir,
1100
+ step: "git push origin main",
1101
+ env: { ...process.env, GH_TOKEN: opts.githubToken }
1102
+ }
1103
+ );
1104
+ return repoHttpsUrl(owner, name);
1105
+ }
442
1106
  const out = await run(
443
1107
  "gh",
444
1108
  [
445
1109
  "repo",
446
1110
  "create",
447
- name,
1111
+ fullName,
448
1112
  "--source",
449
1113
  opts.projectDir,
450
1114
  "--push",
@@ -460,25 +1124,29 @@ async function ghRepoCreate(opts) {
460
1124
  );
461
1125
  const match = out.match(/https:\/\/github\.com\/[^\s]+/g);
462
1126
  if (match && match.length > 0) return match[match.length - 1].replace(/[.,]$/, "");
463
- return `https://github.com/${name}`;
1127
+ return repoHttpsUrl(owner, name);
464
1128
  }
465
- async function amplifyCreateApp(opts, repoUrl) {
1129
+ async function amplifyCreateApp(opts, repoUrl, iamServiceRoleArn) {
1130
+ const cmd = [
1131
+ "amplify",
1132
+ "create-app",
1133
+ "--name",
1134
+ shortName2(opts),
1135
+ "--repository",
1136
+ repoUrl,
1137
+ "--access-token",
1138
+ opts.githubToken,
1139
+ "--platform",
1140
+ "WEB_COMPUTE",
1141
+ "--build-spec",
1142
+ AMPLIFY_BUILD_SPEC
1143
+ ];
1144
+ if (iamServiceRoleArn) {
1145
+ cmd.push("--iam-service-role-arn", iamServiceRoleArn);
1146
+ }
466
1147
  const out = await run(
467
1148
  "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
- ]),
1149
+ awsArgs(opts, cmd),
482
1150
  { step: "aws amplify create-app" }
483
1151
  );
484
1152
  const parsed = JSON.parse(out);
@@ -552,7 +1220,7 @@ async function amplifyCreateDomain(opts, appId) {
552
1220
  const route53Zone = await findRoute53Zone(opts, registrable);
553
1221
  if (route53Zone) {
554
1222
  log.info(
555
- `Route 53 hosted zone detected for ${pc.cyan(registrable)} \u2014 Amplify will auto-create DNS records once ACM validates.`
1223
+ `Route 53 hosted zone detected for ${pc2.cyan(registrable)} \u2014 Amplify will auto-create DNS records once ACM validates.`
556
1224
  );
557
1225
  }
558
1226
  const out = await run(
@@ -598,10 +1266,94 @@ async function amplifyCreateDomain(opts, appId) {
598
1266
  verification: route53Zone ? void 0 : verification.length > 0 ? verification : void 0
599
1267
  };
600
1268
  }
1269
+ async function provisionIamServiceRole(opts) {
1270
+ const roleName = DEFAULT_AMPLIFY_ROLE_NAME;
1271
+ const trustPolicy = JSON.stringify({
1272
+ Version: "2012-10-17",
1273
+ Statement: [
1274
+ {
1275
+ Effect: "Allow",
1276
+ Principal: { Service: "amplify.amazonaws.com" },
1277
+ Action: "sts:AssumeRole"
1278
+ }
1279
+ ]
1280
+ });
1281
+ const createArgs = [
1282
+ "iam",
1283
+ "create-role",
1284
+ "--role-name",
1285
+ roleName,
1286
+ "--assume-role-policy-document",
1287
+ trustPolicy,
1288
+ "--description",
1289
+ "Amplify Hosting service role created by create-ampless"
1290
+ ];
1291
+ try {
1292
+ await run("aws", awsArgs(opts, createArgs), { step: "aws iam create-role" });
1293
+ } catch (err) {
1294
+ const msg = err instanceof Error ? err.message : String(err);
1295
+ if (!/EntityAlreadyExists/i.test(msg) && !/already exists/i.test(msg)) {
1296
+ throw err;
1297
+ }
1298
+ }
1299
+ await run(
1300
+ "aws",
1301
+ awsArgs(opts, [
1302
+ "iam",
1303
+ "attach-role-policy",
1304
+ "--role-name",
1305
+ roleName,
1306
+ "--policy-arn",
1307
+ AMPLIFY_BACKEND_POLICY_ARN
1308
+ ]),
1309
+ { step: "aws iam attach-role-policy" }
1310
+ );
1311
+ const out = await run(
1312
+ "aws",
1313
+ awsArgs(opts, ["iam", "get-role", "--role-name", roleName]),
1314
+ { step: "aws iam get-role" }
1315
+ );
1316
+ const parsed = JSON.parse(out);
1317
+ const arn = parsed.Role?.Arn;
1318
+ if (!arn) {
1319
+ throw new Error(`aws iam get-role for ${roleName}: missing Role.Arn in response`);
1320
+ }
1321
+ return arn;
1322
+ }
1323
+ var PreflightError = class extends Error {
1324
+ result;
1325
+ constructor(result) {
1326
+ super("create-ampless --deploy: pre-flight failed");
1327
+ this.name = "PreflightError";
1328
+ this.result = result;
1329
+ }
1330
+ };
601
1331
  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");
1332
+ const pre = await runPreflight(opts, {
1333
+ iamServiceRoleArn: opts.iamServiceRoleArn,
1334
+ createIamRole: opts.createIamRole === true,
1335
+ mountMode: opts.mount === true
1336
+ });
1337
+ if (pre.problems.length > 0) {
1338
+ printPreflightReport(pre.problems);
1339
+ throw new PreflightError(pre);
1340
+ }
1341
+ const resolution = pre.resolution;
1342
+ let serviceRoleArn = resolution.iamServiceRoleArn;
1343
+ if (resolution.willCreateIamRole && !serviceRoleArn) {
1344
+ const s0 = spinner();
1345
+ s0.start(`Provisioning IAM service role ${DEFAULT_AMPLIFY_ROLE_NAME}`);
1346
+ try {
1347
+ serviceRoleArn = await provisionIamServiceRole(opts);
1348
+ s0.stop(`IAM role: ${serviceRoleArn}`);
1349
+ } catch (err) {
1350
+ s0.stop("IAM role provisioning: failed");
1351
+ throw new Error(
1352
+ `Failed to provision IAM service role: ${err instanceof Error ? err.message : String(err)}`
1353
+ );
1354
+ }
1355
+ }
1356
+ await writeFile2(resolve4(opts.projectDir, "amplify.yml"), AMPLIFY_BUILD_SPEC, "utf-8");
605
1357
  const created = {};
606
1358
  const fail = (step, cause) => {
607
1359
  const msg = cause instanceof Error ? cause.message : String(cause);
@@ -613,7 +1365,7 @@ async function runDeploy(opts) {
613
1365
  }
614
1366
  if (created.repoUrl) {
615
1367
  cleanup.push(
616
- ` - GitHub repo: ${created.repoUrl} (delete: gh repo delete ${opts.githubOwner}/${opts.projectName} --yes)`
1368
+ ` - GitHub repo: ${created.repoUrl} (delete: gh repo delete ${opts.githubOwner}/${shortName2(opts)} --yes)`
617
1369
  );
618
1370
  }
619
1371
  const cleanupBlock = cleanup.length > 0 ? `
@@ -625,10 +1377,14 @@ Re-run after cleaning up.` : "";
625
1377
  ${msg}${cleanupBlock}`);
626
1378
  };
627
1379
  let s = spinner();
628
- s.start("git init + initial commit");
1380
+ const gitMsg = opts.mount ? "git: preparing repo for mount" : "git init + initial commit";
1381
+ s.start(gitMsg);
629
1382
  try {
630
- await gitInitCommit(opts.projectDir);
631
- s.stop("git: committed initial scaffold");
1383
+ if (opts.mount) {
1384
+ await ensureGitignore(opts.projectDir);
1385
+ }
1386
+ await gitInitOrReuse(opts.projectDir, opts.mount === true);
1387
+ s.stop(opts.mount ? "git: ready" : "git: committed initial scaffold");
632
1388
  } catch (err) {
633
1389
  s.stop("git: failed");
634
1390
  fail("git init / commit", err);
@@ -646,7 +1402,7 @@ ${msg}${cleanupBlock}`);
646
1402
  s = spinner();
647
1403
  s.start("Creating Amplify Hosting app");
648
1404
  try {
649
- app = await amplifyCreateApp(opts, created.repoUrl);
1405
+ app = await amplifyCreateApp(opts, created.repoUrl, serviceRoleArn);
650
1406
  created.appId = app.appId;
651
1407
  s.stop(`Amplify app: ${app.appId}`);
652
1408
  } catch (err) {
@@ -671,7 +1427,7 @@ ${msg}${cleanupBlock}`);
671
1427
  s.stop("Amplify start-job: failed");
672
1428
  fail("aws amplify start-job", err);
673
1429
  }
674
- const amplifyAppUrl = `https://main.${app.appId}.${app.defaultDomain}`;
1430
+ const amplifyAppUrl = `https://main.${app.defaultDomain}`;
675
1431
  const result = {
676
1432
  githubRepoUrl: created.repoUrl,
677
1433
  amplifyAppId: app.appId,
@@ -698,7 +1454,7 @@ ${msg}`);
698
1454
  // src/deploy-prompts.ts
699
1455
  async function detectGithubLogin() {
700
1456
  try {
701
- const { stdout } = await execa2("gh", ["api", "user", "--jq", ".login"], { reject: false });
1457
+ const { stdout } = await execa3("gh", ["api", "user", "--jq", ".login"], { reject: false });
702
1458
  const trimmed = stdout?.trim();
703
1459
  return trimmed || void 0;
704
1460
  } catch {
@@ -709,7 +1465,7 @@ async function detectAwsRegion() {
709
1465
  if (process.env.AWS_REGION?.trim()) return process.env.AWS_REGION.trim();
710
1466
  if (process.env.AWS_DEFAULT_REGION?.trim()) return process.env.AWS_DEFAULT_REGION.trim();
711
1467
  try {
712
- const { stdout } = await execa2("aws", ["configure", "get", "region"], { reject: false });
1468
+ const { stdout } = await execa3("aws", ["configure", "get", "region"], { reject: false });
713
1469
  const trimmed = stdout?.trim();
714
1470
  return trimmed || void 0;
715
1471
  } catch {
@@ -836,12 +1592,89 @@ async function gatherDeployOptions(args, projectDir, projectName) {
836
1592
  awsRegion,
837
1593
  domain,
838
1594
  subdomain,
1595
+ iamServiceRoleArn: args.iamServiceRole,
1596
+ createIamRole: args.createIamRole,
839
1597
  skipConfirm: args.skipConfirm
840
1598
  };
841
1599
  }
842
1600
 
843
1601
  // src/index.ts
844
- import pc2 from "picocolors";
1602
+ import pc3 from "picocolors";
1603
+ function buildNonInteractiveOpts(args) {
1604
+ const projectName = args.projectName ?? (() => {
1605
+ const b = basename3(process.cwd());
1606
+ return b && b !== "/" ? b : "my-ampless-site";
1607
+ })();
1608
+ const siteName = args.siteName ?? "My Blog";
1609
+ const themes = args.themes ?? ["blog"];
1610
+ const plugins = args.plugins ?? ["seo"];
1611
+ return {
1612
+ projectName,
1613
+ siteName,
1614
+ themes,
1615
+ defaultTheme: themes[0],
1616
+ plugins
1617
+ };
1618
+ }
1619
+ function warnIgnoredScaffoldFlags(args) {
1620
+ const ignored = [];
1621
+ if (args.siteName) ignored.push("--site-name");
1622
+ if (args.themes) ignored.push("--themes");
1623
+ if (args.plugins) ignored.push("--plugins");
1624
+ if (args.projectName) ignored.push("<project-name> positional");
1625
+ if (ignored.length > 0) {
1626
+ log3.warn(`--mount mode ignores: ${ignored.join(", ")}`);
1627
+ }
1628
+ }
1629
+ async function runMount(args) {
1630
+ const destDir = process.cwd();
1631
+ const projectName = basename3(destDir);
1632
+ warnIgnoredScaffoldFlags(args);
1633
+ const problem = validateMountableProject(destDir);
1634
+ if (problem) {
1635
+ log3.error(problem);
1636
+ log3.info(
1637
+ "Run `npx create-ampless@alpha <name>` first to scaffold, or `cd` into a scaffolded project before passing --mount."
1638
+ );
1639
+ process.exit(1);
1640
+ }
1641
+ const deployOpts = await gatherDeployOptions(args, destDir, projectName);
1642
+ if (!deployOpts) return;
1643
+ try {
1644
+ const result = await runDeploy({ ...deployOpts, mount: true });
1645
+ printDeployResult(result);
1646
+ } catch (err) {
1647
+ if (err instanceof PreflightError) {
1648
+ process.exit(1);
1649
+ }
1650
+ log3.error(err instanceof Error ? err.message : String(err));
1651
+ process.exit(1);
1652
+ }
1653
+ }
1654
+ function printDeployResult(result) {
1655
+ const lines = [
1656
+ `${pc3.green("\u2714")} Project deployed`,
1657
+ ``,
1658
+ ` GitHub: ${pc3.cyan(result.githubRepoUrl)}`,
1659
+ ` Amplify app: ${pc3.cyan(result.amplifyAppId)}`,
1660
+ ` Amplify URL: ${pc3.cyan(result.amplifyAppUrl)}`
1661
+ ];
1662
+ if (result.domainUrl) {
1663
+ lines.push(` Custom domain: ${pc3.cyan(result.domainUrl)}`);
1664
+ }
1665
+ if (result.domainVerification && result.domainVerification.length > 0) {
1666
+ lines.push("", ` ${pc3.bold("Add these DNS records to verify the domain:")}`);
1667
+ for (const v of result.domainVerification) {
1668
+ lines.push(` ${v.cname} CNAME ${v.target}`);
1669
+ }
1670
+ }
1671
+ lines.push(
1672
+ "",
1673
+ ` First build is now running in Amplify Hosting.`,
1674
+ ` Watch it at ${pc3.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
1675
+ );
1676
+ outro(lines.join("\n"));
1677
+ }
845
1678
  async function main() {
846
1679
  const args = parseDeployArgs(process.argv.slice(2));
847
1680
  if (args.help) {
@@ -851,10 +1684,19 @@ async function main() {
851
1684
  for (const flag of args.unknown) {
852
1685
  log3.warn(`Unknown argument ignored: ${flag}`);
853
1686
  }
854
- const opts = await runPrompts(args.projectName);
1687
+ if (args.mount) {
1688
+ await runMount(args);
1689
+ return;
1690
+ }
1691
+ let opts;
1692
+ if (args.skipConfirm) {
1693
+ opts = buildNonInteractiveOpts(args);
1694
+ } else {
1695
+ opts = await runPrompts(args.projectName);
1696
+ }
855
1697
  if (!opts) return;
856
- const destDir = resolve4(process.cwd(), opts.projectName);
857
- if (existsSync2(destDir)) {
1698
+ const destDir = resolve5(process.cwd(), opts.projectName);
1699
+ if (existsSync4(destDir)) {
858
1700
  log3.error(`Directory already exists: ${destDir}`);
859
1701
  process.exit(1);
860
1702
  }
@@ -874,42 +1716,24 @@ async function main() {
874
1716
  if (!deployOpts) return;
875
1717
  try {
876
1718
  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"));
1719
+ printDeployResult(result);
899
1720
  } catch (err) {
1721
+ if (err instanceof PreflightError) {
1722
+ process.exit(1);
1723
+ }
900
1724
  log3.error(err instanceof Error ? err.message : String(err));
901
1725
  process.exit(1);
902
1726
  }
903
1727
  return;
904
1728
  }
905
1729
  outro(
906
- `${pc2.green("\u2714")} Project created at ${pc2.bold(opts.projectName)}
1730
+ `${pc3.green("\u2714")} Project created at ${pc3.bold(opts.projectName)}
907
1731
 
908
1732
  Next steps:
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")}`
1733
+ ${pc3.cyan("cd")} ${opts.projectName}
1734
+ ${pc3.cyan("npm install")}
1735
+ ${pc3.cyan("npx ampx sandbox")} ${pc3.dim("# start Amplify backend")}
1736
+ ${pc3.cyan("npm run dev")} ${pc3.dim("# start Next.js")}`
913
1737
  );
914
1738
  }
915
1739
  main().catch((err) => {
@@ -1,6 +1,17 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import { dirname, resolve } from 'node:path'
1
3
  import { a, defineData, type ClientSchema } from '@aws-amplify/backend'
2
4
  import { amplessSchemaModels, defaultAuthorizationModes } from '@ampless/backend'
3
5
 
6
+ // AppSync's `a.handler.custom({ entry })` paths are resolved by CDK
7
+ // relative to the file that called `a.handler.custom`. When the call
8
+ // originates inside `@ampless/backend` (via amplessSchemaModels), the
9
+ // CDK synth tries to resolve `./list-published-posts.js` relative to
10
+ // `node_modules/@ampless/backend/dist/index.js` and fails with
11
+ // `UnresolvedEntryPathError`. Anchor the resolver paths at THIS file's
12
+ // directory so the result is unambiguous.
13
+ const __dirname = dirname(fileURLToPath(import.meta.url))
14
+
4
15
  // Ampless's built-in models (Post / Page / Media / Taxonomy / PostTag /
5
16
  // KvStore) plus the three public-read custom queries
6
17
  // (listPublishedPosts / getPublishedPost / listPostsByTag).
@@ -8,19 +19,20 @@ import { amplessSchemaModels, defaultAuthorizationModes } from '@ampless/backend
8
19
  // Add project-specific models alongside the spread:
9
20
  //
10
21
  // const schema = a.schema({
11
- // ...amplessSchemaModels(a),
22
+ // ...amplessSchemaModels(a, { resolverPaths }),
12
23
  // MyCustomModel: a
13
24
  // .model({ siteId: a.string().required(), foo: a.string() })
14
25
  // .identifier(['siteId', 'foo'])
15
26
  // .authorization((allow) => [allow.groups(['ampless-admin'])]),
16
27
  // })
17
- //
18
- // The three AppSync JS resolvers (`*.js` in this directory) stay
19
- // user-owned — AppSync resolves `entry: './...'` paths at synth time
20
- // relative to this file. If you relocate them, pass new paths via
21
- // `amplessSchemaModels(a, { resolverPaths: { ... } })`.
28
+ const resolverPaths = {
29
+ listPublishedPosts: resolve(__dirname, 'list-published-posts.js'),
30
+ getPublishedPost: resolve(__dirname, 'get-published-post.js'),
31
+ listPostsByTag: resolve(__dirname, 'list-posts-by-tag.js'),
32
+ }
33
+
22
34
  const schema = a.schema({
23
- ...amplessSchemaModels(a),
35
+ ...amplessSchemaModels(a, { resolverPaths }),
24
36
  })
25
37
 
26
38
  export type Schema = ClientSchema<typeof schema>
@@ -3,7 +3,7 @@ frontend:
3
3
  phases:
4
4
  preBuild:
5
5
  commands:
6
- - npm ci
6
+ - npm install
7
7
  build:
8
8
  commands:
9
9
  - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
@@ -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.5",
28
- "@ampless/backend": "^0.2.0-alpha.1",
29
- "@ampless/runtime": "^0.2.0-alpha.2",
27
+ "@ampless/admin": "^0.2.0-alpha.6",
28
+ "@ampless/backend": "^0.2.0-alpha.2",
29
+ "@ampless/runtime": "^0.2.0-alpha.3",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ampless",
3
- "version": "0.2.0-alpha.5",
3
+ "version": "0.2.0-alpha.7",
4
4
  "description": "Create a new ampless project",
5
5
  "license": "MIT",
6
6
  "type": "module",