create-ampless 0.2.0-alpha.9 → 1.0.0-alpha.100

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 (137) hide show
  1. package/README.ja.md +77 -0
  2. package/README.md +6 -3
  3. package/dist/index.js +743 -84
  4. package/dist/templates/_shared/AGENTS.ja.md +94 -0
  5. package/dist/templates/_shared/AGENTS.md +94 -0
  6. package/dist/templates/_shared/README.ja.md +239 -0
  7. package/dist/templates/_shared/README.md +239 -0
  8. package/dist/templates/_shared/RUNBOOK.ja.md +120 -0
  9. package/dist/templates/_shared/RUNBOOK.md +32 -58
  10. package/dist/templates/_shared/THEMES.ja.md +495 -0
  11. package/dist/templates/_shared/THEMES.md +523 -0
  12. package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +7 -0
  13. package/dist/templates/_shared/amplify/backend.ts +10 -0
  14. package/dist/templates/_shared/amplify/data/get-media-by-src.js +45 -0
  15. package/dist/templates/_shared/amplify/data/get-published-post.js +9 -12
  16. package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +6 -9
  17. package/dist/templates/_shared/amplify/data/list-published-posts.js +10 -11
  18. package/dist/templates/_shared/amplify/data/resource.custom.ts +1 -2
  19. package/dist/templates/_shared/amplify/data/resource.ts +28 -7
  20. package/dist/templates/_shared/amplify/events/dispatcher/resource.ts +1 -0
  21. package/dist/templates/_shared/amplify/events/processor-trusted/resource.ts +1 -0
  22. package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +5 -0
  23. package/dist/templates/_shared/amplify/functions/api-key-renewer/resource.ts +1 -0
  24. package/dist/templates/_shared/amplify/functions/mcp-handler/handler.ts +1 -0
  25. package/dist/templates/_shared/amplify/functions/mcp-handler/resource.ts +12 -0
  26. package/dist/templates/_shared/amplify/functions/plugin-secret-handler/handler.ts +11 -0
  27. package/dist/templates/_shared/amplify/functions/plugin-secret-handler/resource.ts +12 -0
  28. package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +7 -0
  29. package/dist/templates/_shared/amplify/secrets/.gitkeep +0 -0
  30. package/dist/templates/_shared/amplify/secrets/encryption-key.ts +9 -0
  31. package/dist/templates/_shared/app/(admin)/admin/mcp-tokens/page.tsx +5 -0
  32. package/dist/templates/_shared/app/(admin)/admin/plugins/page.tsx +5 -0
  33. package/dist/templates/_shared/app/globals.css +55 -39
  34. package/dist/templates/_shared/app/layout.tsx +51 -16
  35. package/dist/templates/_shared/app/providers.tsx +0 -2
  36. package/dist/templates/_shared/app/raw/[slug]/route.ts +10 -0
  37. package/dist/templates/_shared/app/static/[slug]/[[...path]]/route.ts +14 -0
  38. package/dist/templates/_shared/cms.config.ts +64 -23
  39. package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +3 -4
  40. package/dist/templates/_shared/components/tag-list.tsx +9 -2
  41. package/dist/templates/_shared/components.json +1 -1
  42. package/dist/templates/_shared/docs/plugin-author-guide.ja.md +934 -0
  43. package/dist/templates/_shared/docs/plugin-author-guide.md +1336 -0
  44. package/dist/templates/_shared/lib/admin.ts +12 -12
  45. package/dist/templates/_shared/lib/ampless.ts +2 -8
  46. package/dist/templates/_shared/lib/amplify.ts +0 -5
  47. package/dist/templates/_shared/package.json +51 -43
  48. package/dist/templates/_shared/plugins/README.ja.md +139 -0
  49. package/dist/templates/_shared/plugins/README.md +143 -0
  50. package/dist/templates/_shared/proxy.ts +23 -8
  51. package/dist/templates/blog/README.ja.md +22 -0
  52. package/dist/templates/blog/README.md +17 -47
  53. package/dist/templates/blog/manifest.ts +1 -1
  54. package/dist/templates/blog/pages/feed.ts +4 -5
  55. package/dist/templates/blog/pages/home.tsx +10 -15
  56. package/dist/templates/blog/pages/post.tsx +42 -14
  57. package/dist/templates/blog/pages/sitemap.ts +4 -5
  58. package/dist/templates/blog/pages/tag.tsx +8 -10
  59. package/dist/templates/blog/tokens.css +26 -40
  60. package/dist/templates/corporate/README.ja.md +18 -0
  61. package/dist/templates/corporate/README.md +12 -14
  62. package/dist/templates/corporate/pages/feed.ts +3 -4
  63. package/dist/templates/corporate/pages/home.tsx +7 -12
  64. package/dist/templates/corporate/pages/post.tsx +20 -14
  65. package/dist/templates/corporate/pages/sitemap.ts +3 -4
  66. package/dist/templates/corporate/pages/tag.tsx +8 -10
  67. package/dist/templates/corporate/tokens.css +17 -39
  68. package/dist/templates/dads/README.ja.md +31 -0
  69. package/dist/templates/dads/README.md +13 -17
  70. package/dist/templates/dads/pages/feed.ts +3 -4
  71. package/dist/templates/dads/pages/home.tsx +7 -12
  72. package/dist/templates/dads/pages/post.tsx +20 -14
  73. package/dist/templates/dads/pages/sitemap.ts +3 -4
  74. package/dist/templates/dads/pages/tag.tsx +8 -10
  75. package/dist/templates/dads/tokens.css +22 -42
  76. package/dist/templates/docs/README.ja.md +24 -0
  77. package/dist/templates/docs/README.md +10 -13
  78. package/dist/templates/docs/pages/feed.ts +3 -4
  79. package/dist/templates/docs/pages/home.tsx +6 -9
  80. package/dist/templates/docs/pages/post.tsx +19 -13
  81. package/dist/templates/docs/pages/sitemap.ts +3 -4
  82. package/dist/templates/docs/pages/tag.tsx +8 -10
  83. package/dist/templates/docs/tokens.css +17 -39
  84. package/dist/templates/landing/README.ja.md +20 -0
  85. package/dist/templates/landing/README.md +14 -19
  86. package/dist/templates/landing/pages/feed.ts +4 -5
  87. package/dist/templates/landing/pages/home.tsx +7 -12
  88. package/dist/templates/landing/pages/post.tsx +20 -14
  89. package/dist/templates/landing/pages/sitemap.ts +3 -4
  90. package/dist/templates/landing/pages/tag.tsx +8 -10
  91. package/dist/templates/landing/tokens.css +17 -39
  92. package/dist/templates/minimal/README.ja.md +14 -0
  93. package/dist/templates/minimal/README.md +9 -47
  94. package/dist/templates/minimal/pages/feed.ts +4 -5
  95. package/dist/templates/minimal/pages/home.tsx +6 -8
  96. package/dist/templates/minimal/pages/post.tsx +19 -12
  97. package/dist/templates/minimal/pages/sitemap.ts +4 -5
  98. package/dist/templates/minimal/pages/tag.tsx +7 -8
  99. package/dist/templates/minimal/tokens.css +17 -39
  100. package/dist/templates/plugin-local/README.md +34 -0
  101. package/dist/templates/plugin-local/index.ts +39 -0
  102. package/dist/templates/plugin-standalone/CHANGELOG.md +1 -0
  103. package/dist/templates/plugin-standalone/README.ja.md +43 -0
  104. package/dist/templates/plugin-standalone/README.md +43 -0
  105. package/dist/templates/plugin-standalone/package.json +47 -0
  106. package/dist/templates/plugin-standalone/src/index.test.ts +16 -0
  107. package/dist/templates/plugin-standalone/src/index.ts +29 -0
  108. package/dist/templates/plugin-standalone/tsconfig.json +16 -0
  109. package/dist/templates/plugin-standalone/tsup.config.ts +8 -0
  110. package/package.json +1 -1
  111. package/dist/templates/_shared/app/(admin)/admin/sites/page.tsx +0 -5
  112. package/dist/templates/_shared/app/site/[siteId]/raw/[slug]/route.ts +0 -5
  113. package/dist/templates/_shared/components/i18n-provider.tsx +0 -15
  114. package/dist/templates/_shared/lib/admin-site-client.ts +0 -10
  115. package/dist/templates/_shared/lib/admin-site.ts +0 -12
  116. package/dist/templates/_shared/lib/amplify-server.ts +0 -7
  117. package/dist/templates/_shared/lib/auth-server.ts +0 -15
  118. package/dist/templates/_shared/lib/cn.ts +0 -5
  119. package/dist/templates/_shared/lib/i18n.ts +0 -35
  120. package/dist/templates/_shared/lib/kv-provider.ts +0 -7
  121. package/dist/templates/_shared/lib/media.ts +0 -6
  122. package/dist/templates/_shared/lib/posts-provider.ts +0 -7
  123. package/dist/templates/_shared/lib/posts-public.ts +0 -27
  124. package/dist/templates/_shared/lib/posts.ts +0 -12
  125. package/dist/templates/_shared/lib/seo.ts +0 -11
  126. package/dist/templates/_shared/lib/site-settings.ts +0 -11
  127. package/dist/templates/_shared/lib/storage.ts +0 -10
  128. package/dist/templates/_shared/lib/theme-actions.ts +0 -5
  129. package/dist/templates/_shared/lib/theme-active.ts +0 -10
  130. package/dist/templates/_shared/lib/theme-config.ts +0 -12
  131. package/dist/templates/_shared/lib/upload.ts +0 -6
  132. /package/dist/templates/_shared/app/{site/[siteId]/[slug] → [slug]}/page.tsx +0 -0
  133. /package/dist/templates/_shared/app/{site/[siteId]/feed.xml → feed.xml}/route.ts +0 -0
  134. /package/dist/templates/_shared/app/{site/[siteId]/og → og}/[slug]/route.ts +0 -0
  135. /package/dist/templates/_shared/app/{site/[siteId]/page.tsx → page.tsx} +0 -0
  136. /package/dist/templates/_shared/app/{site/[siteId]/sitemap.xml → sitemap.xml}/route.ts +0 -0
  137. /package/dist/templates/_shared/app/{site/[siteId]/tag → tag}/[tag]/page.tsx +0 -0
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 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";
4
+ import { spinner as spinner2, outro as outro5, log as log7 } from "@clack/prompts";
5
+ import { existsSync as existsSync9 } from "fs";
6
+ import { basename as basename4, resolve as resolve5 } from "path";
7
7
 
