create-ampless 0.2.0-alpha.0 → 0.2.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +36 -0
  2. package/dist/index.js +2098 -48
  3. package/dist/templates/_shared/amplify/auth/resource.ts +7 -2
  4. package/dist/templates/_shared/amplify/backend.custom.ts +41 -0
  5. package/dist/templates/_shared/amplify/backend.ts +10 -2
  6. package/dist/templates/_shared/amplify/data/resource.custom.ts +33 -0
  7. package/dist/templates/_shared/amplify/data/resource.ts +23 -15
  8. package/dist/templates/_shared/amplify/functions/user-admin/handler.ts +4 -0
  9. package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +6 -0
  10. package/dist/templates/_shared/amplify/storage/resource.ts +7 -2
  11. package/dist/templates/_shared/amplify.yml +18 -0
  12. package/dist/templates/_shared/app/(admin)/admin/users/page.tsx +5 -0
  13. package/dist/templates/_shared/app/site/[siteId]/[...path]/route.ts +17 -0
  14. package/dist/templates/_shared/components/i18n-provider.tsx +8 -0
  15. package/dist/templates/_shared/lib/admin-site.ts +6 -2
  16. package/dist/templates/_shared/lib/admin.ts +10 -4
  17. package/dist/templates/_shared/lib/amplify.ts +17 -6
  18. package/dist/templates/_shared/lib/auth-server.ts +7 -3
  19. package/dist/templates/_shared/lib/i18n.ts +5 -1
  20. package/dist/templates/_shared/lib/posts-public.ts +11 -3
  21. package/dist/templates/_shared/lib/seo.ts +5 -2
  22. package/dist/templates/_shared/lib/site-settings.ts +4 -1
  23. package/dist/templates/_shared/lib/storage.ts +5 -2
  24. package/dist/templates/_shared/lib/theme-active.ts +3 -1
  25. package/dist/templates/_shared/lib/theme-config.ts +3 -1
  26. package/dist/templates/_shared/package.json +22 -19
  27. package/dist/templates/_shared/proxy.ts +33 -0
  28. package/package.json +3 -2
  29. package/dist/templates/_shared/middleware.ts +0 -13
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";
5
- import { existsSync as existsSync2 } from "fs";
6
- import { resolve as resolve3 } from "path";
4
+ import { spinner as spinner2, outro as outro3, log as log5 } from "@clack/prompts";
5
+ import { existsSync as existsSync7 } 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";
@@ -16,7 +16,7 @@ async function runPrompts(argProjectName) {
16
16
  placeholder: "my-ampless-site",
17
17
  defaultValue: argProjectName ?? "my-ampless-site",
18
18
  validate: (v) => {
19
- if (!v.trim()) return "Project name is required";
19
+ if (!v || !v.trim()) return "Project name is required";
20
20
  if (!/^[a-z0-9-_]+$/.test(v)) return "Use lowercase letters, numbers, hyphens, underscores";
21
21
  }
22
22
  }),
@@ -28,7 +28,10 @@ async function runPrompts(argProjectName) {
28
28
  // Multiple themes can ship side-by-side. The first selected is the
29
29
  // default active theme; admins can switch per-site at runtime. Add
30
30
  // / remove themes later by editing themes-registry.ts and
31
- // themes/<name>/.
31
+ // themes/<name>/. Initial values pick every shipped theme so the
32
+ // shared `themes-registry.ts` placeholder (which imports all of
33
+ // them) compiles out of the box; deselect any the project won't
34
+ // need before confirming.
32
35
  themes: () => p.multiselect({
33
36
  message: "Themes to install (space to toggle)",
34
37
  options: [
@@ -39,7 +42,7 @@ async function runPrompts(argProjectName) {
39
42
  { value: "docs", label: "Docs \u2014 sidebar-led docs (tag-driven sections)" },
40
43
  { value: "dads", label: "DADS \u2014 Digital Agency Design System (Japanese government style)" }
41
44
  ],
42
- initialValues: ["blog"],
45
+ initialValues: ["blog", "minimal", "landing", "corporate", "docs", "dads"],
43
46
  required: true
44
47
  }),
45
48
  plugins: () => p.multiselect({
@@ -83,8 +86,8 @@ async function runPrompts(argProjectName) {
83
86
  }
84
87
 
85
88
  // src/scaffold.ts
86
- import { cp, readFile, writeFile, readdir, mkdir, rm } from "fs/promises";
87
- import { join, extname, resolve as resolve2 } from "path";
89
+ import { cp, readFile, writeFile, readdir as readdir2, mkdir, rm } from "fs/promises";
90
+ import { join as join2, extname, resolve as resolve2 } from "path";
88
91
 
89
92
  // src/templates.ts
90
93
  import { existsSync } from "fs";
@@ -104,6 +107,100 @@ function templatePath(theme) {
104
107
  return resolve(templatesDir, theme);
105
108
  }
106
109
 
110
+ // src/gitignore.ts
111
+ var DEFAULT_GITIGNORE = `# Dependencies
112
+ node_modules/
113
+
114
+ # Next.js
115
+ .next/
116
+ next-env.d.ts
117
+
118
+ # Amplify
119
+ .amplify/
120
+ amplify_outputs.json
121
+
122
+ # TypeScript build artifacts
123
+ *.tsbuildinfo
124
+
125
+ # Env / OS noise
126
+ .env
127
+ .env.local
128
+ .env.*.local
129
+ .DS_Store
130
+
131
+ # Logs
132
+ npm-debug.log*
133
+ yarn-debug.log*
134
+ yarn-error.log*
135
+
136
+ # Editor
137
+ .vscode/
138
+ .idea/
139
+ `;
140
+
141
+ // src/themes-registry.ts
142
+ import { readdir } from "fs/promises";
143
+ import { existsSync as existsSync2 } from "fs";
144
+ import { join } from "path";
145
+ var CUSTOM_THEME_PREFIX = "my-";
146
+ function toIdentifier(themeName) {
147
+ if (!themeName.includes("-")) return themeName;
148
+ return themeName.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
149
+ }
150
+ function isCustomTheme(themeName) {
151
+ return themeName.startsWith(CUSTOM_THEME_PREFIX);
152
+ }
153
+ async function discoverInstalledThemes(destDir) {
154
+ const themesDir = join(destDir, "themes");
155
+ if (!existsSync2(themesDir)) return [];
156
+ const entries = await readdir(themesDir, { withFileTypes: true });
157
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
158
+ }
159
+ function buildRegistry(themes) {
160
+ const sorted = [...themes].sort((a, b) => {
161
+ const ca = isCustomTheme(a);
162
+ const cb = isCustomTheme(b);
163
+ if (ca !== cb) return ca ? 1 : -1;
164
+ return a.localeCompare(b);
165
+ });
166
+ const imports = sorted.map((t) => `import ${toIdentifier(t)} from '@/themes/${t}'`).join("\n");
167
+ const map = sorted.map((t) => {
168
+ const id = toIdentifier(t);
169
+ return id === t ? ` ${t},` : ` '${t}': ${id},`;
170
+ }).join("\n");
171
+ const fallback = sorted[0] ?? "blog";
172
+ return `// Generated by create-ampless. Lists every theme installed under
173
+ // \`themes/<name>/\`. Auto-managed: \`create-ampless upgrade\` and
174
+ // \`copy-theme\` regenerate this file from the directories present
175
+ // under \`themes/\`. Edits made by hand will survive only until the
176
+ // next upgrade.
177
+ //
178
+ // \`theme.active\` (per-site, in KvStore) picks which theme renders for
179
+ // each request; everything listed here gets bundled so switching is
180
+ // instant. Custom themes use the \`${CUSTOM_THEME_PREFIX}\` prefix and
181
+ // are never touched by upgrade \u2014 clone an ampless default theme into
182
+ // a \`${CUSTOM_THEME_PREFIX}<name>\` directory via \`copy-theme\` before
183
+ // modifying it.
184
+
185
+ ${imports}
186
+
187
+ export const themes = {
188
+ ${map}
189
+ } as const
190
+
191
+ export type ThemeName = keyof typeof themes
192
+
193
+ export const themeList = Object.values(themes)
194
+
195
+ /**
196
+ * Default theme used when a site has no \`theme.active\` override.
197
+ * Falls back to the first registered theme so an empty registry would
198
+ * still surface a clear runtime error rather than silent breakage.
199
+ */
200
+ export const DEFAULT_THEME: ThemeName = (themeList[0]?.name as ThemeName) ?? '${fallback}'
201
+ `;
202
+ }
203
+
107
204
  // src/scaffold.ts
108
205
  var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
109
206
  ".json",
@@ -131,10 +228,10 @@ async function substituteFile(filePath, vars) {
131
228
  if (replaced !== content) await writeFile(filePath, replaced, "utf-8");
132
229
  }
133
230
  async function substituteDir(dirPath, vars) {
134
- const entries = await readdir(dirPath, { withFileTypes: true });
231
+ const entries = await readdir2(dirPath, { withFileTypes: true });
135
232
  await Promise.all(
136
233
  entries.map(async (entry) => {
137
- const fullPath = join(dirPath, entry.name);
234
+ const fullPath = join2(dirPath, entry.name);
138
235
  if (entry.isDirectory()) {
139
236
  await substituteDir(fullPath, vars);
140
237
  } else {
@@ -143,30 +240,6 @@ async function substituteDir(dirPath, vars) {
143
240
  })
144
241
  );
145
242
  }
146
- function buildRegistry(themes) {
147
- const imports = themes.map((t) => `import ${t} from '@/themes/${t}'`).join("\n");
148
- const map = themes.map((t) => ` ${t},`).join("\n");
149
- return `// Generated by create-ampless. Lists every theme installed under
150
- // \`themes/<name>/\`. Adding a theme = drop a directory under themes/,
151
- // add an import + map entry below, and redeploy.
152
- //
153
- // \`theme.active\` (per-site, in KvStore) picks which theme renders for
154
- // each request; everything listed here gets bundled so switching is
155
- // instant.
156
-
157
- ${imports}
158
-
159
- export const themes = {
160
- ${map}
161
- } as const
162
-
163
- export type ThemeName = keyof typeof themes
164
-
165
- export const themeList = Object.values(themes)
166
-
167
- export const DEFAULT_THEME: ThemeName = (themeList[0]?.name as ThemeName) ?? '${themes[0]}'
168
- `;
169
- }
170
243
  async function scaffold(sharedDir, themesRoot, destDir, opts) {
171
244
  await cp(sharedDir, destDir, { recursive: true });
172
245
  const destThemesDir = resolve2(destDir, "themes");
@@ -180,6 +253,8 @@ async function scaffold(sharedDir, themesRoot, destDir, opts) {
180
253
  void themesRoot;
181
254
  const registryPath = resolve2(destDir, "themes-registry.ts");
182
255
  await writeFile(registryPath, buildRegistry(opts.themes), "utf-8");
256
+ const gitignorePath = resolve2(destDir, ".gitignore");
257
+ await writeFile(gitignorePath, DEFAULT_GITIGNORE, "utf-8");
183
258
  const vars = {
184
259
  projectName: opts.projectName,
185
260
  siteName: opts.siteName,
@@ -191,36 +266,2011 @@ async function scaffold(sharedDir, themesRoot, destDir, opts) {
191
266
  await substituteDir(destDir, vars);
192
267
  }
193
268
 
194
- // src/index.ts
269
+ // src/args.ts
270
+ var VALID_THEMES = ["blog", "minimal", "landing", "corporate", "docs", "dads"];
271
+ var VALID_PLUGINS = ["seo", "rss", "webhook"];
272
+ var STRING_FLAGS = /* @__PURE__ */ new Set([
273
+ "--site-name",
274
+ "--themes",
275
+ "--plugins",
276
+ "--github-owner",
277
+ "--github-token",
278
+ "--aws-profile",
279
+ "--aws-region",
280
+ "--domain",
281
+ "--subdomain",
282
+ "--iam-service-role"
283
+ ]);
284
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
285
+ "--deploy",
286
+ "--mount",
287
+ "--upgrade",
288
+ "--dry-run",
289
+ "--no-install",
290
+ "--github-private",
291
+ "--create-iam-role",
292
+ "--skip-confirm",
293
+ "--help",
294
+ "-h"
295
+ ]);
296
+ function parseDeployArgs(argv) {
297
+ const out = {
298
+ deploy: false,
299
+ mount: false,
300
+ upgrade: false,
301
+ copyTheme: false,
302
+ dryRun: false,
303
+ noInstall: false,
304
+ githubPrivate: false,
305
+ createIamRole: false,
306
+ skipConfirm: false,
307
+ help: false,
308
+ unknown: []
309
+ };
310
+ for (let i = 0; i < argv.length; i++) {
311
+ const raw = argv[i];
312
+ let token = raw;
313
+ let inlineValue;
314
+ const eq = raw.indexOf("=");
315
+ if (raw.startsWith("--") && eq > 2) {
316
+ token = raw.slice(0, eq);
317
+ inlineValue = raw.slice(eq + 1);
318
+ }
319
+ if (BOOLEAN_FLAGS.has(token)) {
320
+ switch (token) {
321
+ case "--deploy":
322
+ out.deploy = true;
323
+ break;
324
+ case "--mount":
325
+ out.mount = true;
326
+ break;
327
+ case "--upgrade":
328
+ out.upgrade = true;
329
+ break;
330
+ case "--dry-run":
331
+ out.dryRun = true;
332
+ break;
333
+ case "--no-install":
334
+ out.noInstall = true;
335
+ break;
336
+ case "--github-private":
337
+ out.githubPrivate = true;
338
+ break;
339
+ case "--create-iam-role":
340
+ out.createIamRole = true;
341
+ break;
342
+ case "--skip-confirm":
343
+ out.skipConfirm = true;
344
+ break;
345
+ case "--help":
346
+ case "-h":
347
+ out.help = true;
348
+ break;
349
+ }
350
+ continue;
351
+ }
352
+ if (STRING_FLAGS.has(token)) {
353
+ const value = inlineValue ?? argv[++i];
354
+ if (value === void 0) {
355
+ throw new Error(`Missing value for ${token}`);
356
+ }
357
+ switch (token) {
358
+ case "--site-name":
359
+ out.siteName = value;
360
+ break;
361
+ case "--themes": {
362
+ const themes = value.split(",").map((t) => t.trim()).filter(Boolean);
363
+ const invalid = themes.filter((t) => !VALID_THEMES.includes(t));
364
+ if (invalid.length > 0) {
365
+ throw new Error(
366
+ `Invalid theme(s): ${invalid.join(", ")}. Valid values: ${VALID_THEMES.join(", ")}`
367
+ );
368
+ }
369
+ out.themes = themes;
370
+ break;
371
+ }
372
+ case "--plugins": {
373
+ const plugins = value.split(",").map((p3) => p3.trim()).filter(Boolean);
374
+ const invalid = plugins.filter((p3) => !VALID_PLUGINS.includes(p3));
375
+ if (invalid.length > 0) {
376
+ throw new Error(
377
+ `Invalid plugin(s): ${invalid.join(", ")}. Valid values: ${VALID_PLUGINS.join(", ")}`
378
+ );
379
+ }
380
+ out.plugins = plugins;
381
+ break;
382
+ }
383
+ case "--github-owner":
384
+ out.githubOwner = value;
385
+ break;
386
+ case "--github-token":
387
+ out.githubToken = value;
388
+ break;
389
+ case "--aws-profile":
390
+ out.awsProfile = value;
391
+ break;
392
+ case "--aws-region":
393
+ out.awsRegion = value;
394
+ break;
395
+ case "--domain":
396
+ out.domain = value;
397
+ break;
398
+ case "--subdomain":
399
+ out.subdomain = value;
400
+ break;
401
+ case "--iam-service-role":
402
+ out.iamServiceRole = value;
403
+ break;
404
+ }
405
+ continue;
406
+ }
407
+ if (raw.startsWith("-")) {
408
+ out.unknown.push(raw);
409
+ continue;
410
+ }
411
+ if (raw === "upgrade" && out.projectName === void 0) {
412
+ out.upgrade = true;
413
+ continue;
414
+ }
415
+ if (raw === "copy-theme" && out.projectName === void 0 && !out.copyTheme) {
416
+ out.copyTheme = true;
417
+ continue;
418
+ }
419
+ if (out.copyTheme && out.copyThemeSource === void 0) {
420
+ out.copyThemeSource = raw;
421
+ continue;
422
+ }
423
+ if (out.copyTheme && out.copyThemeTarget === void 0) {
424
+ out.copyThemeTarget = raw;
425
+ continue;
426
+ }
427
+ if (out.projectName === void 0) {
428
+ out.projectName = raw;
429
+ } else {
430
+ out.unknown.push(raw);
431
+ }
432
+ }
433
+ return out;
434
+ }
435
+ var HELP_TEXT = `create-ampless \u2014 scaffold an ampless project
436
+
437
+ Usage:
438
+ npx create-ampless@latest <project-name> [options]
439
+ npx create-ampless@latest --mount [options] # in an existing project dir
440
+ npx create-ampless@latest upgrade [options] # in an existing project dir
441
+
442
+ Options:
443
+ --site-name <name> Site display name (default: "My Blog")
444
+ --themes <list> Comma-separated theme names to install
445
+ Valid: blog, minimal, landing, corporate, docs, dads
446
+ (default: blog)
447
+ --plugins <list> Comma-separated plugin names to install
448
+ Valid: seo, rss, webhook
449
+ (default: seo)
450
+ --deploy Also create GitHub repo + Amplify Hosting app and
451
+ kick off the first deploy after scaffolding
452
+ --mount Skip scaffolding and mount the CURRENT directory
453
+ onto a new GitHub repo + Amplify Hosting app.
454
+ Use after you've scaffolded and tested locally
455
+ with 'npx ampx sandbox' and now want to publish.
456
+ Implies --deploy. Scaffold flags (--site-name,
457
+ --themes, --plugins) are ignored.
458
+ --github-owner <login> GitHub owner (user or org). Defaults to the
459
+ authenticated 'gh' user
460
+ --github-private Create a private repo (default: public)
461
+ --github-token <token> GitHub token. Falls back to GITHUB_TOKEN env,
462
+ then 'gh auth token', then an interactive prompt
463
+ --aws-profile <profile> AWS profile name to pass to the aws CLI
464
+ --aws-region <region> AWS region (defaults to aws config / env)
465
+ --domain <name> Custom domain (apex or subdomain) to attach
466
+ --subdomain <prefix> Subdomain prefix for the domain (default: apex)
467
+ --iam-service-role <arn> Existing IAM role for Amplify Hosting (must trust
468
+ amplify.amazonaws.com and have
469
+ AdministratorAccess-Amplify attached)
470
+ --create-iam-role Let create-ampless provision the Amplify Hosting
471
+ service role (idempotent; defaults to role name
472
+ AmplifyDeployBackend)
473
+ --skip-confirm Skip all interactive prompts and use defaults /
474
+ flag values (for CI / automation)
475
+ -h, --help Show this message
476
+
477
+ upgrade Sync ampless package files / deps to latest alpha.
478
+ Run inside an existing ampless project.
479
+
480
+ --dry-run Show what would change without writing any files
481
+ --no-install Skip running pnpm/npm install after updating
482
+ package.json
483
+
484
+ copy-theme <source> <target>
485
+ Copy a theme so the original stays managed by ampless and
486
+ customisations live in the copy. Target must start with "my-"
487
+ (the convention that flags it as user-owned, so upgrade leaves
488
+ it alone). Run inside an existing ampless project.
489
+
490
+ Example: npx create-ampless@latest copy-theme blog my-blog
491
+ `;
492
+
493
+ // src/deploy-prompts.ts
494
+ import * as p2 from "@clack/prompts";
495
+ import { execa as execa3 } from "execa";
496
+
497
+ // src/deploy.ts
498
+ import { execa as execa2 } from "execa";
499
+ import { existsSync as existsSync4 } from "fs";
500
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
501
+ import { basename as basename2, resolve as resolve4 } from "path";
502
+ import { spinner, log } from "@clack/prompts";
503
+ import pc2 from "picocolors";
504
+
505
+ // src/preflight.ts
506
+ import { execa } from "execa";
507
+ import { basename } from "path";
195
508
  import pc from "picocolors";
509
+ var DEFAULT_AMPLIFY_ROLE_NAME = "AmplifyDeployBackend";
510
+ var AMPLIFY_BACKEND_POLICY_ARN = "arn:aws:iam::aws:policy/AdministratorAccess-Amplify";
511
+ var TRUST_POLICY_JSON = JSON.stringify({
512
+ Version: "2012-10-17",
513
+ Statement: [
514
+ {
515
+ Effect: "Allow",
516
+ Principal: { Service: "amplify.amazonaws.com" },
517
+ Action: "sts:AssumeRole"
518
+ }
519
+ ]
520
+ });
521
+ async function tryExec(cmd, args) {
522
+ try {
523
+ const r = await execa(cmd, args, { reject: false, stdio: "pipe" });
524
+ return {
525
+ exitCode: r.exitCode ?? 0,
526
+ stdout: r.stdout?.toString() ?? "",
527
+ stderr: r.stderr?.toString() ?? ""
528
+ };
529
+ } catch (err) {
530
+ const e = err;
531
+ return {
532
+ exitCode: 1,
533
+ stdout: e.stdout ?? "",
534
+ stderr: e.stderr ?? e.message ?? String(err)
535
+ };
536
+ }
537
+ }
538
+ function awsBaseArgs(opts) {
539
+ const out = [];
540
+ if (opts.awsProfile) out.push("--profile", opts.awsProfile);
541
+ if (opts.awsRegion) out.push("--region", opts.awsRegion);
542
+ return out;
543
+ }
544
+ function awsCmd(opts, extra) {
545
+ return [...awsBaseArgs(opts), ...extra, "--output", "json"];
546
+ }
547
+ function shortName(opts) {
548
+ return basename(opts.projectDir);
549
+ }
550
+ async function checkGhInstalled(problems) {
551
+ const r = await tryExec("gh", ["--version"]);
552
+ if (r.exitCode !== 0) {
553
+ problems.push({
554
+ id: "gh-not-installed",
555
+ message: "GitHub CLI (`gh`) is not installed.",
556
+ remediation: [
557
+ "Install it (macOS: `brew install gh`; other OS: https://cli.github.com/),",
558
+ "then run `gh auth login` to sign in."
559
+ ]
560
+ });
561
+ return false;
562
+ }
563
+ return true;
564
+ }
565
+ async function checkGhAuth(problems) {
566
+ const r = await tryExec("gh", ["auth", "status"]);
567
+ const combined = `${r.stdout}
568
+ ${r.stderr}`;
569
+ if (r.exitCode !== 0 || /not logged in/i.test(combined)) {
570
+ problems.push({
571
+ id: "gh-not-authenticated",
572
+ message: "GitHub CLI is not authenticated.",
573
+ remediation: ["Run `gh auth login` and choose a method that includes the `repo` scope."]
574
+ });
575
+ return false;
576
+ }
577
+ const scopesMatch = combined.match(/Token scopes?:\s*([^\n]+)/i);
578
+ if (!scopesMatch) {
579
+ problems.push({
580
+ id: "gh-scopes-unknown",
581
+ message: "Could not determine GitHub token scopes from `gh auth status`.",
582
+ remediation: ["Run `gh auth refresh -s repo` to ensure the `repo` scope is granted."]
583
+ });
584
+ return false;
585
+ }
586
+ const hasRepo = /(^|[^a-z])repo([^a-z]|$)/i.test(scopesMatch[1]);
587
+ if (!hasRepo) {
588
+ problems.push({
589
+ id: "gh-missing-repo-scope",
590
+ message: "GitHub token is missing the `repo` scope.",
591
+ remediation: ["Run `gh auth refresh -s repo` to add the `repo` scope to your token."]
592
+ });
593
+ return false;
594
+ }
595
+ return true;
596
+ }
597
+ async function checkAwsInstalled(problems) {
598
+ const r = await tryExec("aws", ["--version"]);
599
+ if (r.exitCode !== 0) {
600
+ problems.push({
601
+ id: "aws-not-installed",
602
+ message: "AWS CLI (`aws`) is not installed.",
603
+ remediation: [
604
+ "Install it (macOS: `brew install awscli`; other OS: https://aws.amazon.com/cli/),",
605
+ "then run `aws configure` to set credentials."
606
+ ]
607
+ });
608
+ return false;
609
+ }
610
+ return true;
611
+ }
612
+ async function checkAwsCreds(opts, problems) {
613
+ const r = await tryExec("aws", [...awsBaseArgs(opts), "sts", "get-caller-identity", "--output", "json"]);
614
+ if (r.exitCode !== 0) {
615
+ problems.push({
616
+ id: "aws-credentials-missing",
617
+ message: "AWS credentials are not configured (sts get-caller-identity failed).",
618
+ remediation: [
619
+ "Run `aws configure` (or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN),",
620
+ opts.awsProfile ? `or check that profile \`${opts.awsProfile}\` exists in ~/.aws/config.` : "or pass --aws-profile <name> if you use named profiles."
621
+ ]
622
+ });
623
+ return void 0;
624
+ }
625
+ try {
626
+ const parsed = JSON.parse(r.stdout);
627
+ const account = parsed.Account;
628
+ if (!account) {
629
+ problems.push({
630
+ id: "aws-identity-malformed",
631
+ message: "aws sts get-caller-identity did not return an Account field.",
632
+ remediation: ["Re-run `aws sts get-caller-identity` manually to inspect the response."]
633
+ });
634
+ return void 0;
635
+ }
636
+ let region = opts.awsRegion;
637
+ if (!region) {
638
+ const fromEnv = process.env.AWS_REGION?.trim() || process.env.AWS_DEFAULT_REGION?.trim();
639
+ if (fromEnv) region = fromEnv;
640
+ }
641
+ if (!region) {
642
+ const cfg = await tryExec("aws", [...awsBaseArgs(opts), "configure", "get", "region"]);
643
+ const trimmed = cfg.stdout.trim();
644
+ if (trimmed) region = trimmed;
645
+ }
646
+ if (!region) {
647
+ problems.push({
648
+ id: "aws-region-missing",
649
+ message: "AWS region is not set.",
650
+ remediation: [
651
+ "Pass --aws-region <name>, set AWS_REGION env var, or run `aws configure set region <name>`."
652
+ ]
653
+ });
654
+ return void 0;
655
+ }
656
+ return { account, region };
657
+ } catch (err) {
658
+ problems.push({
659
+ id: "aws-identity-parse-failed",
660
+ message: `Could not parse aws sts get-caller-identity response: ${err instanceof Error ? err.message : String(err)}`,
661
+ remediation: ["Re-run `aws sts get-caller-identity` manually to inspect the response."]
662
+ });
663
+ return void 0;
664
+ }
665
+ }
666
+ async function checkGithubRepoFree(opts, owner, problems) {
667
+ if (!owner) return;
668
+ const name = `${owner}/${shortName(opts)}`;
669
+ const r = await tryExec("gh", ["repo", "view", name]);
670
+ if (r.exitCode === 0) {
671
+ problems.push({
672
+ id: "github-repo-exists",
673
+ message: `GitHub repo ${name} already exists.`,
674
+ remediation: [
675
+ "Pick a different project name, or delete the existing repo first:",
676
+ ` gh repo delete ${name} --yes`
677
+ ]
678
+ });
679
+ }
680
+ }
681
+ async function checkAmplifyAppNameFree(opts, problems) {
682
+ const name = shortName(opts);
683
+ const r = await tryExec(
684
+ "aws",
685
+ awsCmd(opts, [
686
+ "amplify",
687
+ "list-apps",
688
+ "--query",
689
+ `apps[?name=='${name}']`
690
+ ])
691
+ );
692
+ if (r.exitCode !== 0) {
693
+ problems.push({
694
+ id: "amplify-list-apps-failed",
695
+ message: "Could not list Amplify Hosting apps to check for a name collision.",
696
+ remediation: [
697
+ "Verify your IAM identity has `amplify:ListApps` permission, then re-run.",
698
+ `Raw error: ${r.stderr.trim() || r.stdout.trim()}`
699
+ ]
700
+ });
701
+ return;
702
+ }
703
+ try {
704
+ const arr = JSON.parse(r.stdout);
705
+ if (Array.isArray(arr) && arr.length > 0) {
706
+ problems.push({
707
+ id: "amplify-app-name-taken",
708
+ message: `Amplify Hosting already has an app named "${name}" in this region.`,
709
+ remediation: [
710
+ "Pick a different project name, or delete the existing app first:",
711
+ ` aws amplify list-apps --query "apps[?name=='${name}'].appId" --output text${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}${opts.awsRegion ? ` --region ${opts.awsRegion}` : ""}`,
712
+ " aws amplify delete-app --app-id <appId>"
713
+ ]
714
+ });
715
+ }
716
+ } catch {
717
+ }
718
+ }
719
+ async function checkCdkBootstrap(opts, identity, problems) {
720
+ const r = await tryExec(
721
+ "aws",
722
+ [
723
+ ...awsBaseArgs(opts),
724
+ "ssm",
725
+ "get-parameter",
726
+ "--name",
727
+ "/cdk-bootstrap/hnb659fds/version",
728
+ "--output",
729
+ "json"
730
+ ]
731
+ );
732
+ if (r.exitCode !== 0) {
733
+ problems.push({
734
+ id: "cdk-not-bootstrapped",
735
+ message: `CDK is not bootstrapped in region ${identity.region}.`,
736
+ remediation: [
737
+ `Run: npx cdk bootstrap aws://${identity.account}/${identity.region}`
738
+ ]
739
+ });
740
+ }
741
+ }
742
+ async function checkRoute53Zone(opts, problems) {
743
+ if (!opts.domain) return;
744
+ const registrable = extractRegistrableDomain(opts.domain);
745
+ const r = await tryExec(
746
+ "aws",
747
+ [
748
+ ...awsBaseArgs(opts),
749
+ "route53",
750
+ "list-hosted-zones-by-name",
751
+ "--dns-name",
752
+ registrable,
753
+ "--max-items",
754
+ "1",
755
+ "--output",
756
+ "json"
757
+ ]
758
+ );
759
+ if (r.exitCode !== 0) {
760
+ problems.push({
761
+ id: "route53-list-failed",
762
+ message: `Could not query Route 53 for zone ${registrable}.`,
763
+ remediation: [
764
+ "Either grant `route53:ListHostedZonesByName` to your IAM identity,",
765
+ "or be prepared to add the CNAME records Amplify prints manually after deploy.",
766
+ `Raw error: ${r.stderr.trim() || r.stdout.trim()}`
767
+ ]
768
+ });
769
+ return;
770
+ }
771
+ try {
772
+ const parsed = JSON.parse(r.stdout);
773
+ const zones = parsed.HostedZones ?? [];
774
+ const found = zones.some(
775
+ (z) => z.Name && z.Name.replace(/\.$/, "") === registrable
776
+ );
777
+ if (!found) {
778
+ problems.push({
779
+ id: "route53-zone-missing",
780
+ message: `No Route 53 hosted zone for ${registrable} found in this AWS account.`,
781
+ remediation: [
782
+ "Amplify will still issue an ACM certificate, but you will need to add",
783
+ "the verification CNAMEs at your DNS provider manually. If you want",
784
+ "auto-validation, create a hosted zone first:",
785
+ ` aws route53 create-hosted-zone --name ${registrable} --caller-reference $(date +%s)${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}`
786
+ ]
787
+ });
788
+ }
789
+ } catch {
790
+ }
791
+ }
792
+ async function resolveIamServiceRole(opts, args, problems) {
793
+ if (args.explicitArn) {
794
+ const roleName = args.explicitArn.split("/").pop() ?? args.explicitArn;
795
+ const r = await tryExec(
796
+ "aws",
797
+ [
798
+ ...awsBaseArgs(opts),
799
+ "iam",
800
+ "get-role",
801
+ "--role-name",
802
+ roleName,
803
+ "--output",
804
+ "json"
805
+ ]
806
+ );
807
+ if (r.exitCode !== 0) {
808
+ problems.push({
809
+ id: "iam-service-role-not-found",
810
+ message: `IAM role ${args.explicitArn} not found or inaccessible.`,
811
+ remediation: [
812
+ "Verify the ARN is correct and the role exists:",
813
+ ` aws iam get-role --role-name ${roleName}${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}`
814
+ ]
815
+ });
816
+ return { willCreate: false };
817
+ }
818
+ return { arn: args.explicitArn, willCreate: false };
819
+ }
820
+ if (args.willCreate) {
821
+ return { willCreate: true };
822
+ }
823
+ const found = await findExistingAmplifyServiceRole(opts);
824
+ if (found) {
825
+ return { arn: found, willCreate: false };
826
+ }
827
+ problems.push({
828
+ id: "iam-service-role-missing",
829
+ message: "No Amplify Hosting service role found.",
830
+ remediation: [
831
+ "Either pass --iam-service-role <arn> pointing at an existing role,",
832
+ "or let create-ampless build one for you with --create-iam-role.",
833
+ "",
834
+ "Alternatively, create one yourself:",
835
+ ` aws iam create-role --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} \\`,
836
+ ` --assume-role-policy-document '${TRUST_POLICY_JSON}'`,
837
+ ` aws iam attach-role-policy --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} \\`,
838
+ ` --policy-arn ${AMPLIFY_BACKEND_POLICY_ARN}`,
839
+ ` # then re-run with: --iam-service-role $(aws iam get-role --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} --query Role.Arn --output text)`
840
+ ]
841
+ });
842
+ return { willCreate: false };
843
+ }
844
+ function trustPolicyAllowsAmplify(doc) {
845
+ let parsed = doc;
846
+ if (typeof doc === "string") {
847
+ try {
848
+ parsed = JSON.parse(decodeURIComponent(doc));
849
+ } catch {
850
+ try {
851
+ parsed = JSON.parse(doc);
852
+ } catch {
853
+ return false;
854
+ }
855
+ }
856
+ }
857
+ const stmts = parsed?.Statement;
858
+ const arr = Array.isArray(stmts) ? stmts : stmts ? [stmts] : [];
859
+ for (const stmt of arr) {
860
+ const s = stmt;
861
+ const svc = s.Principal?.Service;
862
+ if (typeof svc === "string" && svc === "amplify.amazonaws.com") return true;
863
+ if (Array.isArray(svc) && svc.includes("amplify.amazonaws.com")) return true;
864
+ }
865
+ return false;
866
+ }
867
+ async function findExistingAmplifyServiceRole(opts) {
868
+ let marker;
869
+ for (let page = 0; page < 20; page++) {
870
+ const args = [
871
+ ...awsBaseArgs(opts),
872
+ "iam",
873
+ "list-roles",
874
+ "--max-items",
875
+ "200",
876
+ "--output",
877
+ "json"
878
+ ];
879
+ if (marker) args.push("--starting-token", marker);
880
+ const r = await tryExec("aws", args);
881
+ if (r.exitCode !== 0) return void 0;
882
+ let parsed;
883
+ try {
884
+ parsed = JSON.parse(r.stdout);
885
+ } catch {
886
+ return void 0;
887
+ }
888
+ for (const role of parsed.Roles ?? []) {
889
+ if (!role.RoleName || !role.Arn) continue;
890
+ if (!trustPolicyAllowsAmplify(role.AssumeRolePolicyDocument)) continue;
891
+ const pr = await tryExec(
892
+ "aws",
893
+ [
894
+ ...awsBaseArgs(opts),
895
+ "iam",
896
+ "list-attached-role-policies",
897
+ "--role-name",
898
+ role.RoleName,
899
+ "--output",
900
+ "json"
901
+ ]
902
+ );
903
+ if (pr.exitCode !== 0) continue;
904
+ try {
905
+ const policies = JSON.parse(pr.stdout);
906
+ const attached = (policies.AttachedPolicies ?? []).some(
907
+ (p3) => p3.PolicyArn === AMPLIFY_BACKEND_POLICY_ARN
908
+ );
909
+ if (attached) return role.Arn;
910
+ } catch {
911
+ continue;
912
+ }
913
+ }
914
+ if (!parsed.IsTruncated) break;
915
+ marker = parsed.Marker;
916
+ if (!marker) break;
917
+ }
918
+ return void 0;
919
+ }
920
+ async function runPreflight(opts, extra = {}) {
921
+ const problems = [];
922
+ const hasGh = await checkGhInstalled(problems);
923
+ if (hasGh) await checkGhAuth(problems);
924
+ const hasAws = await checkAwsInstalled(problems);
925
+ const identity = hasAws ? await checkAwsCreds(opts, problems) : void 0;
926
+ if (hasGh && opts.githubOwner && !extra.mountMode) {
927
+ await checkGithubRepoFree(opts, opts.githubOwner, problems);
928
+ }
929
+ if (identity) {
930
+ await checkAmplifyAppNameFree(opts, problems);
931
+ await checkCdkBootstrap(opts, identity, problems);
932
+ await checkRoute53Zone(opts, problems);
933
+ }
934
+ let willCreateIamRole = false;
935
+ let iamServiceRoleArn;
936
+ if (identity) {
937
+ const resolved = await resolveIamServiceRole(
938
+ opts,
939
+ { explicitArn: extra.iamServiceRoleArn, willCreate: extra.createIamRole === true },
940
+ problems
941
+ );
942
+ willCreateIamRole = resolved.willCreate;
943
+ iamServiceRoleArn = resolved.arn;
944
+ }
945
+ if (problems.length > 0) {
946
+ return { problems };
947
+ }
948
+ return {
949
+ problems,
950
+ resolution: {
951
+ iamServiceRoleArn,
952
+ willCreateIamRole,
953
+ awsAccount: identity.account,
954
+ awsRegion: identity.region
955
+ }
956
+ };
957
+ }
958
+ function formatPreflightReport(problems) {
959
+ const lines = [];
960
+ lines.push(`${pc.red("\u2717")} create-ampless --deploy: prerequisites missing`);
961
+ lines.push("");
962
+ for (const p3 of problems) {
963
+ lines.push(` ${pc.red("\u2717")} ${p3.message}`);
964
+ for (const r of p3.remediation) {
965
+ lines.push(r ? ` ${r}` : "");
966
+ }
967
+ lines.push("");
968
+ }
969
+ lines.push("Fix the items above and re-run.");
970
+ return lines.join("\n");
971
+ }
972
+ function printPreflightReport(problems) {
973
+ process.stderr.write(`${formatPreflightReport(problems)}
974
+ `);
975
+ }
976
+
977
+ // src/mount.ts
978
+ import { existsSync as existsSync3 } from "fs";
979
+ import { resolve as resolve3 } from "path";
980
+ function validateMountableProject(destDir) {
981
+ const required = ["package.json", "cms.config.ts"];
982
+ for (const f of required) {
983
+ if (!existsSync3(resolve3(destDir, f))) {
984
+ return `Not an ampless project (missing ${f} in ${destDir})`;
985
+ }
986
+ }
987
+ const amplifyFiles = ["amplify/backend.ts", "amplify/data/resource.ts"];
988
+ if (!amplifyFiles.some((f) => existsSync3(resolve3(destDir, f)))) {
989
+ return `Not an ampless project (missing amplify/ in ${destDir})`;
990
+ }
991
+ return null;
992
+ }
993
+ function originPointsAt(origin, owner, name) {
994
+ const candidates = [
995
+ `https://github.com/${owner}/${name}`,
996
+ `https://github.com/${owner}/${name}.git`,
997
+ `git@github.com:${owner}/${name}`,
998
+ `git@github.com:${owner}/${name}.git`,
999
+ `ssh://git@github.com/${owner}/${name}`,
1000
+ `ssh://git@github.com/${owner}/${name}.git`
1001
+ ];
1002
+ return candidates.includes(origin);
1003
+ }
1004
+
1005
+ // src/deploy.ts
1006
+ var AMPLIFY_BUILD_SPEC = `version: 1
1007
+ frontend:
1008
+ phases:
1009
+ preBuild:
1010
+ commands:
1011
+ - npm install
1012
+ build:
1013
+ commands:
1014
+ - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
1015
+ - npm run build
1016
+ artifacts:
1017
+ baseDirectory: .next
1018
+ files:
1019
+ - '**/*'
1020
+ cache:
1021
+ paths:
1022
+ - node_modules/**/*
1023
+ - .next/cache/**/*
1024
+ `;
1025
+ async function resolveGithubToken(explicit, env = process.env) {
1026
+ if (explicit && explicit.trim()) return explicit.trim();
1027
+ const envToken = env.GITHUB_TOKEN;
1028
+ if (envToken && envToken.trim()) return envToken.trim();
1029
+ try {
1030
+ const { stdout } = await execa2("gh", ["auth", "token"], { reject: false });
1031
+ const trimmed = stdout?.trim();
1032
+ if (trimmed) return trimmed;
1033
+ } catch {
1034
+ }
1035
+ return void 0;
1036
+ }
1037
+ function extractRegistrableDomain(domain) {
1038
+ const labels = domain.replace(/\.$/, "").split(".");
1039
+ if (labels.length <= 2) return labels.join(".");
1040
+ const MULTI_PART_SUFFIXES = /* @__PURE__ */ new Set([
1041
+ "co.uk",
1042
+ "org.uk",
1043
+ "me.uk",
1044
+ "gov.uk",
1045
+ "ac.uk",
1046
+ "co.jp",
1047
+ "ne.jp",
1048
+ "or.jp",
1049
+ "ac.jp",
1050
+ "go.jp",
1051
+ "ad.jp",
1052
+ "gr.jp",
1053
+ "lg.jp",
1054
+ "com.au",
1055
+ "net.au",
1056
+ "org.au",
1057
+ "edu.au",
1058
+ "gov.au",
1059
+ "co.nz",
1060
+ "net.nz",
1061
+ "org.nz",
1062
+ "com.br",
1063
+ "com.mx",
1064
+ "com.cn",
1065
+ "com.sg",
1066
+ "com.hk",
1067
+ "com.tw",
1068
+ "co.kr",
1069
+ "or.kr",
1070
+ "co.in",
1071
+ "co.id",
1072
+ "co.za"
1073
+ ]);
1074
+ const lastTwo = labels.slice(-2).join(".");
1075
+ if (MULTI_PART_SUFFIXES.has(lastTwo) && labels.length >= 3) {
1076
+ return labels.slice(-3).join(".");
1077
+ }
1078
+ return lastTwo;
1079
+ }
1080
+ function splitDomain(domain, subdomain) {
1081
+ const registrable = extractRegistrableDomain(domain);
1082
+ if (subdomain !== void 0) {
1083
+ return { registrable, subdomain };
1084
+ }
1085
+ if (domain === registrable) {
1086
+ return { registrable, subdomain: "" };
1087
+ }
1088
+ const prefix = domain.slice(0, domain.length - registrable.length - 1);
1089
+ return { registrable, subdomain: prefix };
1090
+ }
1091
+ async function rewriteCmsConfigForDomain(projectDir, fullDomain) {
1092
+ const path = resolve4(projectDir, "cms.config.ts");
1093
+ if (!existsSync4(path)) {
1094
+ return { urlRewritten: false, sitesInjected: false };
1095
+ }
1096
+ let content = await readFile2(path, "utf-8");
1097
+ let urlRewritten = false;
1098
+ let sitesInjected = false;
1099
+ const urlRe = /url:\s*['"]http:\/\/localhost:3000['"]/;
1100
+ if (urlRe.test(content)) {
1101
+ content = content.replace(urlRe, `url: 'https://${fullDomain}'`);
1102
+ urlRewritten = true;
1103
+ }
1104
+ const sitesActiveRe = /^\s*sites:\s*\{/m;
1105
+ if (!sitesActiveRe.test(content)) {
1106
+ const siteCloseRe = /(\n(\s*)site:\s*\{[\s\S]*?\n\2\},)/;
1107
+ const m = siteCloseRe.exec(content);
1108
+ if (m) {
1109
+ const indent = m[2] ?? " ";
1110
+ const inner = indent + " ";
1111
+ const innermost = inner + " ";
1112
+ const inject = `
1113
+ ${indent}sites: {
1114
+ ${inner}default: {
1115
+ ${innermost}domains: ['${fullDomain}'],
1116
+ ${inner}},
1117
+ ${indent}},`;
1118
+ content = content.replace(siteCloseRe, `$1${inject}`);
1119
+ sitesInjected = true;
1120
+ }
1121
+ }
1122
+ if (urlRewritten || sitesInjected) {
1123
+ await writeFile2(path, content, "utf-8");
1124
+ }
1125
+ return { urlRewritten, sitesInjected };
1126
+ }
1127
+ async function run(cmd, args, opts = {}) {
1128
+ const { step, ...execaOpts } = opts;
1129
+ try {
1130
+ const result = await execa2(cmd, args, { stdio: "pipe", ...execaOpts });
1131
+ return result.stdout?.toString() ?? "";
1132
+ } catch (err) {
1133
+ const e = err;
1134
+ const detail = (e.stderr || e.stdout || e.shortMessage || e.message || String(err)).trim();
1135
+ const prefix = step ? `${step}: ` : "";
1136
+ throw new Error(`${prefix}${cmd} ${args.join(" ")}
1137
+ ${detail}`);
1138
+ }
1139
+ }
1140
+ function awsArgs(opts, extra) {
1141
+ const base = [];
1142
+ if (opts.awsProfile) base.push("--profile", opts.awsProfile);
1143
+ if (opts.awsRegion) base.push("--region", opts.awsRegion);
1144
+ base.push(...extra);
1145
+ base.push("--output", "json");
1146
+ return base;
1147
+ }
1148
+ async function isGitRepo(dir) {
1149
+ const r = await execa2("git", ["rev-parse", "--git-dir"], { cwd: dir, reject: false });
1150
+ return r.exitCode === 0;
1151
+ }
1152
+ async function isDirty(dir) {
1153
+ const r = await execa2("git", ["status", "--porcelain"], { cwd: dir, reject: false });
1154
+ return (r.stdout?.toString() ?? "").trim().length > 0;
1155
+ }
1156
+ async function currentBranch(dir) {
1157
+ const r = await execa2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1158
+ cwd: dir,
1159
+ reject: false
1160
+ });
1161
+ if (r.exitCode !== 0) return null;
1162
+ const name = r.stdout?.toString().trim();
1163
+ return name && name !== "HEAD" ? name : null;
1164
+ }
1165
+ async function ensureGitignore(dir) {
1166
+ const target = resolve4(dir, ".gitignore");
1167
+ if (existsSync4(target)) return;
1168
+ await writeFile2(target, DEFAULT_GITIGNORE, "utf-8");
1169
+ }
1170
+ async function gitInitOrReuse(dir, mount) {
1171
+ const repo = await isGitRepo(dir);
1172
+ if (!repo) {
1173
+ await run("git", ["init", "-b", "main"], { cwd: dir, step: "git init" });
1174
+ }
1175
+ const dirty = await isDirty(dir);
1176
+ if (dirty) {
1177
+ await run("git", ["add", "."], { cwd: dir, step: "git add" });
1178
+ const message = mount && repo ? "Prepare for create-ampless --mount" : "Initial scaffold (create-ampless)";
1179
+ await run("git", ["commit", "-m", message], { cwd: dir, step: "git commit" });
1180
+ }
1181
+ if (mount) {
1182
+ const branch = await currentBranch(dir);
1183
+ if (branch && branch !== "main") {
1184
+ log.warn(
1185
+ `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\`.`
1186
+ );
1187
+ }
1188
+ }
1189
+ }
1190
+ function shortName2(opts) {
1191
+ return basename2(opts.projectDir);
1192
+ }
1193
+ async function ghRepoExists(name, token) {
1194
+ const r = await execa2("gh", ["repo", "view", name, "--json", "nameWithOwner"], {
1195
+ reject: false,
1196
+ env: { ...process.env, GH_TOKEN: token }
1197
+ });
1198
+ if (r.exitCode !== 0) return false;
1199
+ try {
1200
+ const parsed = JSON.parse(r.stdout?.toString() ?? "{}");
1201
+ const resolved = parsed.nameWithOwner?.toLowerCase();
1202
+ return resolved === name.toLowerCase();
1203
+ } catch {
1204
+ return true;
1205
+ }
1206
+ }
1207
+ async function getOriginUrl(dir) {
1208
+ const r = await execa2("git", ["remote", "get-url", "origin"], {
1209
+ cwd: dir,
1210
+ reject: false
1211
+ });
1212
+ if (r.exitCode !== 0) return null;
1213
+ const url = r.stdout?.toString().trim();
1214
+ return url ? url : null;
1215
+ }
1216
+ function repoHttpsUrl(owner, name) {
1217
+ return `https://github.com/${owner}/${name}`;
1218
+ }
1219
+ async function ghRepoCreate(opts) {
1220
+ const owner = opts.githubOwner;
1221
+ const name = shortName2(opts);
1222
+ const fullName = `${owner}/${name}`;
1223
+ const visibility = opts.githubPrivate ? "--private" : "--public";
1224
+ const exists = await ghRepoExists(fullName, opts.githubToken);
1225
+ if (exists) {
1226
+ const origin = await getOriginUrl(opts.projectDir);
1227
+ if (origin && !originPointsAt(origin, owner, name)) {
1228
+ throw new Error(
1229
+ `Existing 'origin' remote points at ${origin}, but --mount expects ${fullName}.
1230
+ Remove or rename the existing remote (\`git remote remove origin\`) and re-run.`
1231
+ );
1232
+ }
1233
+ if (!origin) {
1234
+ await run(
1235
+ "git",
1236
+ ["remote", "add", "origin", `https://github.com/${fullName}.git`],
1237
+ { cwd: opts.projectDir, step: "git remote add origin" }
1238
+ );
1239
+ }
1240
+ await run(
1241
+ "git",
1242
+ ["push", "-u", "origin", "HEAD:main"],
1243
+ {
1244
+ cwd: opts.projectDir,
1245
+ step: "git push origin main",
1246
+ env: { ...process.env, GH_TOKEN: opts.githubToken }
1247
+ }
1248
+ );
1249
+ return repoHttpsUrl(owner, name);
1250
+ }
1251
+ const out = await run(
1252
+ "gh",
1253
+ [
1254
+ "repo",
1255
+ "create",
1256
+ fullName,
1257
+ "--source",
1258
+ opts.projectDir,
1259
+ "--push",
1260
+ visibility,
1261
+ "--description",
1262
+ "Created by create-ampless"
1263
+ ],
1264
+ {
1265
+ cwd: opts.projectDir,
1266
+ step: "gh repo create",
1267
+ env: { ...process.env, GH_TOKEN: opts.githubToken }
1268
+ }
1269
+ );
1270
+ const match = out.match(/https:\/\/github\.com\/[^\s]+/g);
1271
+ if (match && match.length > 0) return match[match.length - 1].replace(/[.,]$/, "");
1272
+ return repoHttpsUrl(owner, name);
1273
+ }
1274
+ async function amplifyCreateApp(opts, repoUrl, iamServiceRoleArn) {
1275
+ const cmd = [
1276
+ "amplify",
1277
+ "create-app",
1278
+ "--name",
1279
+ shortName2(opts),
1280
+ "--repository",
1281
+ repoUrl,
1282
+ "--access-token",
1283
+ opts.githubToken,
1284
+ "--platform",
1285
+ "WEB_COMPUTE",
1286
+ "--build-spec",
1287
+ AMPLIFY_BUILD_SPEC
1288
+ ];
1289
+ if (iamServiceRoleArn) {
1290
+ cmd.push("--iam-service-role-arn", iamServiceRoleArn);
1291
+ }
1292
+ const out = await run(
1293
+ "aws",
1294
+ awsArgs(opts, cmd),
1295
+ { step: "aws amplify create-app" }
1296
+ );
1297
+ const parsed = JSON.parse(out);
1298
+ const appId = parsed.app?.appId;
1299
+ if (!appId) throw new Error("aws amplify create-app: missing app.appId in response");
1300
+ return {
1301
+ appId,
1302
+ defaultDomain: parsed.app?.defaultDomain ?? "amplifyapp.com"
1303
+ };
1304
+ }
1305
+ async function amplifyCreateBranch(opts, appId) {
1306
+ await run(
1307
+ "aws",
1308
+ awsArgs(opts, [
1309
+ "amplify",
1310
+ "create-branch",
1311
+ "--app-id",
1312
+ appId,
1313
+ "--branch-name",
1314
+ "main",
1315
+ "--stage",
1316
+ "PRODUCTION"
1317
+ ]),
1318
+ { step: "aws amplify create-branch" }
1319
+ );
1320
+ }
1321
+ async function amplifyStartJob(opts, appId) {
1322
+ await run(
1323
+ "aws",
1324
+ awsArgs(opts, [
1325
+ "amplify",
1326
+ "start-job",
1327
+ "--app-id",
1328
+ appId,
1329
+ "--branch-name",
1330
+ "main",
1331
+ "--job-type",
1332
+ "RELEASE"
1333
+ ]),
1334
+ { step: "aws amplify start-job" }
1335
+ );
1336
+ }
1337
+ async function findRoute53Zone(opts, registrable) {
1338
+ try {
1339
+ const out = await run(
1340
+ "aws",
1341
+ awsArgs(opts, [
1342
+ "route53",
1343
+ "list-hosted-zones-by-name",
1344
+ "--dns-name",
1345
+ registrable,
1346
+ "--max-items",
1347
+ "1"
1348
+ ]),
1349
+ { step: "aws route53 list-hosted-zones-by-name" }
1350
+ );
1351
+ const parsed = JSON.parse(out);
1352
+ const zones = parsed.HostedZones ?? [];
1353
+ for (const z of zones) {
1354
+ if (z.Name && z.Name.replace(/\.$/, "") === registrable && z.Id) {
1355
+ return z.Id.replace("/hostedzone/", "");
1356
+ }
1357
+ }
1358
+ } catch {
1359
+ }
1360
+ return void 0;
1361
+ }
1362
+ async function amplifyCreateDomain(opts, appId) {
1363
+ const { registrable, subdomain } = splitDomain(opts.domain, opts.subdomain);
1364
+ const fullName = subdomain ? `${subdomain}.${registrable}` : registrable;
1365
+ const route53Zone = await findRoute53Zone(opts, registrable);
1366
+ if (route53Zone) {
1367
+ log.info(
1368
+ `Route 53 hosted zone detected for ${pc2.cyan(registrable)} \u2014 Amplify will auto-create DNS records once ACM validates.`
1369
+ );
1370
+ }
1371
+ const out = await run(
1372
+ "aws",
1373
+ awsArgs(opts, [
1374
+ "amplify",
1375
+ "create-domain-association",
1376
+ "--app-id",
1377
+ appId,
1378
+ "--domain-name",
1379
+ registrable,
1380
+ "--sub-domain-settings",
1381
+ `prefix=${subdomain},branchName=main`
1382
+ ]),
1383
+ { step: "aws amplify create-domain-association" }
1384
+ );
1385
+ const parsed = JSON.parse(out);
1386
+ const verification = [];
1387
+ const certRecord = parsed.domainAssociation?.certificateVerificationDNSRecord;
1388
+ if (certRecord) {
1389
+ const parts = certRecord.trim().split(/\s+/);
1390
+ if (parts.length >= 3) {
1391
+ verification.push({
1392
+ cname: parts[0].replace(/\.$/, ""),
1393
+ target: parts.slice(2).join(" ").replace(/\.$/, "")
1394
+ });
1395
+ }
1396
+ }
1397
+ for (const sd of parsed.domainAssociation?.subDomains ?? []) {
1398
+ if (!sd.dnsRecord) continue;
1399
+ const parts = sd.dnsRecord.trim().split(/\s+/);
1400
+ if (parts.length >= 3) {
1401
+ const prefix = sd.subDomainSetting?.prefix ?? "";
1402
+ const cname = prefix ? `${prefix}.${registrable}` : registrable;
1403
+ verification.push({
1404
+ cname,
1405
+ target: parts.slice(2).join(" ").replace(/\.$/, "")
1406
+ });
1407
+ }
1408
+ }
1409
+ return {
1410
+ domainUrl: `https://${fullName}`,
1411
+ verification: route53Zone ? void 0 : verification.length > 0 ? verification : void 0
1412
+ };
1413
+ }
1414
+ async function provisionIamServiceRole(opts) {
1415
+ const roleName = DEFAULT_AMPLIFY_ROLE_NAME;
1416
+ const trustPolicy = JSON.stringify({
1417
+ Version: "2012-10-17",
1418
+ Statement: [
1419
+ {
1420
+ Effect: "Allow",
1421
+ Principal: { Service: "amplify.amazonaws.com" },
1422
+ Action: "sts:AssumeRole"
1423
+ }
1424
+ ]
1425
+ });
1426
+ const createArgs = [
1427
+ "iam",
1428
+ "create-role",
1429
+ "--role-name",
1430
+ roleName,
1431
+ "--assume-role-policy-document",
1432
+ trustPolicy,
1433
+ "--description",
1434
+ "Amplify Hosting service role created by create-ampless"
1435
+ ];
1436
+ try {
1437
+ await run("aws", awsArgs(opts, createArgs), { step: "aws iam create-role" });
1438
+ } catch (err) {
1439
+ const msg = err instanceof Error ? err.message : String(err);
1440
+ if (!/EntityAlreadyExists/i.test(msg) && !/already exists/i.test(msg)) {
1441
+ throw err;
1442
+ }
1443
+ }
1444
+ await run(
1445
+ "aws",
1446
+ awsArgs(opts, [
1447
+ "iam",
1448
+ "attach-role-policy",
1449
+ "--role-name",
1450
+ roleName,
1451
+ "--policy-arn",
1452
+ AMPLIFY_BACKEND_POLICY_ARN
1453
+ ]),
1454
+ { step: "aws iam attach-role-policy" }
1455
+ );
1456
+ const out = await run(
1457
+ "aws",
1458
+ awsArgs(opts, ["iam", "get-role", "--role-name", roleName]),
1459
+ { step: "aws iam get-role" }
1460
+ );
1461
+ const parsed = JSON.parse(out);
1462
+ const arn = parsed.Role?.Arn;
1463
+ if (!arn) {
1464
+ throw new Error(`aws iam get-role for ${roleName}: missing Role.Arn in response`);
1465
+ }
1466
+ return arn;
1467
+ }
1468
+ var PreflightError = class extends Error {
1469
+ result;
1470
+ constructor(result) {
1471
+ super("create-ampless --deploy: pre-flight failed");
1472
+ this.name = "PreflightError";
1473
+ this.result = result;
1474
+ }
1475
+ };
1476
+ async function runDeploy(opts) {
1477
+ const pre = await runPreflight(opts, {
1478
+ iamServiceRoleArn: opts.iamServiceRoleArn,
1479
+ createIamRole: opts.createIamRole === true,
1480
+ mountMode: opts.mount === true
1481
+ });
1482
+ if (pre.problems.length > 0) {
1483
+ printPreflightReport(pre.problems);
1484
+ throw new PreflightError(pre);
1485
+ }
1486
+ const resolution = pre.resolution;
1487
+ let serviceRoleArn = resolution.iamServiceRoleArn;
1488
+ if (resolution.willCreateIamRole && !serviceRoleArn) {
1489
+ const s0 = spinner();
1490
+ s0.start(`Provisioning IAM service role ${DEFAULT_AMPLIFY_ROLE_NAME}`);
1491
+ try {
1492
+ serviceRoleArn = await provisionIamServiceRole(opts);
1493
+ s0.stop(`IAM role: ${serviceRoleArn}`);
1494
+ } catch (err) {
1495
+ s0.stop("IAM role provisioning: failed");
1496
+ throw new Error(
1497
+ `Failed to provision IAM service role: ${err instanceof Error ? err.message : String(err)}`
1498
+ );
1499
+ }
1500
+ }
1501
+ await writeFile2(resolve4(opts.projectDir, "amplify.yml"), AMPLIFY_BUILD_SPEC, "utf-8");
1502
+ if (opts.domain) {
1503
+ const { registrable, subdomain } = splitDomain(opts.domain, opts.subdomain);
1504
+ const fullDomain = subdomain ? `${subdomain}.${registrable}` : registrable;
1505
+ const mutations = await rewriteCmsConfigForDomain(opts.projectDir, fullDomain);
1506
+ if (mutations.urlRewritten || mutations.sitesInjected) {
1507
+ const parts = [];
1508
+ if (mutations.urlRewritten) parts.push(`site.url \u2192 https://${fullDomain}`);
1509
+ if (mutations.sitesInjected) parts.push(`sites.default.domains += ${fullDomain}`);
1510
+ log.info(`cms.config.ts updated: ${parts.join(", ")}`);
1511
+ }
1512
+ }
1513
+ const created = {};
1514
+ const fail = (step, cause) => {
1515
+ const msg = cause instanceof Error ? cause.message : String(cause);
1516
+ const cleanup = [];
1517
+ if (created.appId) {
1518
+ cleanup.push(
1519
+ ` - Amplify app: ${created.appId} (delete: aws amplify delete-app --app-id ${created.appId}${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}${opts.awsRegion ? ` --region ${opts.awsRegion}` : ""})`
1520
+ );
1521
+ }
1522
+ if (created.repoUrl) {
1523
+ cleanup.push(
1524
+ ` - GitHub repo: ${created.repoUrl} (delete: gh repo delete ${opts.githubOwner}/${shortName2(opts)} --yes)`
1525
+ );
1526
+ }
1527
+ const cleanupBlock = cleanup.length > 0 ? `
1528
+ Created so far:
1529
+ ${cleanup.join("\n")}
1530
+
1531
+ Re-run after cleaning up.` : "";
1532
+ throw new Error(`Deploy failed at: ${step}
1533
+ ${msg}${cleanupBlock}`);
1534
+ };
1535
+ let s = spinner();
1536
+ const gitMsg = opts.mount ? "git: preparing repo for mount" : "git init + initial commit";
1537
+ s.start(gitMsg);
1538
+ try {
1539
+ if (opts.mount) {
1540
+ await ensureGitignore(opts.projectDir);
1541
+ }
1542
+ await gitInitOrReuse(opts.projectDir, opts.mount === true);
1543
+ s.stop(opts.mount ? "git: ready" : "git: committed initial scaffold");
1544
+ } catch (err) {
1545
+ s.stop("git: failed");
1546
+ fail("git init / commit", err);
1547
+ }
1548
+ s = spinner();
1549
+ s.start("Creating GitHub repo + pushing");
1550
+ try {
1551
+ created.repoUrl = await ghRepoCreate(opts);
1552
+ s.stop(`GitHub: ${created.repoUrl}`);
1553
+ } catch (err) {
1554
+ s.stop("GitHub: failed");
1555
+ fail("gh repo create", err);
1556
+ }
1557
+ let app;
1558
+ s = spinner();
1559
+ s.start("Creating Amplify Hosting app");
1560
+ try {
1561
+ app = await amplifyCreateApp(opts, created.repoUrl, serviceRoleArn);
1562
+ created.appId = app.appId;
1563
+ s.stop(`Amplify app: ${app.appId}`);
1564
+ } catch (err) {
1565
+ s.stop("Amplify create-app: failed");
1566
+ return fail("aws amplify create-app", err);
1567
+ }
1568
+ s = spinner();
1569
+ s.start("Creating main branch");
1570
+ try {
1571
+ await amplifyCreateBranch(opts, app.appId);
1572
+ s.stop("Amplify branch: main");
1573
+ } catch (err) {
1574
+ s.stop("Amplify create-branch: failed");
1575
+ fail("aws amplify create-branch", err);
1576
+ }
1577
+ s = spinner();
1578
+ s.start("Starting first deployment");
1579
+ try {
1580
+ await amplifyStartJob(opts, app.appId);
1581
+ s.stop("Amplify: first job started");
1582
+ } catch (err) {
1583
+ s.stop("Amplify start-job: failed");
1584
+ fail("aws amplify start-job", err);
1585
+ }
1586
+ const amplifyAppUrl = `https://main.${app.defaultDomain}`;
1587
+ const result = {
1588
+ githubRepoUrl: created.repoUrl,
1589
+ amplifyAppId: app.appId,
1590
+ amplifyAppUrl
1591
+ };
1592
+ if (opts.domain) {
1593
+ s = spinner();
1594
+ s.start(`Associating custom domain ${opts.domain}`);
1595
+ try {
1596
+ const dom = await amplifyCreateDomain(opts, app.appId);
1597
+ result.domainUrl = dom.domainUrl;
1598
+ result.domainVerification = dom.verification;
1599
+ s.stop(`Custom domain queued: ${opts.domain}`);
1600
+ } catch (err) {
1601
+ s.stop("Custom domain: failed");
1602
+ const msg = err instanceof Error ? err.message : String(err);
1603
+ log.warn(`Custom domain step failed:
1604
+ ${msg}`);
1605
+ }
1606
+ }
1607
+ return result;
1608
+ }
1609
+
1610
+ // src/deploy-prompts.ts
1611
+ async function detectGithubLogin() {
1612
+ try {
1613
+ const { stdout } = await execa3("gh", ["api", "user", "--jq", ".login"], { reject: false });
1614
+ const trimmed = stdout?.trim();
1615
+ return trimmed || void 0;
1616
+ } catch {
1617
+ return void 0;
1618
+ }
1619
+ }
1620
+ async function detectAwsRegion() {
1621
+ if (process.env.AWS_REGION?.trim()) return process.env.AWS_REGION.trim();
1622
+ if (process.env.AWS_DEFAULT_REGION?.trim()) return process.env.AWS_DEFAULT_REGION.trim();
1623
+ try {
1624
+ const { stdout } = await execa3("aws", ["configure", "get", "region"], { reject: false });
1625
+ const trimmed = stdout?.trim();
1626
+ return trimmed || void 0;
1627
+ } catch {
1628
+ return void 0;
1629
+ }
1630
+ }
1631
+ async function gatherDeployOptions(args, projectDir, projectName) {
1632
+ p2.log.info("Configuring deploy (GitHub + Amplify Hosting)");
1633
+ let githubOwner = args.githubOwner;
1634
+ if (!githubOwner) {
1635
+ const detected = await detectGithubLogin();
1636
+ const answer = await p2.text({
1637
+ message: "GitHub owner (user or org)",
1638
+ placeholder: detected ?? "your-github-username",
1639
+ defaultValue: detected ?? "",
1640
+ validate: (v) => !v?.trim() ? "GitHub owner is required" : void 0
1641
+ });
1642
+ if (p2.isCancel(answer)) {
1643
+ p2.cancel("Cancelled.");
1644
+ return null;
1645
+ }
1646
+ githubOwner = answer.trim();
1647
+ }
1648
+ let githubPrivate = args.githubPrivate;
1649
+ if (!args.githubPrivate && !args.skipConfirm) {
1650
+ const choice = await p2.select({
1651
+ message: "Repository visibility",
1652
+ options: [
1653
+ { value: "public", label: "Public" },
1654
+ { value: "private", label: "Private" }
1655
+ ],
1656
+ initialValue: "public"
1657
+ });
1658
+ if (p2.isCancel(choice)) {
1659
+ p2.cancel("Cancelled.");
1660
+ return null;
1661
+ }
1662
+ githubPrivate = choice === "private";
1663
+ }
1664
+ let githubToken = await resolveGithubToken(args.githubToken);
1665
+ if (!githubToken) {
1666
+ const answer = await p2.password({
1667
+ message: "GitHub token (needs `repo` scope) \u2014 or run `gh auth login` first",
1668
+ validate: (v) => !v?.trim() ? "Token is required" : void 0
1669
+ });
1670
+ if (p2.isCancel(answer)) {
1671
+ p2.cancel("Cancelled.");
1672
+ return null;
1673
+ }
1674
+ githubToken = answer.trim();
1675
+ }
1676
+ let awsRegion = args.awsRegion;
1677
+ if (!awsRegion) {
1678
+ const detected = await detectAwsRegion();
1679
+ const answer = await p2.text({
1680
+ message: "AWS region",
1681
+ placeholder: detected ?? "us-east-1",
1682
+ defaultValue: detected ?? "us-east-1"
1683
+ });
1684
+ if (p2.isCancel(answer)) {
1685
+ p2.cancel("Cancelled.");
1686
+ return null;
1687
+ }
1688
+ awsRegion = answer.trim() || detected || "us-east-1";
1689
+ }
1690
+ let domain = args.domain;
1691
+ let subdomain = args.subdomain;
1692
+ if (!domain) {
1693
+ const wantDomain = await p2.confirm({
1694
+ message: "Attach a custom domain?",
1695
+ initialValue: false
1696
+ });
1697
+ if (p2.isCancel(wantDomain)) {
1698
+ p2.cancel("Cancelled.");
1699
+ return null;
1700
+ }
1701
+ if (wantDomain) {
1702
+ const dom = await p2.text({
1703
+ message: "Domain (apex or subdomain, e.g. example.com or blog.example.com)",
1704
+ validate: (v) => {
1705
+ if (!v?.trim()) return "Domain is required";
1706
+ if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(v.trim())) return "Looks invalid";
1707
+ }
1708
+ });
1709
+ if (p2.isCancel(dom)) {
1710
+ p2.cancel("Cancelled.");
1711
+ return null;
1712
+ }
1713
+ domain = dom.trim();
1714
+ if (!subdomain) {
1715
+ const sub = await p2.text({
1716
+ message: "Subdomain prefix (leave blank for apex)",
1717
+ placeholder: "",
1718
+ defaultValue: ""
1719
+ });
1720
+ if (p2.isCancel(sub)) {
1721
+ p2.cancel("Cancelled.");
1722
+ return null;
1723
+ }
1724
+ subdomain = sub.trim() || void 0;
1725
+ }
1726
+ }
1727
+ }
1728
+ if (!args.skipConfirm) {
1729
+ const proceed = await p2.confirm({
1730
+ message: `Proceed?
1731
+ GitHub: ${githubOwner}/${projectName} (${githubPrivate ? "private" : "public"})
1732
+ Region: ${awsRegion}` + (domain ? `
1733
+ Domain: ${subdomain ? `${subdomain}.` : ""}${domain}` : ""),
1734
+ initialValue: true
1735
+ });
1736
+ if (p2.isCancel(proceed) || !proceed) {
1737
+ p2.cancel("Cancelled.");
1738
+ return null;
1739
+ }
1740
+ }
1741
+ return {
1742
+ projectDir,
1743
+ projectName,
1744
+ githubOwner,
1745
+ githubPrivate,
1746
+ githubToken,
1747
+ awsProfile: args.awsProfile,
1748
+ awsRegion,
1749
+ domain,
1750
+ subdomain,
1751
+ iamServiceRoleArn: args.iamServiceRole,
1752
+ createIamRole: args.createIamRole,
1753
+ skipConfirm: args.skipConfirm
1754
+ };
1755
+ }
1756
+
1757
+ // src/upgrade.ts
1758
+ import { cp as cp2, readFile as readFile3, writeFile as writeFile3, readdir as readdir3, mkdir as mkdir2, rm as rm2 } from "fs/promises";
1759
+ import { existsSync as existsSync5 } from "fs";
1760
+ import { join as join3, relative, extname as extname2, dirname } from "path";
1761
+ import { log as log3, outro } from "@clack/prompts";
1762
+ import pc3 from "picocolors";
1763
+ import { execa as execa4 } from "execa";
1764
+ var AMPLESS_PACKAGES = /* @__PURE__ */ new Set([
1765
+ "ampless",
1766
+ "@ampless/admin",
1767
+ "@ampless/backend",
1768
+ "@ampless/runtime",
1769
+ "@ampless/plugin-seo",
1770
+ "@ampless/plugin-rss",
1771
+ "@ampless/plugin-webhook",
1772
+ "@ampless/plugin-og-image"
1773
+ ]);
1774
+ var AMPLESS_MANAGED_SCRIPTS = /* @__PURE__ */ new Set([
1775
+ "sandbox",
1776
+ "sandbox:dev",
1777
+ "update-ampless",
1778
+ "copy-theme"
1779
+ ]);
1780
+ var PROTECTED_PATTERNS = [
1781
+ /^cms\.config\.ts$/,
1782
+ // `themes/` and `themes-registry.ts` are handled separately — see
1783
+ // syncThemes() below. The themes directory is owned per-entry by
1784
+ // either ampless (default themes, resynced on upgrade) or the user
1785
+ // (`my-*` themes, preserved). The registry is regenerated to match
1786
+ // whatever is on disk afterward.
1787
+ /^\.env/,
1788
+ /^node_modules(\/|$)/,
1789
+ /^\.next(\/|$)/,
1790
+ /^\.turbo(\/|$)/,
1791
+ /^\.amplify(\/|$)/,
1792
+ /^amplify_outputs\.json$/,
1793
+ /^next-env\.d\.ts$/,
1794
+ /^tsconfig\.tsbuildinfo$/,
1795
+ /^pnpm-lock\.yaml$/,
1796
+ /^package-lock\.json$/,
1797
+ // Anything under themes/ is handled by syncThemes; the file-walk
1798
+ // classifier must not double-process it.
1799
+ /^themes(\/|$)/,
1800
+ /^themes-registry\.ts$/
1801
+ ];
1802
+ var SEED_IF_MISSING_PATTERN = /\.custom\.ts$/;
1803
+ var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
1804
+ ".json",
1805
+ ".md",
1806
+ ".ts",
1807
+ ".tsx",
1808
+ ".js",
1809
+ ".jsx",
1810
+ ".mjs",
1811
+ ".cjs",
1812
+ ".html",
1813
+ ".css",
1814
+ ".env",
1815
+ ".txt",
1816
+ ".yaml",
1817
+ ".yml",
1818
+ ".toml",
1819
+ ".gitignore"
1820
+ ]);
1821
+ function isProtected(relPath) {
1822
+ return PROTECTED_PATTERNS.some((re) => re.test(relPath));
1823
+ }
1824
+ async function listShippedThemes(templatesRoot) {
1825
+ if (!existsSync5(templatesRoot)) return [];
1826
+ const entries = await readdir3(templatesRoot, { withFileTypes: true });
1827
+ return entries.filter((e) => e.isDirectory() && e.name !== "_shared").map((e) => e.name).sort();
1828
+ }
1829
+ var USER_OWNED_THEME_FILES = /* @__PURE__ */ new Set(["README.md", ".gitignore"]);
1830
+ function isUserOwnedThemeFile(path) {
1831
+ const name = path.split(/[/\\]/).pop() ?? "";
1832
+ return USER_OWNED_THEME_FILES.has(name);
1833
+ }
1834
+ async function captureUserOwnedFiles(dir) {
1835
+ const out = /* @__PURE__ */ new Map();
1836
+ if (!existsSync5(dir)) return out;
1837
+ for (const name of USER_OWNED_THEME_FILES) {
1838
+ const p3 = join3(dir, name);
1839
+ if (existsSync5(p3)) {
1840
+ out.set(name, await readFile3(p3));
1841
+ }
1842
+ }
1843
+ return out;
1844
+ }
1845
+ async function restorePreservedFiles(dir, files) {
1846
+ for (const [name, content] of files) {
1847
+ await mkdir2(dir, { recursive: true });
1848
+ await writeFile3(join3(dir, name), content);
1849
+ }
1850
+ }
1851
+ async function syncThemes(destDir, templatesRoot) {
1852
+ const shipped = await listShippedThemes(templatesRoot);
1853
+ const themesDir = join3(destDir, "themes");
1854
+ await mkdir2(themesDir, { recursive: true });
1855
+ for (const name of shipped) {
1856
+ const src = join3(templatesRoot, name);
1857
+ const dst = join3(themesDir, name);
1858
+ const preservedFiles = await captureUserOwnedFiles(dst);
1859
+ await rm2(dst, { recursive: true, force: true });
1860
+ await cp2(src, dst, {
1861
+ recursive: true,
1862
+ filter: (sourcePath) => !isUserOwnedThemeFile(sourcePath)
1863
+ });
1864
+ await restorePreservedFiles(dst, preservedFiles);
1865
+ }
1866
+ const installed = await discoverInstalledThemes(destDir);
1867
+ const shippedSet = new Set(shipped);
1868
+ const preserved = installed.filter((t) => !shippedSet.has(t));
1869
+ const all = [...shipped, ...preserved];
1870
+ await writeFile3(join3(destDir, "themes-registry.ts"), buildRegistry(all), "utf-8");
1871
+ return { synced: shipped, preserved };
1872
+ }
1873
+ async function walkDir(dir, base, out) {
1874
+ const entries = await readdir3(dir, { withFileTypes: true });
1875
+ for (const entry of entries) {
1876
+ const full = join3(dir, entry.name);
1877
+ const rel = relative(base, full);
1878
+ if (entry.isDirectory()) {
1879
+ await walkDir(full, base, out);
1880
+ } else {
1881
+ out.push(rel);
1882
+ }
1883
+ }
1884
+ }
1885
+ function substituteVars(content, vars) {
1886
+ return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
1887
+ }
1888
+ async function copyWithSubstitution(src, dst, vars) {
1889
+ const ext = extname2(src) || (src.endsWith(".gitignore") ? ".gitignore" : "");
1890
+ await mkdir2(dirname(dst), { recursive: true });
1891
+ if (TEXT_EXTENSIONS2.has(ext)) {
1892
+ const content = await readFile3(src, "utf-8");
1893
+ await writeFile3(dst, substituteVars(content, vars), "utf-8");
1894
+ } else {
1895
+ const buf = await readFile3(src);
1896
+ await writeFile3(dst, buf);
1897
+ }
1898
+ }
1899
+ function detectIndent(jsonStr) {
1900
+ const m = jsonStr.match(/^{\n(\s+)/);
1901
+ if (!m) return 2;
1902
+ return m[1].length;
1903
+ }
1904
+ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
1905
+ const derivedRoot = opts.templatesRoot ?? join3(sharedDir, "..");
1906
+ const themeSyncEnabled = existsSync5(join3(derivedRoot, "_shared"));
1907
+ const templatesRoot = themeSyncEnabled ? derivedRoot : "";
1908
+ const problem = validateMountableProject(destDir);
1909
+ if (problem) {
1910
+ throw new Error(problem);
1911
+ }
1912
+ const projectPkgPath = join3(destDir, "package.json");
1913
+ const projectPkgRaw = await readFile3(projectPkgPath, "utf-8");
1914
+ const projectPkg = JSON.parse(projectPkgRaw);
1915
+ const projectName = typeof projectPkg.name === "string" && projectPkg.name ? projectPkg.name : "my-ampless-site";
1916
+ const vars = { projectName };
1917
+ const allRelPaths = [];
1918
+ await walkDir(sharedDir, sharedDir, allRelPaths);
1919
+ const classification = { replace: [], merge: [], seed: [], protected: [] };
1920
+ for (const rel of allRelPaths) {
1921
+ if (SEED_IF_MISSING_PATTERN.test(rel)) {
1922
+ classification.seed.push(rel);
1923
+ } else if (isProtected(rel)) {
1924
+ classification.protected.push(rel);
1925
+ } else if (rel === "package.json") {
1926
+ classification.merge.push(rel);
1927
+ } else {
1928
+ classification.replace.push(rel);
1929
+ }
1930
+ }
1931
+ const replaceNew = classification.replace.filter((r) => !existsSync5(join3(destDir, r)));
1932
+ const replaceUpdate = classification.replace.filter((r) => existsSync5(join3(destDir, r)));
1933
+ const seedNew = classification.seed.filter((r) => !existsSync5(join3(destDir, r)));
1934
+ const seedSkipped = classification.seed.filter((r) => existsSync5(join3(destDir, r)));
1935
+ const shippedThemes = themeSyncEnabled ? await listShippedThemes(templatesRoot) : [];
1936
+ const existingThemes = themeSyncEnabled ? await discoverInstalledThemes(destDir) : [];
1937
+ const preservedThemes = existingThemes.filter((t) => !shippedThemes.includes(t));
1938
+ log3.info(
1939
+ `replace: ${pc3.green(`\u8FFD\u52A0 ${replaceNew.length}`)} / ${pc3.yellow(`\u66F4\u65B0 ${replaceUpdate.length}`)}`
1940
+ );
1941
+ log3.info(`merge: ${pc3.cyan("package.json: ampless deps / managed scripts \u3092\u30C6\u30F3\u30D7\u30EC\u5074\u306B\u5408\u308F\u305B\u308B")}`);
1942
+ if (classification.seed.length > 0) {
1943
+ log3.info(
1944
+ `seed: ${pc3.green(`\u8FFD\u52A0 ${seedNew.length}`)} / ${pc3.dim(`\u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u306F\u4E0A\u66F8\u304D\u3057\u306A\u3044: ${seedSkipped.length}`)} (*.custom.ts)`
1945
+ );
1946
+ }
1947
+ if (themeSyncEnabled) {
1948
+ log3.info(
1949
+ `themes: ${pc3.cyan(`\u30C7\u30D5\u30A9\u30EB\u30C8\u30C6\u30FC\u30DE\u3092\u540C\u671F: ${shippedThemes.length}`)} / ${pc3.dim(`\u30AB\u30B9\u30BF\u30E0\u30C6\u30FC\u30DE (my-*) \u3092\u4FDD\u6301: ${preservedThemes.length}`)}`
1950
+ );
1951
+ }
1952
+ log3.info(`protected: ${pc3.dim(`\u30C6\u30F3\u30D7\u30EC\u306B\u5B58\u5728\u3059\u308B\u304C\u89E6\u3089\u306A\u3044: ${classification.protected.length} \u500B`)}`);
1953
+ if (opts.dryRun) {
1954
+ return {
1955
+ added: replaceNew,
1956
+ updated: replaceUpdate,
1957
+ seeded: seedNew,
1958
+ protected: classification.protected,
1959
+ themesSynced: shippedThemes,
1960
+ themesPreserved: preservedThemes,
1961
+ packageJsonMerged: false
1962
+ };
1963
+ }
1964
+ for (const rel of classification.replace) {
1965
+ const src = join3(sharedDir, rel);
1966
+ const dst = join3(destDir, rel);
1967
+ await copyWithSubstitution(src, dst, vars);
1968
+ }
1969
+ for (const rel of seedNew) {
1970
+ const src = join3(sharedDir, rel);
1971
+ const dst = join3(destDir, rel);
1972
+ await copyWithSubstitution(src, dst, vars);
1973
+ }
1974
+ const themeResult = themeSyncEnabled ? await syncThemes(destDir, templatesRoot) : { synced: [], preserved: [] };
1975
+ const templatePkgRaw = await readFile3(join3(sharedDir, "package.json"), "utf-8");
1976
+ const templatePkg = JSON.parse(templatePkgRaw);
1977
+ const indent = detectIndent(projectPkgRaw);
1978
+ for (const section of ["dependencies", "devDependencies"]) {
1979
+ const templateDeps = templatePkg[section] ?? {};
1980
+ const projectDeps = projectPkg[section] ?? {};
1981
+ for (const [name, version] of Object.entries(templateDeps)) {
1982
+ if (!AMPLESS_PACKAGES.has(name)) continue;
1983
+ projectDeps[name] = version;
1984
+ }
1985
+ if (Object.keys(projectDeps).length > 0) {
1986
+ projectPkg[section] = projectDeps;
1987
+ }
1988
+ }
1989
+ const templateScripts = templatePkg["scripts"] ?? {};
1990
+ const projectScripts = projectPkg["scripts"] ?? {};
1991
+ for (const [name, body] of Object.entries(templateScripts)) {
1992
+ if (!AMPLESS_MANAGED_SCRIPTS.has(name)) continue;
1993
+ projectScripts[name] = body;
1994
+ }
1995
+ if (Object.keys(projectScripts).length > 0) {
1996
+ projectPkg["scripts"] = projectScripts;
1997
+ }
1998
+ await writeFile3(projectPkgPath, JSON.stringify(projectPkg, null, indent) + "\n", "utf-8");
1999
+ if (!opts.noInstall) {
2000
+ const usePnpm = existsSync5(join3(destDir, "pnpm-lock.yaml"));
2001
+ const pm = usePnpm ? "pnpm" : "npm";
2002
+ await execa4(pm, ["install"], { cwd: destDir, stdio: "inherit" });
2003
+ }
2004
+ return {
2005
+ added: replaceNew,
2006
+ updated: replaceUpdate,
2007
+ seeded: seedNew,
2008
+ protected: classification.protected,
2009
+ themesSynced: themeResult.synced,
2010
+ themesPreserved: themeResult.preserved,
2011
+ packageJsonMerged: true
2012
+ };
2013
+ }
2014
+ async function runUpgrade(args) {
2015
+ const destDir = process.cwd();
2016
+ try {
2017
+ await runUpgradeIn(destDir, sharedTemplateDir(), {
2018
+ dryRun: args.dryRun,
2019
+ noInstall: args.noInstall
2020
+ });
2021
+ } catch (err) {
2022
+ log3.error(err instanceof Error ? err.message : String(err));
2023
+ process.exit(1);
2024
+ }
2025
+ if (args.dryRun) {
2026
+ outro(`${pc3.dim("(dry-run) No files were changed.")}`);
2027
+ return;
2028
+ }
2029
+ outro(
2030
+ `${pc3.green("\u2714")} Upgrade complete
2031
+
2032
+ Next steps:
2033
+ ${pc3.cyan("git diff")} ${pc3.dim("# review changes")}
2034
+ ${pc3.cyan("git commit && git push")} ${pc3.dim("# deploy via Amplify Hosting")}`
2035
+ );
2036
+ }
2037
+
2038
+ // src/copy-theme.ts
2039
+ import { cp as cp3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2040
+ import { existsSync as existsSync6 } from "fs";
2041
+ import { join as join4 } from "path";
2042
+ import { log as log4, outro as outro2 } from "@clack/prompts";
2043
+ import pc4 from "picocolors";
2044
+ async function runCopyThemeIn(destDir, source, target) {
2045
+ const problem = validateMountableProject(destDir);
2046
+ if (problem) {
2047
+ throw new Error(problem);
2048
+ }
2049
+ if (!isCustomTheme(target)) {
2050
+ throw new Error(
2051
+ `Target theme name must start with "${CUSTOM_THEME_PREFIX}" (got "${target}"). This prefix marks the theme as user-owned so create-ampless upgrade leaves it alone.`
2052
+ );
2053
+ }
2054
+ if (source === target) {
2055
+ throw new Error(`Source and target are identical (${source}).`);
2056
+ }
2057
+ const themesDir = join4(destDir, "themes");
2058
+ const sourceDir = join4(themesDir, source);
2059
+ const targetDir = join4(themesDir, target);
2060
+ if (!existsSync6(sourceDir)) {
2061
+ throw new Error(`Source theme not found: themes/${source}/`);
2062
+ }
2063
+ if (existsSync6(targetDir)) {
2064
+ throw new Error(`Target theme already exists: themes/${target}/`);
2065
+ }
2066
+ await cp3(sourceDir, targetDir, { recursive: true });
2067
+ const filesRewritten = await rewriteThemeName(targetDir, source, target);
2068
+ const installed = await discoverInstalledThemes(destDir);
2069
+ await writeFile4(join4(destDir, "themes-registry.ts"), buildRegistry(installed), "utf-8");
2070
+ return { source, target, filesRewritten };
2071
+ }
2072
+ async function rewriteThemeName(targetDir, source, target) {
2073
+ const touched = [];
2074
+ await rewriteFile(
2075
+ join4(targetDir, "index.ts"),
2076
+ (s) => s.replace(new RegExp(`name:\\s*'${escapeRegex(source)}'`), `name: '${target}'`).replace(new RegExp(`name:\\s*"${escapeRegex(source)}"`), `name: "${target}"`),
2077
+ touched
2078
+ );
2079
+ await rewriteFile(
2080
+ join4(targetDir, "manifest.ts"),
2081
+ (s) => s.replace(new RegExp(`name:\\s*'${escapeRegex(source)}'`), `name: '${target}'`).replace(new RegExp(`name:\\s*"${escapeRegex(source)}"`), `name: "${target}"`),
2082
+ touched
2083
+ );
2084
+ await rewriteFile(
2085
+ join4(targetDir, "tokens.css"),
2086
+ (s) => s.replace(new RegExp(`\\[data-theme='${escapeRegex(source)}'\\]`, "g"), `[data-theme='${target}']`).replace(new RegExp(`\\[data-theme="${escapeRegex(source)}"\\]`, "g"), `[data-theme="${target}"]`),
2087
+ touched
2088
+ );
2089
+ return touched;
2090
+ }
2091
+ async function rewriteFile(path, transform, touched) {
2092
+ if (!existsSync6(path)) return;
2093
+ const before = await readFile4(path, "utf-8");
2094
+ const after = transform(before);
2095
+ if (after === before) return;
2096
+ await writeFile4(path, after, "utf-8");
2097
+ touched.push(path);
2098
+ }
2099
+ function escapeRegex(s) {
2100
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2101
+ }
2102
+ async function runCopyTheme(args) {
2103
+ const destDir = process.cwd();
2104
+ const source = args.copyThemeSource;
2105
+ const target = args.copyThemeTarget;
2106
+ if (!source || !target) {
2107
+ log4.error("Usage: npx create-ampless@latest copy-theme <source> <target>");
2108
+ log4.info("Example: npx create-ampless@latest copy-theme blog my-blog");
2109
+ process.exit(1);
2110
+ }
2111
+ try {
2112
+ const result = await runCopyThemeIn(destDir, source, target);
2113
+ outro2(
2114
+ `${pc4.green("\u2714")} Copied themes/${result.source}/ \u2192 themes/${result.target}/
2115
+
2116
+ Files rewritten:
2117
+ ` + result.filesRewritten.map((f) => ` ${pc4.dim(f)}`).join("\n") + `
2118
+
2119
+ themes-registry.ts updated. Next:
2120
+ ${pc4.cyan(`Open themes/${result.target}/ and start customising`)}
2121
+ ${pc4.cyan(`Activate via /admin/sites/<siteId>/theme`)}`
2122
+ );
2123
+ } catch (err) {
2124
+ log4.error(err instanceof Error ? err.message : String(err));
2125
+ process.exit(1);
2126
+ }
2127
+ }
2128
+
2129
+ // src/index.ts
2130
+ import pc5 from "picocolors";
2131
+ var DEFAULT_THEMES = VALID_THEMES;
2132
+ function buildNonInteractiveOpts(args) {
2133
+ const projectName = args.projectName ?? (() => {
2134
+ const b = basename3(process.cwd());
2135
+ return b && b !== "/" ? b : "my-ampless-site";
2136
+ })();
2137
+ const siteName = args.siteName ?? "My Blog";
2138
+ const themes = args.themes ?? [...DEFAULT_THEMES];
2139
+ const plugins = args.plugins ?? ["seo"];
2140
+ return {
2141
+ projectName,
2142
+ siteName,
2143
+ themes,
2144
+ defaultTheme: themes[0],
2145
+ plugins
2146
+ };
2147
+ }
2148
+ function warnIgnoredScaffoldFlags(args) {
2149
+ const ignored = [];
2150
+ if (args.siteName) ignored.push("--site-name");
2151
+ if (args.themes) ignored.push("--themes");
2152
+ if (args.plugins) ignored.push("--plugins");
2153
+ if (args.projectName) ignored.push("<project-name> positional");
2154
+ if (ignored.length > 0) {
2155
+ log5.warn(`--mount mode ignores: ${ignored.join(", ")}`);
2156
+ }
2157
+ }
2158
+ async function runMount(args) {
2159
+ const destDir = process.cwd();
2160
+ const projectName = basename3(destDir);
2161
+ warnIgnoredScaffoldFlags(args);
2162
+ const problem = validateMountableProject(destDir);
2163
+ if (problem) {
2164
+ log5.error(problem);
2165
+ log5.info(
2166
+ "Run `npx create-ampless@latest <name>` first to scaffold, or `cd` into a scaffolded project before passing --mount."
2167
+ );
2168
+ process.exit(1);
2169
+ }
2170
+ const deployOpts = await gatherDeployOptions(args, destDir, projectName);
2171
+ if (!deployOpts) return;
2172
+ try {
2173
+ const result = await runDeploy({ ...deployOpts, mount: true });
2174
+ printDeployResult(result);
2175
+ } catch (err) {
2176
+ if (err instanceof PreflightError) {
2177
+ process.exit(1);
2178
+ }
2179
+ log5.error(err instanceof Error ? err.message : String(err));
2180
+ process.exit(1);
2181
+ }
2182
+ }
2183
+ function printDeployResult(result) {
2184
+ const lines = [
2185
+ `${pc5.green("\u2714")} Project deployed`,
2186
+ ``,
2187
+ ` GitHub: ${pc5.cyan(result.githubRepoUrl)}`,
2188
+ ` Amplify app: ${pc5.cyan(result.amplifyAppId)}`,
2189
+ ` Amplify URL: ${pc5.cyan(result.amplifyAppUrl)}`
2190
+ ];
2191
+ if (result.domainUrl) {
2192
+ lines.push(` Custom domain: ${pc5.cyan(result.domainUrl)}`);
2193
+ }
2194
+ if (result.domainVerification && result.domainVerification.length > 0) {
2195
+ lines.push("", ` ${pc5.bold("Add these DNS records to verify the domain:")}`);
2196
+ for (const v of result.domainVerification) {
2197
+ lines.push(` ${v.cname} CNAME ${v.target}`);
2198
+ }
2199
+ }
2200
+ lines.push(
2201
+ "",
2202
+ ` First build is now running in Amplify Hosting.`,
2203
+ ` Watch it at ${pc5.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
2204
+ );
2205
+ outro3(lines.join("\n"));
2206
+ }
196
2207
  async function main() {
197
- const argProjectName = process.argv[2];
198
- const opts = await runPrompts(argProjectName);
2208
+ const args = parseDeployArgs(process.argv.slice(2));
2209
+ if (args.help) {
2210
+ process.stdout.write(HELP_TEXT);
2211
+ return;
2212
+ }
2213
+ for (const flag of args.unknown) {
2214
+ log5.warn(`Unknown argument ignored: ${flag}`);
2215
+ }
2216
+ if (args.upgrade) {
2217
+ await runUpgrade(args);
2218
+ return;
2219
+ }
2220
+ if (args.copyTheme) {
2221
+ await runCopyTheme(args);
2222
+ return;
2223
+ }
2224
+ if (args.mount) {
2225
+ await runMount(args);
2226
+ return;
2227
+ }
2228
+ let opts;
2229
+ if (args.skipConfirm) {
2230
+ opts = buildNonInteractiveOpts(args);
2231
+ } else {
2232
+ opts = await runPrompts(args.projectName);
2233
+ }
199
2234
  if (!opts) return;
200
- const destDir = resolve3(process.cwd(), opts.projectName);
201
- if (existsSync2(destDir)) {
202
- log.error(`Directory already exists: ${destDir}`);
2235
+ const destDir = resolve5(process.cwd(), opts.projectName);
2236
+ if (existsSync7(destDir)) {
2237
+ log5.error(`Directory already exists: ${destDir}`);
203
2238
  process.exit(1);
204
2239
  }
205
2240
  const sharedDir = sharedTemplateDir();
206
- const s = spinner();
2241
+ const s = spinner2();
207
2242
  s.start("Scaffolding project...");
208
2243
  try {
209
2244
  await scaffold(sharedDir, templatesDir, destDir, opts);
210
2245
  s.stop("Done!");
211
2246
  } catch (err) {
212
2247
  s.stop("Failed.");
213
- log.error(String(err));
2248
+ log5.error(String(err));
214
2249
  process.exit(1);
215
2250
  }
216
- outro(
217
- `${pc.green("\u2714")} Project created at ${pc.bold(opts.projectName)}
2251
+ if (args.deploy) {
2252
+ const deployOpts = await gatherDeployOptions(args, destDir, opts.projectName);
2253
+ if (!deployOpts) return;
2254
+ try {
2255
+ const result = await runDeploy(deployOpts);
2256
+ printDeployResult(result);
2257
+ } catch (err) {
2258
+ if (err instanceof PreflightError) {
2259
+ process.exit(1);
2260
+ }
2261
+ log5.error(err instanceof Error ? err.message : String(err));
2262
+ process.exit(1);
2263
+ }
2264
+ return;
2265
+ }
2266
+ outro3(
2267
+ `${pc5.green("\u2714")} Project created at ${pc5.bold(opts.projectName)}
218
2268
 
219
2269
  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")}`
2270
+ ${pc5.cyan("cd")} ${opts.projectName}
2271
+ ${pc5.cyan("npm install")}
2272
+ ${pc5.cyan("npx ampx sandbox")} ${pc5.dim("# start Amplify backend")}
2273
+ ${pc5.cyan("npm run dev")} ${pc5.dim("# start Next.js")}`
224
2274
  );
225
2275
  }
226
2276
  main().catch((err) => {