8
8
  // src/prompts.ts
9
9
  import * as p from "@clack/prompts";
@@ -106,6 +106,9 @@ function sharedTemplateDir() {
106
106
  function templatePath(theme) {
107
107
  return resolve(templatesDir, theme);
108
108
  }
109
+ function pluginTemplateDir(mode) {
110
+ return resolve(templatesDir, mode === "local" ? "plugin-local" : "plugin-standalone");
111
+ }
109
112
 
110
113
  // src/gitignore.ts
111
114
  var DEFAULT_GITIGNORE = `# Dependencies
@@ -269,6 +272,20 @@ async function scaffold(sharedDir, themesRoot, destDir, opts) {
269
272
  // src/args.ts
270
273
  var VALID_THEMES = ["blog", "minimal", "landing", "corporate", "docs", "dads"];
271
274
  var VALID_PLUGINS = ["seo", "rss", "webhook"];
275
+ var VALID_PLUGIN_TRUST_LEVELS = [
276
+ "untrusted",
277
+ "trusted",
278
+ "privileged"
279
+ ];
280
+ var VALID_PLUGIN_CAPABILITIES = [
281
+ "publicHead",
282
+ "publicBody",
283
+ "metadata",
284
+ "eventHooks",
285
+ "adminSettings",
286
+ "writePublicAsset",
287
+ "schema"
288
+ ];
272
289
  var STRING_FLAGS = /* @__PURE__ */ new Set([
273
290
  "--site-name",
274
291
  "--themes",
@@ -279,7 +296,11 @@ var STRING_FLAGS = /* @__PURE__ */ new Set([
279
296
  "--aws-region",
280
297
  "--domain",
281
298
  "--subdomain",
282
- "--iam-service-role"
299
+ "--iam-service-role",
300
+ // Phase 5 `plugin <name>` flags
301
+ "--trust-level",
302
+ "--capabilities",
303
+ "--description"
283
304
  ]);
284
305
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
285
306
  "--deploy",
@@ -291,7 +312,12 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
291
312
  "--create-iam-role",
292
313
  "--skip-confirm",
293
314
  "--help",
294
- "-h"
315
+ "-h",
316
+ // Phase 5 `plugin <name>` mode flags
317
+ "--local",
318
+ "--standalone",
319
+ // setup-encryption-key flags
320
+ "--gitignore"
295
321
  ]);
296
322
  function parseDeployArgs(argv) {
297
323
  const out = {
@@ -299,6 +325,9 @@ function parseDeployArgs(argv) {
299
325
  mount: false,
300
326
  upgrade: false,
301
327
  copyTheme: false,
328
+ createPlugin: false,
329
+ setupEncryptionKey: false,
330
+ gitignore: false,
302
331
  dryRun: false,
303
332
  noInstall: false,
304
333
  githubPrivate: false,
@@ -346,6 +375,21 @@ function parseDeployArgs(argv) {
346
375
  case "-h":
347
376
  out.help = true;
348
377
  break;
378
+ case "--local":
379
+ if (out.pluginMode === "standalone") {
380
+ throw new Error("Cannot combine --local and --standalone");
381
+ }
382
+ out.pluginMode = "local";
383
+ break;
384
+ case "--standalone":
385
+ if (out.pluginMode === "local") {
386
+ throw new Error("Cannot combine --local and --standalone");
387
+ }
388
+ out.pluginMode = "standalone";
389
+ break;
390
+ case "--gitignore":
391
+ out.gitignore = true;
392
+ break;
349
393
  }
350
394
  continue;
351
395
  }
@@ -401,6 +445,31 @@ function parseDeployArgs(argv) {
401
445
  case "--iam-service-role":
402
446
  out.iamServiceRole = value;
403
447
  break;
448
+ case "--trust-level": {
449
+ if (!VALID_PLUGIN_TRUST_LEVELS.includes(value)) {
450
+ throw new Error(
451
+ `Invalid --trust-level "${value}". Valid values: ${VALID_PLUGIN_TRUST_LEVELS.join(", ")}`
452
+ );
453
+ }
454
+ out.pluginTrustLevel = value;
455
+ break;
456
+ }
457
+ case "--capabilities": {
458
+ const caps = value.split(",").map((c) => c.trim()).filter(Boolean);
459
+ const invalid = caps.filter(
460
+ (c) => !VALID_PLUGIN_CAPABILITIES.includes(c)
461
+ );
462
+ if (invalid.length > 0) {
463
+ throw new Error(
464
+ `Invalid capability(ies): ${invalid.join(", ")}. Valid values: ${VALID_PLUGIN_CAPABILITIES.join(", ")}`
465
+ );
466
+ }
467
+ out.pluginCapabilities = caps;
468
+ break;
469
+ }
470
+ case "--description":
471
+ out.pluginDescription = value;
472
+ break;
404
473
  }
405
474
  continue;
406
475
  }
@@ -412,6 +481,10 @@ function parseDeployArgs(argv) {
412
481
  out.upgrade = true;
413
482
  continue;
414
483
  }
484
+ if (raw === "setup-encryption-key" && out.projectName === void 0 && !out.setupEncryptionKey && !out.upgrade && !out.copyTheme && !out.createPlugin) {
485
+ out.setupEncryptionKey = true;
486
+ continue;
487
+ }
415
488
  if (raw === "copy-theme" && out.projectName === void 0 && !out.copyTheme) {
416
489
  out.copyTheme = true;
417
490
  continue;
@@ -424,6 +497,14 @@ function parseDeployArgs(argv) {
424
497
  out.copyThemeTarget = raw;
425
498
  continue;
426
499
  }
500
+ if (raw === "plugin" && out.projectName === void 0 && !out.createPlugin && !out.upgrade && !out.copyTheme) {
501
+ out.createPlugin = true;
502
+ continue;
503
+ }
504
+ if (out.createPlugin && out.pluginName === void 0) {
505
+ out.pluginName = raw;
506
+ continue;
507
+ }
427
508
  if (out.projectName === void 0) {
428
509
  out.projectName = raw;
429
510
  } else {
@@ -435,9 +516,12 @@ function parseDeployArgs(argv) {
435
516
  var HELP_TEXT = `create-ampless \u2014 scaffold an ampless project
436
517
 
437
518
  Usage:
438
- npx create-ampless@alpha <project-name> [options]
439
- npx create-ampless@alpha --mount [options] # in an existing project dir
440
- npx create-ampless@alpha upgrade [options] # in an existing project dir
519
+ npx create-ampless@latest <project-name> [options]
520
+ npx create-ampless@latest --mount [options] # in an existing project dir
521
+ npx create-ampless@latest upgrade [options] # in an existing project dir
522
+ npx create-ampless@latest copy-theme <src> <dst> # in an existing project dir
523
+ npx create-ampless@latest plugin <name> [options] # scaffold a plugin (Phase 5)
524
+ npx create-ampless@latest setup-encryption-key [--gitignore] # generate encryption key file
441
525
 
442
526
  Options:
443
527
  --site-name <name> Site display name (default: "My Blog")
@@ -487,7 +571,61 @@ copy-theme <source> <target>
487
571
  (the convention that flags it as user-owned, so upgrade leaves
488
572
  it alone). Run inside an existing ampless project.
489
573
 
490
- Example: npx create-ampless@alpha copy-theme blog my-blog
574
+ Example: npx create-ampless@latest copy-theme blog my-blog
575
+
576
+ plugin <name>
577
+ Scaffold an ampless plugin. Two modes:
578
+
579
+ --local Default. Writes plugins/<name>/index.ts inside
580
+ the current ampless site. The site itself is the
581
+ build / publish unit; the plugin file is just
582
+ code that sits there. Plugin name is a kebab-case
583
+ identifier (e.g. "site-verification").
584
+
585
+ --standalone Writes a complete npm package at ./<name>/ ready
586
+ for 'npm publish'. Plugin name should be the npm
587
+ package name (e.g. "@scope/ampless-plugin-foo"
588
+ or "ampless-plugin-foo"); the kebab segment
589
+ becomes the AmplessPlugin.name.
590
+
591
+ --trust-level <value> 'untrusted' (default), 'trusted', or 'privileged'
592
+ --capabilities <list> Comma-separated. Valid: publicHead, publicBody,
593
+ metadata, eventHooks, adminSettings,
594
+ writePublicAsset, schema
595
+ (default: publicHead,adminSettings)
596
+ --description "<text>" Optional one-line description
597
+
598
+ Examples:
599
+ npx create-ampless@latest plugin site-verification
600
+ npx create-ampless@latest plugin @ishinao/ampless-plugin-foo --standalone
601
+
602
+ setup-encryption-key
603
+ Generate the AES-256-GCM encryption key for plugin secret storage
604
+ and write it to amplify/secrets/encryption-key.ts (adjacent to
605
+ amplify/backend.ts). No AWS credentials required \u2014 this is a
606
+ local file operation only.
607
+
608
+ After generating, import the constant in amplify/backend.ts and
609
+ pass it to defineAmplessBackend({ pluginSecretEncryptionKey: ... }).
610
+ Then redeploy (or restart the sandbox) to inject the key into the
611
+ Lambda env vars.
612
+
613
+ --gitignore Add amplify/secrets/encryption-key.ts to
614
+ .gitignore. Default: file is committed
615
+ (safe for private repos). Use this flag only
616
+ for public repos; distribute the key separately.
617
+
618
+ Threat model:
619
+ \u2713 DDB Console operator sees ciphertext only
620
+ \u26A0 Source repo / deploy artifact access defeats encryption
621
+ \u2717 Malicious trusted plugin in the same Lambda can also read
622
+ the key (per-plugin Lambda isolation = roadmap)
623
+
624
+ Rotation: re-run with confirm overwrite. Existing ciphertext
625
+ becomes unreadable; re-save each secret via /admin/plugins.
626
+
627
+ Example: npx create-ampless@latest setup-encryption-key
628
+ Example: npx create-ampless@latest setup-encryption-key --gitignore
491
629
  `;
492
630
 
493
631
  // src/deploy-prompts.ts
@@ -1091,38 +1229,19 @@ function splitDomain(domain, subdomain) {
1091
1229
  async function rewriteCmsConfigForDomain(projectDir, fullDomain) {
1092
1230
  const path = resolve4(projectDir, "cms.config.ts");
1093
1231
  if (!existsSync4(path)) {
1094
- return { urlRewritten: false, sitesInjected: false };
1232
+ return { urlRewritten: false };
1095
1233
  }
1096
1234
  let content = await readFile2(path, "utf-8");
1097
1235
  let urlRewritten = false;
1098
- let sitesInjected = false;
1099
1236
  const urlRe = /url:\s*['"]http:\/\/localhost:3000['"]/;
1100
1237
  if (urlRe.test(content)) {
1101
1238
  content = content.replace(urlRe, `url: 'https://${fullDomain}'`);
1102
1239
  urlRewritten = true;
1103
1240
  }
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) {
1241
+ if (urlRewritten) {
1123
1242
  await writeFile2(path, content, "utf-8");
1124
1243
  }
1125
- return { urlRewritten, sitesInjected };
1244
+ return { urlRewritten };
1126
1245
  }
1127
1246
  async function run(cmd, args, opts = {}) {
1128
1247
  const { step, ...execaOpts } = opts;
@@ -1503,11 +1622,8 @@ async function runDeploy(opts) {
1503
1622
  const { registrable, subdomain } = splitDomain(opts.domain, opts.subdomain);
1504
1623
  const fullDomain = subdomain ? `${subdomain}.${registrable}` : registrable;
1505
1624
  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(", ")}`);
1625
+ if (mutations.urlRewritten) {
1626
+ log.info(`cms.config.ts updated: site.url \u2192 https://${fullDomain}`);
1511
1627
  }
1512
1628
  }
1513
1629
  const created = {};
@@ -1761,6 +1877,15 @@ import { join as join3, relative, extname as extname2, dirname } from "path";
1761
1877
  import { log as log3, outro } from "@clack/prompts";
1762
1878
  import pc3 from "picocolors";
1763
1879
  import { execa as execa4 } from "execa";
1880
+ var AMPLESS_MANAGED_APP_PATHS = [
1881
+ "app/(admin)/admin",
1882
+ "app/api/admin",
1883
+ "app/api/media",
1884
+ "app/api/mcp",
1885
+ "app/login",
1886
+ "app/site"
1887
+ ];
1888
+ var AMPLESS_RETIRED_PATHS = [];
1764
1889
  var AMPLESS_PACKAGES = /* @__PURE__ */ new Set([
1765
1890
  "ampless",
1766
1891
  "@ampless/admin",
@@ -1768,8 +1893,14 @@ var AMPLESS_PACKAGES = /* @__PURE__ */ new Set([
1768
1893
  "@ampless/runtime",
1769
1894
  "@ampless/plugin-seo",
1770
1895
  "@ampless/plugin-rss",
1896
+ "@ampless/plugin-schema-jsonld",
1771
1897
  "@ampless/plugin-webhook",
1772
- "@ampless/plugin-og-image"
1898
+ "@ampless/plugin-og-image",
1899
+ "@ampless/plugin-analytics-ga4",
1900
+ "@ampless/plugin-gtm",
1901
+ "@ampless/plugin-plausible",
1902
+ "@ampless/plugin-cookie-consent",
1903
+ "@ampless/plugin-reading-time"
1773
1904
  ]);
1774
1905
  var AMPLESS_MANAGED_SCRIPTS = /* @__PURE__ */ new Set([
1775
1906
  "sandbox",
@@ -1791,15 +1922,34 @@ var PROTECTED_PATTERNS = [
1791
1922
  /^\.amplify(\/|$)/,
1792
1923
  /^amplify_outputs\.json$/,
1793
1924
  /^next-env\.d\.ts$/,
1925
+ // `tsconfig.json` is auto-managed by Next.js during `next build` /
1926
+ // `next dev` — it rewrites `jsx: "preserve"` → `"react-jsx"` for
1927
+ // the React automatic runtime and appends `.next/dev/types/**/*.ts`
1928
+ // to `include`. Treating the file as `replace` here would overwrite
1929
+ // those mutations on every `update-ampless`, only for Next.js to
1930
+ // re-apply them on the next build — a churn cycle that produces a
1931
+ // dirty diff after every upgrade. Protect the file; users hand-
1932
+ // merge new compiler options in the rare case the template
1933
+ // tsconfig changes meaningfully.
1934
+ /^tsconfig\.json$/,
1794
1935
  /^tsconfig\.tsbuildinfo$/,
1795
1936
  /^pnpm-lock\.yaml$/,
1796
1937
  /^package-lock\.json$/,
1797
1938
  // Anything under themes/ is handled by syncThemes; the file-walk
1798
1939
  // classifier must not double-process it.
1799
1940
  /^themes(\/|$)/,
1800
- /^themes-registry\.ts$/
1941
+ /^themes-registry\.ts$/,
1942
+ // `plugins/` is the site-local plugin directory (user-owned, mirrors
1943
+ // the `themes/my-*` convention for user-customised themes). The
1944
+ // initial scaffold seeds a README into this directory; everything
1945
+ // afterwards is the user's territory. We never overwrite or delete
1946
+ // files here — even the seeded README is frozen at scaffold time,
1947
+ // because plugin authors will sometimes edit it to document the
1948
+ // site's own conventions. The kept-up-to-date "how to write a
1949
+ // plugin" doc lives in `packages/ampless/docs/plugin-author-guide.md`.
1950
+ /^plugins(\/|$)/
1801
1951
  ];
1802
- var SEED_IF_MISSING_PATTERN = /\.custom\.ts$/;
1952
+ var SEED_IF_MISSING_PATTERN = /\.custom\.ts$|^plugins\/README(\.ja)?\.md$|^amplify\/secrets\/encryption-key\.ts$/;
1803
1953
  var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
1804
1954
  ".json",
1805
1955
  ".md",
@@ -1819,12 +1969,24 @@ var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
1819
1969
  ".gitignore"
1820
1970
  ]);
1821
1971
  function isProtected(relPath) {
1972
+ if (SEED_IF_MISSING_PATTERN.test(relPath)) return false;
1822
1973
  return PROTECTED_PATTERNS.some((re) => re.test(relPath));
1823
1974
  }
1975
+ var NON_THEME_TEMPLATE_PREFIXES = ["plugin-"];
1976
+ function isThemeDirName(name) {
1977
+ if (name === "_shared") return false;
1978
+ for (const prefix of NON_THEME_TEMPLATE_PREFIXES) {
1979
+ if (name.startsWith(prefix)) return false;
1980
+ }
1981
+ return true;
1982
+ }
1983
+ function isQuarantinedThemeName(name) {
1984
+ return !isThemeDirName(name);
1985
+ }
1824
1986
  async function listShippedThemes(templatesRoot) {
1825
1987
  if (!existsSync5(templatesRoot)) return [];
1826
1988
  const entries = await readdir3(templatesRoot, { withFileTypes: true });
1827
- return entries.filter((e) => e.isDirectory() && e.name !== "_shared").map((e) => e.name).sort();
1989
+ return entries.filter((e) => e.isDirectory() && isThemeDirName(e.name)).map((e) => e.name).sort();
1828
1990
  }
1829
1991
  var USER_OWNED_THEME_FILES = /* @__PURE__ */ new Set(["README.md", ".gitignore"]);
1830
1992
  function isUserOwnedThemeFile(path) {
@@ -1852,6 +2014,16 @@ async function syncThemes(destDir, templatesRoot) {
1852
2014
  const shipped = await listShippedThemes(templatesRoot);
1853
2015
  const themesDir = join3(destDir, "themes");
1854
2016
  await mkdir2(themesDir, { recursive: true });
2017
+ const quarantined = [];
2018
+ if (existsSync5(themesDir)) {
2019
+ const present = await readdir3(themesDir, { withFileTypes: true });
2020
+ for (const entry of present) {
2021
+ if (!entry.isDirectory()) continue;
2022
+ if (!isQuarantinedThemeName(entry.name)) continue;
2023
+ await rm2(join3(themesDir, entry.name), { recursive: true, force: true });
2024
+ quarantined.push(entry.name);
2025
+ }
2026
+ }
1855
2027
  for (const name of shipped) {
1856
2028
  const src = join3(templatesRoot, name);
1857
2029
  const dst = join3(themesDir, name);
@@ -1865,10 +2037,12 @@ async function syncThemes(destDir, templatesRoot) {
1865
2037
  }
1866
2038
  const installed = await discoverInstalledThemes(destDir);
1867
2039
  const shippedSet = new Set(shipped);
1868
- const preserved = installed.filter((t) => !shippedSet.has(t));
2040
+ const preserved = installed.filter(
2041
+ (t) => !shippedSet.has(t) && !isQuarantinedThemeName(t)
2042
+ );
1869
2043
  const all = [...shipped, ...preserved];
1870
2044
  await writeFile3(join3(destDir, "themes-registry.ts"), buildRegistry(all), "utf-8");
1871
- return { synced: shipped, preserved };
2045
+ return { synced: shipped, preserved, quarantined };
1872
2046
  }
1873
2047
  async function walkDir(dir, base, out) {
1874
2048
  const entries = await readdir3(dir, { withFileTypes: true });
@@ -1882,6 +2056,69 @@ async function walkDir(dir, base, out) {
1882
2056
  }
1883
2057
  }
1884
2058
  }
2059
+ async function listFilesRecursive(root) {
2060
+ const out = [];
2061
+ async function walk(current, relPrefix) {
2062
+ const entries = await readdir3(current, { withFileTypes: true });
2063
+ for (const entry of entries) {
2064
+ const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
2065
+ if (entry.isDirectory()) {
2066
+ await walk(join3(current, entry.name), rel);
2067
+ } else if (entry.isFile()) {
2068
+ out.push(rel);
2069
+ }
2070
+ }
2071
+ }
2072
+ if (!existsSync5(root)) return out;
2073
+ await walk(root, "");
2074
+ return out;
2075
+ }
2076
+ async function pruneEmptyDirs(root) {
2077
+ if (!existsSync5(root)) return;
2078
+ const entries = await readdir3(root, { withFileTypes: true });
2079
+ for (const entry of entries) {
2080
+ if (entry.isDirectory()) {
2081
+ await pruneEmptyDirs(join3(root, entry.name));
2082
+ }
2083
+ }
2084
+ const remaining = await readdir3(root);
2085
+ if (remaining.length === 0) {
2086
+ await rm2(root, { recursive: true, force: true });
2087
+ }
2088
+ }
2089
+ async function findObsoleteFiles(destDir, sharedDir) {
2090
+ const obsolete = [];
2091
+ for (const managedPath of AMPLESS_MANAGED_APP_PATHS) {
2092
+ const userPath = join3(destDir, managedPath);
2093
+ if (!existsSync5(userPath)) continue;
2094
+ const templatePath2 = join3(sharedDir, managedPath);
2095
+ const templateFiles = new Set(await listFilesRecursive(templatePath2));
2096
+ const userFiles = await listFilesRecursive(userPath);
2097
+ for (const f of userFiles) {
2098
+ if (!templateFiles.has(f)) {
2099
+ obsolete.push(`${managedPath}/${f}`);
2100
+ }
2101
+ }
2102
+ }
2103
+ for (const retiredPath of AMPLESS_RETIRED_PATHS) {
2104
+ const userFile = join3(destDir, retiredPath);
2105
+ if (existsSync5(userFile)) {
2106
+ obsolete.push(retiredPath);
2107
+ }
2108
+ }
2109
+ return obsolete;
2110
+ }
2111
+ async function removeObsoleteFiles(destDir, paths) {
2112
+ for (const rel of paths) {
2113
+ const abs = join3(destDir, rel);
2114
+ if (existsSync5(abs)) {
2115
+ await rm2(abs, { force: true });
2116
+ }
2117
+ }
2118
+ for (const managedPath of AMPLESS_MANAGED_APP_PATHS) {
2119
+ await pruneEmptyDirs(join3(destDir, managedPath));
2120
+ }
2121
+ }
1885
2122
  function substituteVars(content, vars) {
1886
2123
  return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
1887
2124
  }
@@ -1934,22 +2171,34 @@ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
1934
2171
  const seedSkipped = classification.seed.filter((r) => existsSync5(join3(destDir, r)));
1935
2172
  const shippedThemes = themeSyncEnabled ? await listShippedThemes(templatesRoot) : [];
1936
2173
  const existingThemes = themeSyncEnabled ? await discoverInstalledThemes(destDir) : [];
1937
- const preservedThemes = existingThemes.filter((t) => !shippedThemes.includes(t));
2174
+ const quarantinedThemesPreview = existingThemes.filter(isQuarantinedThemeName);
2175
+ const preservedThemes = existingThemes.filter(
2176
+ (t) => !shippedThemes.includes(t) && !isQuarantinedThemeName(t)
2177
+ );
2178
+ const obsoleteFiles = await findObsoleteFiles(destDir, sharedDir);
1938
2179
  log3.info(
1939
- `replace: ${pc3.green(`\u8FFD\u52A0 ${replaceNew.length}`)} / ${pc3.yellow(`\u66F4\u65B0 ${replaceUpdate.length}`)}`
2180
+ `replace: ${pc3.green(`${replaceNew.length} added`)} / ${pc3.yellow(`${replaceUpdate.length} updated`)}`
1940
2181
  );
1941
- log3.info(`merge: ${pc3.cyan("package.json: ampless deps / managed scripts \u3092\u30C6\u30F3\u30D7\u30EC\u5074\u306B\u5408\u308F\u305B\u308B")}`);
2182
+ log3.info(`merge: ${pc3.cyan("package.json: sync ampless deps and managed scripts with the template")}`);
1942
2183
  if (classification.seed.length > 0) {
1943
2184
  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)`
2185
+ `seed: ${pc3.green(`${seedNew.length} added`)} / ${pc3.dim(`${seedSkipped.length} kept (existing *.custom.ts left untouched)`)}`
1945
2186
  );
1946
2187
  }
1947
2188
  if (themeSyncEnabled) {
1948
2189
  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}`)}`
2190
+ `themes: ${pc3.cyan(`${shippedThemes.length} default themes synced`)} / ${pc3.dim(`${preservedThemes.length} custom (my-*) themes preserved`)}`
1950
2191
  );
2192
+ if (quarantinedThemesPreview.length > 0) {
2193
+ log3.info(
2194
+ `recover: ${pc3.yellow(`${quarantinedThemesPreview.length} bogus theme dir(s) removed`)} (${quarantinedThemesPreview.join(", ")} \u2014 scaffold templates leaked in by an earlier buggy create-ampless@alpha)`
2195
+ );
2196
+ }
2197
+ }
2198
+ if (obsoleteFiles.length > 0) {
2199
+ log3.info(`cleanup: ${pc3.yellow(`${obsoleteFiles.length} removed`)} (files under ampless-managed app/ paths that no longer exist in the template)`);
1951
2200
  }
1952
- log3.info(`protected: ${pc3.dim(`\u30C6\u30F3\u30D7\u30EC\u306B\u5B58\u5728\u3059\u308B\u304C\u89E6\u3089\u306A\u3044: ${classification.protected.length} \u500B`)}`);
2201
+ log3.info(`protected: ${pc3.dim(`${classification.protected.length} template files left untouched`)}`);
1953
2202
  if (opts.dryRun) {
1954
2203
  return {
1955
2204
  added: replaceNew,
@@ -1958,7 +2207,9 @@ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
1958
2207
  protected: classification.protected,
1959
2208
  themesSynced: shippedThemes,
1960
2209
  themesPreserved: preservedThemes,
1961
- packageJsonMerged: false
2210
+ themesQuarantined: quarantinedThemesPreview,
2211
+ packageJsonMerged: false,
2212
+ obsoleteRemoved: obsoleteFiles
1962
2213
  };
1963
2214
  }
1964
2215
  for (const rel of classification.replace) {
@@ -1971,7 +2222,8 @@ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
1971
2222
  const dst = join3(destDir, rel);
1972
2223
  await copyWithSubstitution(src, dst, vars);
1973
2224
  }
1974
- const themeResult = themeSyncEnabled ? await syncThemes(destDir, templatesRoot) : { synced: [], preserved: [] };
2225
+ const themeResult = themeSyncEnabled ? await syncThemes(destDir, templatesRoot) : { synced: [], preserved: [], quarantined: [] };
2226
+ await removeObsoleteFiles(destDir, obsoleteFiles);
1975
2227
  const templatePkgRaw = await readFile3(join3(sharedDir, "package.json"), "utf-8");
1976
2228
  const templatePkg = JSON.parse(templatePkgRaw);
1977
2229
  const indent = detectIndent(projectPkgRaw);
@@ -1988,9 +2240,12 @@ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
1988
2240
  }
1989
2241
  const templateScripts = templatePkg["scripts"] ?? {};
1990
2242
  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;
2243
+ for (const name of AMPLESS_MANAGED_SCRIPTS) {
2244
+ if (name in templateScripts) {
2245
+ projectScripts[name] = templateScripts[name];
2246
+ } else {
2247
+ delete projectScripts[name];
2248
+ }
1994
2249
  }
1995
2250
  if (Object.keys(projectScripts).length > 0) {
1996
2251
  projectPkg["scripts"] = projectScripts;
@@ -2008,7 +2263,9 @@ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
2008
2263
  protected: classification.protected,
2009
2264
  themesSynced: themeResult.synced,
2010
2265
  themesPreserved: themeResult.preserved,
2011
- packageJsonMerged: true
2266
+ themesQuarantined: themeResult.quarantined,
2267
+ packageJsonMerged: true,
2268
+ obsoleteRemoved: obsoleteFiles
2012
2269
  };
2013
2270
  }
2014
2271
  async function runUpgrade(args) {
@@ -2104,8 +2361,8 @@ async function runCopyTheme(args) {
2104
2361
  const source = args.copyThemeSource;
2105
2362
  const target = args.copyThemeTarget;
2106
2363
  if (!source || !target) {
2107
- log4.error("Usage: npx create-ampless@alpha copy-theme <source> <target>");
2108
- log4.info("Example: npx create-ampless@alpha copy-theme blog my-blog");
2364
+ log4.error("Usage: npx create-ampless@latest copy-theme <source> <target>");
2365
+ log4.info("Example: npx create-ampless@latest copy-theme blog my-blog");
2109
2366
  process.exit(1);
2110
2367
  }
2111
2368
  try {
@@ -2118,7 +2375,7 @@ async function runCopyTheme(args) {
2118
2375
 
2119
2376
  themes-registry.ts updated. Next:
2120
2377
  ${pc4.cyan(`Open themes/${result.target}/ and start customising`)}
2121
- ${pc4.cyan(`Activate via /admin/sites/<siteId>/theme`)}`
2378
+ ${pc4.cyan(`Activate via /admin/sites/default/theme`)}`
2122
2379
  );
2123
2380
  } catch (err) {
2124
2381
  log4.error(err instanceof Error ? err.message : String(err));
@@ -2126,12 +2383,407 @@ async function runCopyTheme(args) {
2126
2383
  }
2127
2384
  }
2128
2385
 
2129
- // src/index.ts
2386
+ // src/plugin.ts
2387
+ import { cp as cp4, mkdir as mkdir3, readFile as readFile5, writeFile as writeFile5, readdir as readdir5 } from "fs/promises";
2388
+ import { existsSync as existsSync7 } from "fs";
2389
+ import { join as join5, extname as extname3, basename as basename3 } from "path";
2390
+ import {
2391
+ log as log5,
2392
+ outro as outro3,
2393
+ text as text3,
2394
+ select as select2,
2395
+ multiselect as multiselect2,
2396
+ isCancel as isCancel3,
2397
+ cancel as cancel3
2398
+ } from "@clack/prompts";
2130
2399
  import pc5 from "picocolors";
2400
+ var SCAFFOLD_AMPLESS_VERSION = "1.0.0-alpha.22";
2401
+ var TEXT_EXTENSIONS3 = /* @__PURE__ */ new Set([
2402
+ ".json",
2403
+ ".md",
2404
+ ".ts",
2405
+ ".tsx",
2406
+ ".js",
2407
+ ".jsx",
2408
+ ".mjs",
2409
+ ".cjs",
2410
+ ".html",
2411
+ ".css",
2412
+ ".env",
2413
+ ".txt",
2414
+ ".yaml",
2415
+ ".yml",
2416
+ ".toml",
2417
+ ".gitignore"
2418
+ ]);
2419
+ function toKebab(s) {
2420
+ return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()).replace(/^-/, "");
2421
+ }
2422
+ function toCamelCase(kebab) {
2423
+ return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2424
+ }
2425
+ function toPascalCase(kebab) {
2426
+ const c = toCamelCase(kebab);
2427
+ return c.charAt(0).toUpperCase() + c.slice(1);
2428
+ }
2429
+ function toDisplayName(kebab) {
2430
+ const words = kebab.split("-");
2431
+ return words.map((w, i) => i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w).join(" ");
2432
+ }
2433
+ function sanitizeForJsComment(s) {
2434
+ return s.replace(/\*\//g, "*\\/").replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " ").trim();
2435
+ }
2436
+ function lastSegment(packageName) {
2437
+ return packageName.replace(/^@[^/]+\//, "");
2438
+ }
2439
+ function pluginNameFromPackage(packageName) {
2440
+ const segment = lastSegment(packageName);
2441
+ if (segment.startsWith("ampless-plugin-")) {
2442
+ return segment.slice("ampless-plugin-".length);
2443
+ }
2444
+ if (segment.startsWith("plugin-")) {
2445
+ return segment.slice("plugin-".length);
2446
+ }
2447
+ return segment;
2448
+ }
2449
+ async function substituteFile2(filePath, vars) {
2450
+ const ext = extname3(filePath) || (basename3(filePath) === ".gitignore" ? ".gitignore" : "");
2451
+ if (!TEXT_EXTENSIONS3.has(ext)) return;
2452
+ const content = await readFile5(filePath, "utf-8");
2453
+ const replaced = content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
2454
+ if (replaced !== content) await writeFile5(filePath, replaced, "utf-8");
2455
+ }
2456
+ async function substituteDir2(dir, vars) {
2457
+ const entries = await readdir5(dir, { withFileTypes: true });
2458
+ await Promise.all(
2459
+ entries.map(async (entry) => {
2460
+ const fullPath = join5(dir, entry.name);
2461
+ if (entry.isDirectory()) {
2462
+ await substituteDir2(fullPath, vars);
2463
+ } else {
2464
+ await substituteFile2(fullPath, vars);
2465
+ }
2466
+ })
2467
+ );
2468
+ }
2469
+ var KEBAB_RE = /^[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*$/;
2470
+ var PACKAGE_NAME_RE = /^(@[a-z][a-z0-9-]*\/)?[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
2471
+ async function runCreatePluginIn(destDir, args) {
2472
+ const mode = args.pluginMode ?? "local";
2473
+ let rawName = args.pluginName;
2474
+ if (!rawName) {
2475
+ if (args.skipConfirm === true) {
2476
+ throw new Error(
2477
+ mode === "local" ? "Plugin name is required when --skip-confirm is set. Pass it as the positional argument: `create-ampless plugin <name> --skip-confirm`." : "Plugin package name is required when --skip-confirm is set. Pass it as the positional argument: `create-ampless plugin <package-name> --standalone --skip-confirm`."
2478
+ );
2479
+ }
2480
+ const answer = await text3({
2481
+ message: mode === "local" ? 'Plugin name (kebab-case, e.g. "site-verification"):' : 'Plugin package name (e.g. "@scope/ampless-plugin-foo" or "ampless-plugin-foo"):',
2482
+ validate(v) {
2483
+ if (!v) return "Plugin name is required";
2484
+ const re = mode === "local" ? KEBAB_RE : PACKAGE_NAME_RE;
2485
+ if (!re.test(v)) {
2486
+ return mode === "local" ? 'Must be kebab-case starting with a lowercase letter (e.g. "site-verification")' : 'Must be a valid npm package name (e.g. "ampless-plugin-foo" or "@scope/ampless-plugin-foo")';
2487
+ }
2488
+ }
2489
+ });
2490
+ if (isCancel3(answer)) {
2491
+ cancel3("Cancelled.");
2492
+ process.exit(0);
2493
+ }
2494
+ rawName = answer;
2495
+ } else {
2496
+ const re = mode === "local" ? KEBAB_RE : PACKAGE_NAME_RE;
2497
+ if (!re.test(rawName)) {
2498
+ throw new Error(
2499
+ mode === "local" ? `Invalid plugin name "${rawName}". Must be kebab-case (e.g. "site-verification").` : `Invalid plugin name "${rawName}". Must be a valid npm package name (e.g. "ampless-plugin-foo" or "@scope/ampless-plugin-foo").`
2500
+ );
2501
+ }
2502
+ }
2503
+ const skipConfirm = args.skipConfirm === true;
2504
+ let trustLevel = args.pluginTrustLevel;
2505
+ if (!trustLevel) {
2506
+ if (skipConfirm) {
2507
+ trustLevel = "untrusted";
2508
+ } else {
2509
+ const answer = await select2({
2510
+ message: "Trust level:",
2511
+ options: VALID_PLUGIN_TRUST_LEVELS.map((t) => ({
2512
+ value: t,
2513
+ label: t,
2514
+ hint: t === "untrusted" ? "No special permissions (default)" : t === "trusted" ? "Can read site config" : "Full access to internal APIs"
2515
+ }))
2516
+ });
2517
+ if (isCancel3(answer)) {
2518
+ cancel3("Cancelled.");
2519
+ process.exit(0);
2520
+ }
2521
+ trustLevel = answer;
2522
+ }
2523
+ }
2524
+ let capabilities = args.pluginCapabilities;
2525
+ if (!capabilities) {
2526
+ if (skipConfirm) {
2527
+ capabilities = ["publicHead", "adminSettings"];
2528
+ } else {
2529
+ const answer = await multiselect2({
2530
+ message: "Capabilities (space to toggle, enter to confirm):",
2531
+ options: VALID_PLUGIN_CAPABILITIES.map((c) => ({
2532
+ value: c,
2533
+ label: c
2534
+ })),
2535
+ initialValues: ["publicHead", "adminSettings"],
2536
+ required: false
2537
+ });
2538
+ if (isCancel3(answer)) {
2539
+ cancel3("Cancelled.");
2540
+ process.exit(0);
2541
+ }
2542
+ capabilities = answer;
2543
+ }
2544
+ }
2545
+ const rawDescription = args.pluginDescription ?? "";
2546
+ const description = sanitizeForJsComment(rawDescription);
2547
+ const descriptionJson = JSON.stringify(rawDescription);
2548
+ const nameKebab = mode === "local" ? rawName : pluginNameFromPackage(rawName);
2549
+ const packageName = mode === "standalone" ? rawName : nameKebab;
2550
+ if (!KEBAB_RE.test(nameKebab)) {
2551
+ throw new Error(
2552
+ `Invalid plugin name: "${nameKebab}" (derived from "${rawName}" by stripping the npm scope and the conventional ampless-plugin- prefix) is not valid kebab-case. Each segment must start with a letter and contain only letters and digits. Examples: "site-verification", "image-zoom".`
2553
+ );
2554
+ }
2555
+ const nameCamelCase = toCamelCase(nameKebab);
2556
+ const NamePascalCase = toPascalCase(nameKebab);
2557
+ const DisplayName = toDisplayName(nameKebab);
2558
+ void toKebab;
2559
+ let outputDir;
2560
+ if (mode === "local") {
2561
+ const problem = validateMountableProject(destDir);
2562
+ if (problem) {
2563
+ throw new Error(problem);
2564
+ }
2565
+ outputDir = join5(destDir, "plugins", nameKebab);
2566
+ if (existsSync7(outputDir)) {
2567
+ throw new Error(
2568
+ `Plugin directory already exists: plugins/${nameKebab}/
2569
+ Remove it first or choose a different name.`
2570
+ );
2571
+ }
2572
+ } else {
2573
+ const nameSegment = lastSegment(rawName);
2574
+ outputDir = join5(destDir, nameSegment);
2575
+ if (existsSync7(outputDir)) {
2576
+ throw new Error(
2577
+ `Directory already exists: ${nameSegment}/
2578
+ Remove it first or choose a different name.`
2579
+ );
2580
+ }
2581
+ }
2582
+ const templateDir = pluginTemplateDir(mode);
2583
+ if (!existsSync7(templateDir)) {
2584
+ throw new Error(
2585
+ `Plugin template not found: ${templateDir}
2586
+ The ${mode === "local" ? "plugin-local" : "plugin-standalone"} template directory has not been added to the repo yet. Run \`git pull\` to pick up the latest templates.`
2587
+ );
2588
+ }
2589
+ await mkdir3(outputDir, { recursive: true });
2590
+ await cp4(templateDir, outputDir, { recursive: true });
2591
+ const capabilitiesList = capabilities && capabilities.length > 0 ? capabilities.map((c) => `'${c}'`).join(", ") : "";
2592
+ const capabilitiesJsonArray = capabilities && capabilities.length > 0 ? capabilities.map((c) => `"${c}"`).join(", ") : "";
2593
+ const vars = {
2594
+ // Use camelCase keys so they survive substituteVars' `\w+` regex.
2595
+ // Templates reference them as `{{nameKebab}}` (not `{{name-kebab}}`).
2596
+ nameKebab,
2597
+ nameCamelCase,
2598
+ NameCamelCase: NamePascalCase,
2599
+ packageName,
2600
+ description,
2601
+ descriptionJson,
2602
+ trustLevel: trustLevel ?? "untrusted",
2603
+ capabilitiesList,
2604
+ capabilitiesJsonArray,
2605
+ DisplayName,
2606
+ displayNameJa: DisplayName,
2607
+ // same as English for now; maintainers can localise
2608
+ amplessVersion: SCAFFOLD_AMPLESS_VERSION
2609
+ };
2610
+ await substituteDir2(outputDir, vars);
2611
+ return {
2612
+ mode,
2613
+ outputDir,
2614
+ pluginName: nameKebab,
2615
+ packageName: mode === "standalone" ? packageName : void 0
2616
+ };
2617
+ }
2618
+ async function runCreatePlugin(args) {
2619
+ const destDir = process.cwd();
2620
+ try {
2621
+ const result = await runCreatePluginIn(destDir, args);
2622
+ if (result.mode === "local") {
2623
+ const rel = `plugins/${result.pluginName}/`;
2624
+ const factoryName = `${toCamelCase(result.pluginName)}Plugin`;
2625
+ outro3(
2626
+ `${pc5.green("\u2714")} Plugin scaffolded at ${pc5.bold(rel)}
2627
+
2628
+ Register it in ${pc5.cyan("cms.config.ts")}:
2629
+
2630
+ import ${factoryName} from './plugins/${result.pluginName}'
2631
+
2632
+ export default defineConfig({
2633
+ // ...
2634
+ plugins: [
2635
+ ${factoryName}(),
2636
+ ],
2637
+ })
2638
+
2639
+ Then restart the dev server to pick up the new plugin.`
2640
+ );
2641
+ } else {
2642
+ const dirName = result.packageName ? lastSegment(result.packageName) : result.pluginName;
2643
+ outro3(
2644
+ `${pc5.green("\u2714")} Standalone plugin scaffolded at ${pc5.bold(dirName + "/")}
2645
+
2646
+ Next steps:
2647
+ ${pc5.cyan(`cd ${dirName}`)}
2648
+ ${pc5.cyan("pnpm install")}
2649
+ ${pc5.cyan("pnpm test")}
2650
+ ${pc5.cyan("pnpm build")}
2651
+ ${pc5.cyan("pnpm publish --access public --tag alpha")}
2652
+
2653
+ Then add the published package to your ampless site:
2654
+ ${pc5.cyan(`pnpm add ${result.packageName ?? result.pluginName}`)}`
2655
+ );
2656
+ }
2657
+ } catch (err) {
2658
+ log5.error(err instanceof Error ? err.message : String(err));
2659
+ process.exit(1);
2660
+ }
2661
+ }
2662
+
2663
+ // src/setup-encryption-key.ts
2664
+ import { writeFile as writeFile6, mkdir as mkdir4, readFile as readFile6 } from "fs/promises";
2665
+ import { existsSync as existsSync8 } from "fs";
2666
+ import { join as join6 } from "path";
2667
+ import { randomBytes } from "crypto";
2668
+ import { log as log6, confirm as confirm3, outro as outro4, cancel as cancel4 } from "@clack/prompts";
2669
+ import pc6 from "picocolors";
2670
+ var SECRETS_DIR = "amplify/secrets";
2671
+ var KEY_FILE = "encryption-key.ts";
2672
+ var KEY_FILE_PATH = `${SECRETS_DIR}/${KEY_FILE}`;
2673
+ function buildFileContent(keyB64) {
2674
+ return `// Generated by \`npx create-ampless setup-encryption-key\`.
2675
+ // AES-256-GCM key (32 bytes, base64) for plugin secret storage.
2676
+ // Rotate via the same CLI command. Existing ciphertext becomes unreadable on rotation.
2677
+
2678
+ export const PLUGIN_SECRET_ENCRYPTION_KEY = '${keyB64}'
2679
+ `;
2680
+ }
2681
+ function extractKeyValue(source) {
2682
+ const match = source.match(
2683
+ /\bPLUGIN_SECRET_ENCRYPTION_KEY\b\s*(?::\s*[^=]+)?=\s*(['"])(.*?)\1/
2684
+ );
2685
+ return match?.[2] ?? null;
2686
+ }
2687
+ function isEmptyPlaceholder(source) {
2688
+ return extractKeyValue(source) === "";
2689
+ }
2690
+ async function appendGitignoreEntry(cwd, entry) {
2691
+ const gitignorePath = join6(cwd, ".gitignore");
2692
+ if (existsSync8(gitignorePath)) {
2693
+ const content = await readFile6(gitignorePath, "utf-8");
2694
+ if (content.includes(entry)) return;
2695
+ await writeFile6(gitignorePath, content + (content.endsWith("\n") ? "" : "\n") + entry + "\n");
2696
+ } else {
2697
+ await writeFile6(gitignorePath, entry + "\n");
2698
+ }
2699
+ }
2700
+ async function runSetupEncryptionKey(args) {
2701
+ const cwd = process.cwd();
2702
+ if (!existsSync8(join6(cwd, "amplify"))) {
2703
+ log6.error(
2704
+ `No \`amplify/\` directory found in ${cwd}.
2705
+ Run this command from the root of an ampless project (where amplify/backend.ts lives).`
2706
+ );
2707
+ process.exit(1);
2708
+ }
2709
+ const keyFilePath = join6(cwd, KEY_FILE_PATH);
2710
+ if (existsSync8(keyFilePath)) {
2711
+ const existingContent = await readFile6(keyFilePath, "utf-8");
2712
+ if (!isEmptyPlaceholder(existingContent)) {
2713
+ log6.warn(
2714
+ pc6.yellow(
2715
+ `${KEY_FILE_PATH} already exists.
2716
+ Overwriting it will make all existing plugin secrets unreadable
2717
+ until each secret is re-saved through the admin UI.`
2718
+ )
2719
+ );
2720
+ const confirmed = await confirm3({
2721
+ message: "Overwrite the existing encryption key?",
2722
+ initialValue: false
2723
+ });
2724
+ if (confirmed === false || confirmed === void 0) {
2725
+ cancel4("Cancelled \u2014 existing key preserved.");
2726
+ return;
2727
+ }
2728
+ }
2729
+ }
2730
+ const keyBytes = randomBytes(32);
2731
+ const keyB64 = keyBytes.toString("base64");
2732
+ const secretsDir = join6(cwd, SECRETS_DIR);
2733
+ if (!existsSync8(secretsDir)) {
2734
+ await mkdir4(secretsDir, { recursive: true });
2735
+ }
2736
+ try {
2737
+ await writeFile6(keyFilePath, buildFileContent(keyB64), "utf-8");
2738
+ } catch (err) {
2739
+ log6.error(
2740
+ `Failed to write ${KEY_FILE_PATH}: ${err instanceof Error ? err.message : String(err)}`
2741
+ );
2742
+ process.exit(1);
2743
+ }
2744
+ if (args.gitignore) {
2745
+ try {
2746
+ await appendGitignoreEntry(cwd, KEY_FILE_PATH);
2747
+ log6.info(`Added ${KEY_FILE_PATH} to .gitignore`);
2748
+ } catch (err) {
2749
+ log6.warn(
2750
+ `Could not update .gitignore: ${err instanceof Error ? err.message : String(err)}
2751
+ Add \`${KEY_FILE_PATH}\` to your .gitignore manually.`
2752
+ );
2753
+ }
2754
+ }
2755
+ outro4(
2756
+ `${pc6.green("\u2714")} Generated ${pc6.cyan(KEY_FILE_PATH)}
2757
+
2758
+ ${pc6.bold("Next steps:")}
2759
+ 1. In \`amplify/backend.ts\`, add:
2760
+ import { PLUGIN_SECRET_ENCRYPTION_KEY } from './secrets/encryption-key.js'
2761
+ defineAmplessBackend({ ..., pluginSecretEncryptionKey: PLUGIN_SECRET_ENCRYPTION_KEY })
2762
+ 2. Redeploy (or restart the sandbox) to inject the key into Lambda env vars.
2763
+ 3. Save plugin secrets via the admin UI \u2014 they are now encrypted at rest.
2764
+
2765
+ ${pc6.bold("Threat model summary:")}
2766
+ ${pc6.green("\u2713")} AWS Console operator browsing DDB sees ciphertext only
2767
+ ${pc6.yellow("\u26A0")} Anyone with source repo / deploy artifact access can decrypt
2768
+ ${pc6.red("\u2717")} Malicious trusted plugin in the same Lambda can also decrypt
2769
+ (per-plugin Lambda isolation = roadmap)
2770
+
2771
+ ` + (args.gitignore ? ` ${pc6.dim(`${KEY_FILE_PATH} added to .gitignore.`)}
2772
+
2773
+ ` : ` ${pc6.yellow("Tip:")} If committing to a PUBLIC repo, run with --gitignore instead
2774
+ and distribute the key separately.
2775
+
2776
+ `) + ` ${pc6.yellow("Rotation:")} Re-run this command and confirm overwrite.
2777
+ Existing ciphertext will be unreadable; re-save via /admin/plugins.`
2778
+ );
2779
+ }
2780
+
2781
+ // src/index.ts
2782
+ import pc7 from "picocolors";
2131
2783
  var DEFAULT_THEMES = VALID_THEMES;
2132
2784
  function buildNonInteractiveOpts(args) {
2133
2785
  const projectName = args.projectName ?? (() => {
2134
- const b = basename3(process.cwd());
2786
+ const b = basename4(process.cwd());
2135
2787
  return b && b !== "/" ? b : "my-ampless-site";
2136
2788
  })();
2137
2789
  const siteName = args.siteName ?? "My Blog";
@@ -2152,18 +2804,18 @@ function warnIgnoredScaffoldFlags(args) {
2152
2804
  if (args.plugins) ignored.push("--plugins");
2153
2805
  if (args.projectName) ignored.push("<project-name> positional");
2154
2806
  if (ignored.length > 0) {
2155
- log5.warn(`--mount mode ignores: ${ignored.join(", ")}`);
2807
+ log7.warn(`--mount mode ignores: ${ignored.join(", ")}`);
2156
2808
  }
2157
2809
  }
2158
2810
  async function runMount(args) {
2159
2811
  const destDir = process.cwd();
2160
- const projectName = basename3(destDir);
2812
+ const projectName = basename4(destDir);
2161
2813
  warnIgnoredScaffoldFlags(args);
2162
2814
  const problem = validateMountableProject(destDir);
2163
2815
  if (problem) {
2164
- log5.error(problem);
2165
- log5.info(
2166
- "Run `npx create-ampless@alpha <name>` first to scaffold, or `cd` into a scaffolded project before passing --mount."
2816
+ log7.error(problem);
2817
+ log7.info(
2818
+ "Run `npx create-ampless@latest <name>` first to scaffold, or `cd` into a scaffolded project before passing --mount."
2167
2819
  );
2168
2820
  process.exit(1);
2169
2821
  }
@@ -2176,23 +2828,23 @@ async function runMount(args) {
2176
2828
  if (err instanceof PreflightError) {
2177
2829
  process.exit(1);
2178
2830
  }
2179
- log5.error(err instanceof Error ? err.message : String(err));
2831
+ log7.error(err instanceof Error ? err.message : String(err));
2180
2832
  process.exit(1);
2181
2833
  }
2182
2834
  }
2183
2835
  function printDeployResult(result) {
2184
2836
  const lines = [
2185
- `${pc5.green("\u2714")} Project deployed`,
2837
+ `${pc7.green("\u2714")} Project deployed`,
2186
2838
  ``,
2187
- ` GitHub: ${pc5.cyan(result.githubRepoUrl)}`,
2188
- ` Amplify app: ${pc5.cyan(result.amplifyAppId)}`,
2189
- ` Amplify URL: ${pc5.cyan(result.amplifyAppUrl)}`
2839
+ ` GitHub: ${pc7.cyan(result.githubRepoUrl)}`,
2840
+ ` Amplify app: ${pc7.cyan(result.amplifyAppId)}`,
2841
+ ` Amplify URL: ${pc7.cyan(result.amplifyAppUrl)}`
2190
2842
  ];
2191
2843
  if (result.domainUrl) {
2192
- lines.push(` Custom domain: ${pc5.cyan(result.domainUrl)}`);
2844
+ lines.push(` Custom domain: ${pc7.cyan(result.domainUrl)}`);
2193
2845
  }
2194
2846
  if (result.domainVerification && result.domainVerification.length > 0) {
2195
- lines.push("", ` ${pc5.bold("Add these DNS records to verify the domain:")}`);
2847
+ lines.push("", ` ${pc7.bold("Add these DNS records to verify the domain:")}`);
2196
2848
  for (const v of result.domainVerification) {
2197
2849
  lines.push(` ${v.cname} CNAME ${v.target}`);
2198
2850
  }
@@ -2200,9 +2852,9 @@ function printDeployResult(result) {
2200
2852
  lines.push(
2201
2853
  "",
2202
2854
  ` First build is now running in Amplify Hosting.`,
2203
- ` Watch it at ${pc5.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
2855
+ ` Watch it at ${pc7.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
2204
2856
  );
2205
- outro3(lines.join("\n"));
2857
+ outro5(lines.join("\n"));
2206
2858
  }
2207
2859
  async function main() {
2208
2860
  const args = parseDeployArgs(process.argv.slice(2));
@@ -2211,7 +2863,7 @@ async function main() {
2211
2863
  return;
2212
2864
  }
2213
2865
  for (const flag of args.unknown) {
2214
- log5.warn(`Unknown argument ignored: ${flag}`);
2866
+ log7.warn(`Unknown argument ignored: ${flag}`);
2215
2867
  }
2216
2868
  if (args.upgrade) {
2217
2869
  await runUpgrade(args);
@@ -2221,6 +2873,14 @@ async function main() {
2221
2873
  await runCopyTheme(args);
2222
2874
  return;
2223
2875
  }
2876
+ if (args.createPlugin) {
2877
+ await runCreatePlugin(args);
2878
+ return;
2879
+ }
2880
+ if (args.setupEncryptionKey) {
2881
+ await runSetupEncryptionKey(args);
2882
+ return;
2883
+ }
2224
2884
  if (args.mount) {
2225
2885
  await runMount(args);
2226
2886
  return;
@@ -2233,8 +2893,8 @@ async function main() {
2233
2893
  }
2234
2894
  if (!opts) return;
2235
2895
  const destDir = resolve5(process.cwd(), opts.projectName);
2236
- if (existsSync7(destDir)) {
2237
- log5.error(`Directory already exists: ${destDir}`);
2896
+ if (existsSync9(destDir)) {
2897
+ log7.error(`Directory already exists: ${destDir}`);
2238
2898
  process.exit(1);
2239
2899
  }
2240
2900
  const sharedDir = sharedTemplateDir();
@@ -2245,7 +2905,7 @@ async function main() {
2245
2905
  s.stop("Done!");
2246
2906
  } catch (err) {
2247
2907
  s.stop("Failed.");
2248
- log5.error(String(err));
2908
+ log7.error(String(err));
2249
2909
  process.exit(1);
2250
2910
  }
2251
2911
  if (args.deploy) {
@@ -2258,19 +2918,18 @@ async function main() {
2258
2918
  if (err instanceof PreflightError) {
2259
2919
  process.exit(1);
2260
2920
  }
2261
- log5.error(err instanceof Error ? err.message : String(err));
2921
+ log7.error(err instanceof Error ? err.message : String(err));
2262
2922
  process.exit(1);
2263
2923
  }
2264
2924
  return;
2265
2925
  }
2266
- outro3(
2267
- `${pc5.green("\u2714")} Project created at ${pc5.bold(opts.projectName)}
2926
+ outro5(
2927
+ `${pc7.green("\u2714")} Project created at ${pc7.bold(opts.projectName)}
2268
2928
 
2269
2929
  Next steps:
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")}`
2930
+ ${pc7.cyan("cd")} ${opts.projectName}
2931
+ ${pc7.cyan("npm install")}
2932
+ ${pc7.cyan("npm run sandbox")} ${pc7.dim("# deploy sandbox + start Next.js")}`
2274
2933
  );
2275
2934
  }
2276
2935
  main().catch((err) => {