create-ampless 0.2.0-alpha.8 → 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 (140) hide show
  1. package/README.ja.md +77 -0
  2. package/README.md +6 -3
  3. package/dist/index.js +1282 -93
  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.custom.ts +41 -0
  14. package/dist/templates/_shared/amplify/backend.ts +20 -2
  15. package/dist/templates/_shared/amplify/data/get-media-by-src.js +45 -0
  16. package/dist/templates/_shared/amplify/data/get-published-post.js +9 -12
  17. package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +6 -9
  18. package/dist/templates/_shared/amplify/data/list-published-posts.js +10 -11
  19. package/dist/templates/_shared/amplify/data/resource.custom.ts +32 -0
  20. package/dist/templates/_shared/amplify/data/resource.ts +32 -15
  21. package/dist/templates/_shared/amplify/events/dispatcher/resource.ts +1 -0
  22. package/dist/templates/_shared/amplify/events/processor-trusted/resource.ts +1 -0
  23. package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +5 -0
  24. package/dist/templates/_shared/amplify/functions/api-key-renewer/resource.ts +1 -0
  25. package/dist/templates/_shared/amplify/functions/mcp-handler/handler.ts +1 -0
  26. package/dist/templates/_shared/amplify/functions/mcp-handler/resource.ts +12 -0
  27. package/dist/templates/_shared/amplify/functions/plugin-secret-handler/handler.ts +11 -0
  28. package/dist/templates/_shared/amplify/functions/plugin-secret-handler/resource.ts +12 -0
  29. package/dist/templates/_shared/amplify/functions/user-admin/handler.ts +4 -0
  30. package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +13 -0
  31. package/dist/templates/_shared/amplify/secrets/.gitkeep +0 -0
  32. package/dist/templates/_shared/amplify/secrets/encryption-key.ts +9 -0
  33. package/dist/templates/_shared/app/(admin)/admin/mcp-tokens/page.tsx +5 -0
  34. package/dist/templates/_shared/app/(admin)/admin/plugins/page.tsx +5 -0
  35. package/dist/templates/_shared/app/(admin)/admin/users/page.tsx +5 -0
  36. package/dist/templates/_shared/app/globals.css +55 -39
  37. package/dist/templates/_shared/app/layout.tsx +51 -16
  38. package/dist/templates/_shared/app/providers.tsx +0 -2
  39. package/dist/templates/_shared/app/raw/[slug]/route.ts +10 -0
  40. package/dist/templates/_shared/app/static/[slug]/[[...path]]/route.ts +14 -0
  41. package/dist/templates/_shared/cms.config.ts +64 -23
  42. package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +3 -4
  43. package/dist/templates/_shared/components/tag-list.tsx +9 -2
  44. package/dist/templates/_shared/components.json +1 -1
  45. package/dist/templates/_shared/docs/plugin-author-guide.ja.md +934 -0
  46. package/dist/templates/_shared/docs/plugin-author-guide.md +1336 -0
  47. package/dist/templates/_shared/lib/admin.ts +12 -12
  48. package/dist/templates/_shared/lib/ampless.ts +2 -8
  49. package/dist/templates/_shared/lib/amplify.ts +0 -5
  50. package/dist/templates/_shared/package.json +51 -40
  51. package/dist/templates/_shared/plugins/README.ja.md +139 -0
  52. package/dist/templates/_shared/plugins/README.md +143 -0
  53. package/dist/templates/_shared/proxy.ts +23 -8
  54. package/dist/templates/blog/README.ja.md +22 -0
  55. package/dist/templates/blog/README.md +17 -47
  56. package/dist/templates/blog/manifest.ts +1 -1
  57. package/dist/templates/blog/pages/feed.ts +4 -5
  58. package/dist/templates/blog/pages/home.tsx +10 -15
  59. package/dist/templates/blog/pages/post.tsx +42 -14
  60. package/dist/templates/blog/pages/sitemap.ts +4 -5
  61. package/dist/templates/blog/pages/tag.tsx +8 -10
  62. package/dist/templates/blog/tokens.css +26 -40
  63. package/dist/templates/corporate/README.ja.md +18 -0
  64. package/dist/templates/corporate/README.md +12 -14
  65. package/dist/templates/corporate/pages/feed.ts +3 -4
  66. package/dist/templates/corporate/pages/home.tsx +7 -12
  67. package/dist/templates/corporate/pages/post.tsx +20 -14
  68. package/dist/templates/corporate/pages/sitemap.ts +3 -4
  69. package/dist/templates/corporate/pages/tag.tsx +8 -10
  70. package/dist/templates/corporate/tokens.css +17 -39
  71. package/dist/templates/dads/README.ja.md +31 -0
  72. package/dist/templates/dads/README.md +13 -17
  73. package/dist/templates/dads/pages/feed.ts +3 -4
  74. package/dist/templates/dads/pages/home.tsx +7 -12
  75. package/dist/templates/dads/pages/post.tsx +20 -14
  76. package/dist/templates/dads/pages/sitemap.ts +3 -4
  77. package/dist/templates/dads/pages/tag.tsx +8 -10
  78. package/dist/templates/dads/tokens.css +22 -42
  79. package/dist/templates/docs/README.ja.md +24 -0
  80. package/dist/templates/docs/README.md +10 -13
  81. package/dist/templates/docs/pages/feed.ts +3 -4
  82. package/dist/templates/docs/pages/home.tsx +6 -9
  83. package/dist/templates/docs/pages/post.tsx +19 -13
  84. package/dist/templates/docs/pages/sitemap.ts +3 -4
  85. package/dist/templates/docs/pages/tag.tsx +8 -10
  86. package/dist/templates/docs/tokens.css +17 -39
  87. package/dist/templates/landing/README.ja.md +20 -0
  88. package/dist/templates/landing/README.md +14 -19
  89. package/dist/templates/landing/pages/feed.ts +4 -5
  90. package/dist/templates/landing/pages/home.tsx +7 -12
  91. package/dist/templates/landing/pages/post.tsx +20 -14
  92. package/dist/templates/landing/pages/sitemap.ts +3 -4
  93. package/dist/templates/landing/pages/tag.tsx +8 -10
  94. package/dist/templates/landing/tokens.css +17 -39
  95. package/dist/templates/minimal/README.ja.md +14 -0
  96. package/dist/templates/minimal/README.md +9 -47
  97. package/dist/templates/minimal/pages/feed.ts +4 -5
  98. package/dist/templates/minimal/pages/home.tsx +6 -8
  99. package/dist/templates/minimal/pages/post.tsx +19 -12
  100. package/dist/templates/minimal/pages/sitemap.ts +4 -5
  101. package/dist/templates/minimal/pages/tag.tsx +7 -8
  102. package/dist/templates/minimal/tokens.css +17 -39
  103. package/dist/templates/plugin-local/README.md +34 -0
  104. package/dist/templates/plugin-local/index.ts +39 -0
  105. package/dist/templates/plugin-standalone/CHANGELOG.md +1 -0
  106. package/dist/templates/plugin-standalone/README.ja.md +43 -0
  107. package/dist/templates/plugin-standalone/README.md +43 -0
  108. package/dist/templates/plugin-standalone/package.json +47 -0
  109. package/dist/templates/plugin-standalone/src/index.test.ts +16 -0
  110. package/dist/templates/plugin-standalone/src/index.ts +29 -0
  111. package/dist/templates/plugin-standalone/tsconfig.json +16 -0
  112. package/dist/templates/plugin-standalone/tsup.config.ts +8 -0
  113. package/package.json +1 -1
  114. package/dist/templates/_shared/app/(admin)/admin/sites/page.tsx +0 -5
  115. package/dist/templates/_shared/app/site/[siteId]/raw/[slug]/route.ts +0 -5
  116. package/dist/templates/_shared/components/i18n-provider.tsx +0 -15
  117. package/dist/templates/_shared/lib/admin-site-client.ts +0 -10
  118. package/dist/templates/_shared/lib/admin-site.ts +0 -12
  119. package/dist/templates/_shared/lib/amplify-server.ts +0 -7
  120. package/dist/templates/_shared/lib/auth-server.ts +0 -15
  121. package/dist/templates/_shared/lib/cn.ts +0 -5
  122. package/dist/templates/_shared/lib/i18n.ts +0 -35
  123. package/dist/templates/_shared/lib/kv-provider.ts +0 -7
  124. package/dist/templates/_shared/lib/media.ts +0 -6
  125. package/dist/templates/_shared/lib/posts-provider.ts +0 -7
  126. package/dist/templates/_shared/lib/posts-public.ts +0 -27
  127. package/dist/templates/_shared/lib/posts.ts +0 -12
  128. package/dist/templates/_shared/lib/seo.ts +0 -11
  129. package/dist/templates/_shared/lib/site-settings.ts +0 -11
  130. package/dist/templates/_shared/lib/storage.ts +0 -10
  131. package/dist/templates/_shared/lib/theme-actions.ts +0 -5
  132. package/dist/templates/_shared/lib/theme-active.ts +0 -10
  133. package/dist/templates/_shared/lib/theme-config.ts +0 -12
  134. package/dist/templates/_shared/lib/upload.ts +0 -6
  135. /package/dist/templates/_shared/app/{site/[siteId]/[slug] → [slug]}/page.tsx +0 -0
  136. /package/dist/templates/_shared/app/{site/[siteId]/feed.xml → feed.xml}/route.ts +0 -0
  137. /package/dist/templates/_shared/app/{site/[siteId]/og → og}/[slug]/route.ts +0 -0
  138. /package/dist/templates/_shared/app/{site/[siteId]/page.tsx → page.tsx} +0 -0
  139. /package/dist/templates/_shared/app/{site/[siteId]/sitemap.xml → sitemap.xml}/route.ts +0 -0
  140. /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, log as log3 } from "@clack/prompts";
5
- import { existsSync as existsSync4 } 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";
@@ -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";
@@ -103,6 +106,103 @@ function sharedTemplateDir() {
103
106
  function templatePath(theme) {
104
107
  return resolve(templatesDir, theme);
105
108
  }
109
+ function pluginTemplateDir(mode) {
110
+ return resolve(templatesDir, mode === "local" ? "plugin-local" : "plugin-standalone");
111
+ }
112
+
113
+ // src/gitignore.ts
114
+ var DEFAULT_GITIGNORE = `# Dependencies
115
+ node_modules/
116
+
117
+ # Next.js
118
+ .next/
119
+ next-env.d.ts
120
+
121
+ # Amplify
122
+ .amplify/
123
+ amplify_outputs.json
124
+
125
+ # TypeScript build artifacts
126
+ *.tsbuildinfo
127
+
128
+ # Env / OS noise
129
+ .env
130
+ .env.local
131
+ .env.*.local
132
+ .DS_Store
133
+
134
+ # Logs
135
+ npm-debug.log*
136
+ yarn-debug.log*
137
+ yarn-error.log*
138
+
139
+ # Editor
140
+ .vscode/
141
+ .idea/
142
+ `;
143
+
144
+ // src/themes-registry.ts
145
+ import { readdir } from "fs/promises";
146
+ import { existsSync as existsSync2 } from "fs";
147
+ import { join } from "path";
148
+ var CUSTOM_THEME_PREFIX = "my-";
149
+ function toIdentifier(themeName) {
150
+ if (!themeName.includes("-")) return themeName;
151
+ return themeName.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
152
+ }
153
+ function isCustomTheme(themeName) {
154
+ return themeName.startsWith(CUSTOM_THEME_PREFIX);
155
+ }
156
+ async function discoverInstalledThemes(destDir) {
157
+ const themesDir = join(destDir, "themes");
158
+ if (!existsSync2(themesDir)) return [];
159
+ const entries = await readdir(themesDir, { withFileTypes: true });
160
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
161
+ }
162
+ function buildRegistry(themes) {
163
+ const sorted = [...themes].sort((a, b) => {
164
+ const ca = isCustomTheme(a);
165
+ const cb = isCustomTheme(b);
166
+ if (ca !== cb) return ca ? 1 : -1;
167
+ return a.localeCompare(b);
168
+ });
169
+ const imports = sorted.map((t) => `import ${toIdentifier(t)} from '@/themes/${t}'`).join("\n");
170
+ const map = sorted.map((t) => {
171
+ const id = toIdentifier(t);
172
+ return id === t ? ` ${t},` : ` '${t}': ${id},`;
173
+ }).join("\n");
174
+ const fallback = sorted[0] ?? "blog";
175
+ return `// Generated by create-ampless. Lists every theme installed under
176
+ // \`themes/<name>/\`. Auto-managed: \`create-ampless upgrade\` and
177
+ // \`copy-theme\` regenerate this file from the directories present
178
+ // under \`themes/\`. Edits made by hand will survive only until the
179
+ // next upgrade.
180
+ //
181
+ // \`theme.active\` (per-site, in KvStore) picks which theme renders for
182
+ // each request; everything listed here gets bundled so switching is
183
+ // instant. Custom themes use the \`${CUSTOM_THEME_PREFIX}\` prefix and
184
+ // are never touched by upgrade \u2014 clone an ampless default theme into
185
+ // a \`${CUSTOM_THEME_PREFIX}<name>\` directory via \`copy-theme\` before
186
+ // modifying it.
187
+
188
+ ${imports}
189
+
190
+ export const themes = {
191
+ ${map}
192
+ } as const
193
+
194
+ export type ThemeName = keyof typeof themes
195
+
196
+ export const themeList = Object.values(themes)
197
+
198
+ /**
199
+ * Default theme used when a site has no \`theme.active\` override.
200
+ * Falls back to the first registered theme so an empty registry would
201
+ * still surface a clear runtime error rather than silent breakage.
202
+ */
203
+ export const DEFAULT_THEME: ThemeName = (themeList[0]?.name as ThemeName) ?? '${fallback}'
204
+ `;
205
+ }
106
206
 
107
207
  // src/scaffold.ts
108
208
  var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
@@ -131,10 +231,10 @@ async function substituteFile(filePath, vars) {
131
231
  if (replaced !== content) await writeFile(filePath, replaced, "utf-8");
132
232
  }
133
233
  async function substituteDir(dirPath, vars) {
134
- const entries = await readdir(dirPath, { withFileTypes: true });
234
+ const entries = await readdir2(dirPath, { withFileTypes: true });
135
235
  await Promise.all(
136
236
  entries.map(async (entry) => {
137
- const fullPath = join(dirPath, entry.name);
237
+ const fullPath = join2(dirPath, entry.name);
138
238
  if (entry.isDirectory()) {
139
239
  await substituteDir(fullPath, vars);
140
240
  } else {
@@ -143,30 +243,6 @@ async function substituteDir(dirPath, vars) {
143
243
  })
144
244
  );
145
245
  }
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
246
  async function scaffold(sharedDir, themesRoot, destDir, opts) {
171
247
  await cp(sharedDir, destDir, { recursive: true });
172
248
  const destThemesDir = resolve2(destDir, "themes");
@@ -180,6 +256,8 @@ async function scaffold(sharedDir, themesRoot, destDir, opts) {
180
256
  void themesRoot;
181
257
  const registryPath = resolve2(destDir, "themes-registry.ts");
182
258
  await writeFile(registryPath, buildRegistry(opts.themes), "utf-8");
259
+ const gitignorePath = resolve2(destDir, ".gitignore");
260
+ await writeFile(gitignorePath, DEFAULT_GITIGNORE, "utf-8");
183
261
  const vars = {
184
262
  projectName: opts.projectName,
185
263
  siteName: opts.siteName,
@@ -194,6 +272,20 @@ async function scaffold(sharedDir, themesRoot, destDir, opts) {
194
272
  // src/args.ts
195
273
  var VALID_THEMES = ["blog", "minimal", "landing", "corporate", "docs", "dads"];
196
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
+ ];
197
289
  var STRING_FLAGS = /* @__PURE__ */ new Set([
198
290
  "--site-name",
199
291
  "--themes",
@@ -204,21 +296,40 @@ var STRING_FLAGS = /* @__PURE__ */ new Set([
204
296
  "--aws-region",
205
297
  "--domain",
206
298
  "--subdomain",
207
- "--iam-service-role"
299
+ "--iam-service-role",
300
+ // Phase 5 `plugin <name>` flags
301
+ "--trust-level",
302
+ "--capabilities",
303
+ "--description"
208
304
  ]);
209
305
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
210
306
  "--deploy",
211
307
  "--mount",
308
+ "--upgrade",
309
+ "--dry-run",
310
+ "--no-install",
212
311
  "--github-private",
213
312
  "--create-iam-role",
214
313
  "--skip-confirm",
215
314
  "--help",
216
- "-h"
315
+ "-h",
316
+ // Phase 5 `plugin <name>` mode flags
317
+ "--local",
318
+ "--standalone",
319
+ // setup-encryption-key flags
320
+ "--gitignore"
217
321
  ]);
218
322
  function parseDeployArgs(argv) {
219
323
  const out = {
220
324
  deploy: false,
221
325
  mount: false,
326
+ upgrade: false,
327
+ copyTheme: false,
328
+ createPlugin: false,
329
+ setupEncryptionKey: false,
330
+ gitignore: false,
331
+ dryRun: false,
332
+ noInstall: false,
222
333
  githubPrivate: false,
223
334
  createIamRole: false,
224
335
  skipConfirm: false,
@@ -242,6 +353,15 @@ function parseDeployArgs(argv) {
242
353
  case "--mount":
243
354
  out.mount = true;
244
355
  break;
356
+ case "--upgrade":
357
+ out.upgrade = true;
358
+ break;
359
+ case "--dry-run":
360
+ out.dryRun = true;
361
+ break;
362
+ case "--no-install":
363
+ out.noInstall = true;
364
+ break;
245
365
  case "--github-private":
246
366
  out.githubPrivate = true;
247
367
  break;
@@ -255,6 +375,21 @@ function parseDeployArgs(argv) {
255
375
  case "-h":
256
376
  out.help = true;
257
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;
258
393
  }
259
394
  continue;
260
395
  }
@@ -310,6 +445,31 @@ function parseDeployArgs(argv) {
310
445
  case "--iam-service-role":
311
446
  out.iamServiceRole = value;
312
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;
313
473
  }
314
474
  continue;
315
475
  }
@@ -317,6 +477,34 @@ function parseDeployArgs(argv) {
317
477
  out.unknown.push(raw);
318
478
  continue;
319
479
  }
480
+ if (raw === "upgrade" && out.projectName === void 0) {
481
+ out.upgrade = true;
482
+ continue;
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
+ }
488
+ if (raw === "copy-theme" && out.projectName === void 0 && !out.copyTheme) {
489
+ out.copyTheme = true;
490
+ continue;
491
+ }
492
+ if (out.copyTheme && out.copyThemeSource === void 0) {
493
+ out.copyThemeSource = raw;
494
+ continue;
495
+ }
496
+ if (out.copyTheme && out.copyThemeTarget === void 0) {
497
+ out.copyThemeTarget = raw;
498
+ continue;
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
+ }
320
508
  if (out.projectName === void 0) {
321
509
  out.projectName = raw;
322
510
  } else {
@@ -328,8 +516,12 @@ function parseDeployArgs(argv) {
328
516
  var HELP_TEXT = `create-ampless \u2014 scaffold an ampless project
329
517
 
330
518
  Usage:
331
- npx create-ampless@alpha <project-name> [options]
332
- npx create-ampless@alpha --mount [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
333
525
 
334
526
  Options:
335
527
  --site-name <name> Site display name (default: "My Blog")
@@ -365,6 +557,75 @@ Options:
365
557
  --skip-confirm Skip all interactive prompts and use defaults /
366
558
  flag values (for CI / automation)
367
559
  -h, --help Show this message
560
+
561
+ upgrade Sync ampless package files / deps to latest alpha.
562
+ Run inside an existing ampless project.
563
+
564
+ --dry-run Show what would change without writing any files
565
+ --no-install Skip running pnpm/npm install after updating
566
+ package.json
567
+
568
+ copy-theme <source> <target>
569
+ Copy a theme so the original stays managed by ampless and
570
+ customisations live in the copy. Target must start with "my-"
571
+ (the convention that flags it as user-owned, so upgrade leaves
572
+ it alone). Run inside an existing ampless project.
573
+
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
368
629
  `;
369
630
 
370
631
  // src/deploy-prompts.ts
@@ -373,8 +634,8 @@ import { execa as execa3 } from "execa";
373
634
 
374
635
  // src/deploy.ts
375
636
  import { execa as execa2 } from "execa";
376
- import { existsSync as existsSync3 } from "fs";
377
- import { writeFile as writeFile2 } from "fs/promises";
637
+ import { existsSync as existsSync4 } from "fs";
638
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
378
639
  import { basename as basename2, resolve as resolve4 } from "path";
379
640
  import { spinner, log } from "@clack/prompts";
380
641
  import pc2 from "picocolors";
@@ -852,42 +1113,21 @@ function printPreflightReport(problems) {
852
1113
  }
853
1114
 
854
1115
  // src/mount.ts
855
- import { existsSync as existsSync2 } from "fs";
1116
+ import { existsSync as existsSync3 } from "fs";
856
1117
  import { resolve as resolve3 } from "path";
857
1118
  function validateMountableProject(destDir) {
858
1119
  const required = ["package.json", "cms.config.ts"];
859
1120
  for (const f of required) {
860
- if (!existsSync2(resolve3(destDir, f))) {
1121
+ if (!existsSync3(resolve3(destDir, f))) {
861
1122
  return `Not an ampless project (missing ${f} in ${destDir})`;
862
1123
  }
863
1124
  }
864
1125
  const amplifyFiles = ["amplify/backend.ts", "amplify/data/resource.ts"];
865
- if (!amplifyFiles.some((f) => existsSync2(resolve3(destDir, f)))) {
1126
+ if (!amplifyFiles.some((f) => existsSync3(resolve3(destDir, f)))) {
866
1127
  return `Not an ampless project (missing amplify/ in ${destDir})`;
867
1128
  }
868
1129
  return null;
869
1130
  }
870
- var MOUNT_DEFAULT_GITIGNORE = `# Dependencies
871
- node_modules/
872
-
873
- # Next.js
874
- .next/
875
- .amplify/
876
-
877
- # Amplify outputs (regenerated on every deploy / sandbox)
878
- amplify_outputs.json
879
-
880
- # Env / OS noise
881
- .env
882
- .env.local
883
- .env.*.local
884
- .DS_Store
885
-
886
- # Logs
887
- npm-debug.log*
888
- yarn-debug.log*
889
- yarn-error.log*
890
- `;
891
1131
  function originPointsAt(origin, owner, name) {
892
1132
  const candidates = [
893
1133
  `https://github.com/${owner}/${name}`,
@@ -986,6 +1226,23 @@ function splitDomain(domain, subdomain) {
986
1226
  const prefix = domain.slice(0, domain.length - registrable.length - 1);
987
1227
  return { registrable, subdomain: prefix };
988
1228
  }
1229
+ async function rewriteCmsConfigForDomain(projectDir, fullDomain) {
1230
+ const path = resolve4(projectDir, "cms.config.ts");
1231
+ if (!existsSync4(path)) {
1232
+ return { urlRewritten: false };
1233
+ }
1234
+ let content = await readFile2(path, "utf-8");
1235
+ let urlRewritten = false;
1236
+ const urlRe = /url:\s*['"]http:\/\/localhost:3000['"]/;
1237
+ if (urlRe.test(content)) {
1238
+ content = content.replace(urlRe, `url: 'https://${fullDomain}'`);
1239
+ urlRewritten = true;
1240
+ }
1241
+ if (urlRewritten) {
1242
+ await writeFile2(path, content, "utf-8");
1243
+ }
1244
+ return { urlRewritten };
1245
+ }
989
1246
  async function run(cmd, args, opts = {}) {
990
1247
  const { step, ...execaOpts } = opts;
991
1248
  try {
@@ -1026,8 +1283,8 @@ async function currentBranch(dir) {
1026
1283
  }
1027
1284
  async function ensureGitignore(dir) {
1028
1285
  const target = resolve4(dir, ".gitignore");
1029
- if (existsSync3(target)) return;
1030
- await writeFile2(target, MOUNT_DEFAULT_GITIGNORE, "utf-8");
1286
+ if (existsSync4(target)) return;
1287
+ await writeFile2(target, DEFAULT_GITIGNORE, "utf-8");
1031
1288
  }
1032
1289
  async function gitInitOrReuse(dir, mount) {
1033
1290
  const repo = await isGitRepo(dir);
@@ -1361,6 +1618,14 @@ async function runDeploy(opts) {
1361
1618
  }
1362
1619
  }
1363
1620
  await writeFile2(resolve4(opts.projectDir, "amplify.yml"), AMPLIFY_BUILD_SPEC, "utf-8");
1621
+ if (opts.domain) {
1622
+ const { registrable, subdomain } = splitDomain(opts.domain, opts.subdomain);
1623
+ const fullDomain = subdomain ? `${subdomain}.${registrable}` : registrable;
1624
+ const mutations = await rewriteCmsConfigForDomain(opts.projectDir, fullDomain);
1625
+ if (mutations.urlRewritten) {
1626
+ log.info(`cms.config.ts updated: site.url \u2192 https://${fullDomain}`);
1627
+ }
1628
+ }
1364
1629
  const created = {};
1365
1630
  const fail = (step, cause) => {
1366
1631
  const msg = cause instanceof Error ? cause.message : String(cause);
@@ -1605,15 +1870,924 @@ async function gatherDeployOptions(args, projectDir, projectName) {
1605
1870
  };
1606
1871
  }
1607
1872
 
1608
- // src/index.ts
1873
+ // src/upgrade.ts
1874
+ import { cp as cp2, readFile as readFile3, writeFile as writeFile3, readdir as readdir3, mkdir as mkdir2, rm as rm2 } from "fs/promises";
1875
+ import { existsSync as existsSync5 } from "fs";
1876
+ import { join as join3, relative, extname as extname2, dirname } from "path";
1877
+ import { log as log3, outro } from "@clack/prompts";
1609
1878
  import pc3 from "picocolors";
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 = [];
1889
+ var AMPLESS_PACKAGES = /* @__PURE__ */ new Set([
1890
+ "ampless",
1891
+ "@ampless/admin",
1892
+ "@ampless/backend",
1893
+ "@ampless/runtime",
1894
+ "@ampless/plugin-seo",
1895
+ "@ampless/plugin-rss",
1896
+ "@ampless/plugin-schema-jsonld",
1897
+ "@ampless/plugin-webhook",
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"
1904
+ ]);
1905
+ var AMPLESS_MANAGED_SCRIPTS = /* @__PURE__ */ new Set([
1906
+ "sandbox",
1907
+ "sandbox:dev",
1908
+ "update-ampless",
1909
+ "copy-theme"
1910
+ ]);
1911
+ var PROTECTED_PATTERNS = [
1912
+ /^cms\.config\.ts$/,
1913
+ // `themes/` and `themes-registry.ts` are handled separately — see
1914
+ // syncThemes() below. The themes directory is owned per-entry by
1915
+ // either ampless (default themes, resynced on upgrade) or the user
1916
+ // (`my-*` themes, preserved). The registry is regenerated to match
1917
+ // whatever is on disk afterward.
1918
+ /^\.env/,
1919
+ /^node_modules(\/|$)/,
1920
+ /^\.next(\/|$)/,
1921
+ /^\.turbo(\/|$)/,
1922
+ /^\.amplify(\/|$)/,
1923
+ /^amplify_outputs\.json$/,
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$/,
1935
+ /^tsconfig\.tsbuildinfo$/,
1936
+ /^pnpm-lock\.yaml$/,
1937
+ /^package-lock\.json$/,
1938
+ // Anything under themes/ is handled by syncThemes; the file-walk
1939
+ // classifier must not double-process it.
1940
+ /^themes(\/|$)/,
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(\/|$)/
1951
+ ];
1952
+ var SEED_IF_MISSING_PATTERN = /\.custom\.ts$|^plugins\/README(\.ja)?\.md$|^amplify\/secrets\/encryption-key\.ts$/;
1953
+ var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
1954
+ ".json",
1955
+ ".md",
1956
+ ".ts",
1957
+ ".tsx",
1958
+ ".js",
1959
+ ".jsx",
1960
+ ".mjs",
1961
+ ".cjs",
1962
+ ".html",
1963
+ ".css",
1964
+ ".env",
1965
+ ".txt",
1966
+ ".yaml",
1967
+ ".yml",
1968
+ ".toml",
1969
+ ".gitignore"
1970
+ ]);
1971
+ function isProtected(relPath) {
1972
+ if (SEED_IF_MISSING_PATTERN.test(relPath)) return false;
1973
+ return PROTECTED_PATTERNS.some((re) => re.test(relPath));
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
+ }
1986
+ async function listShippedThemes(templatesRoot) {
1987
+ if (!existsSync5(templatesRoot)) return [];
1988
+ const entries = await readdir3(templatesRoot, { withFileTypes: true });
1989
+ return entries.filter((e) => e.isDirectory() && isThemeDirName(e.name)).map((e) => e.name).sort();
1990
+ }
1991
+ var USER_OWNED_THEME_FILES = /* @__PURE__ */ new Set(["README.md", ".gitignore"]);
1992
+ function isUserOwnedThemeFile(path) {
1993
+ const name = path.split(/[/\\]/).pop() ?? "";
1994
+ return USER_OWNED_THEME_FILES.has(name);
1995
+ }
1996
+ async function captureUserOwnedFiles(dir) {
1997
+ const out = /* @__PURE__ */ new Map();
1998
+ if (!existsSync5(dir)) return out;
1999
+ for (const name of USER_OWNED_THEME_FILES) {
2000
+ const p3 = join3(dir, name);
2001
+ if (existsSync5(p3)) {
2002
+ out.set(name, await readFile3(p3));
2003
+ }
2004
+ }
2005
+ return out;
2006
+ }
2007
+ async function restorePreservedFiles(dir, files) {
2008
+ for (const [name, content] of files) {
2009
+ await mkdir2(dir, { recursive: true });
2010
+ await writeFile3(join3(dir, name), content);
2011
+ }
2012
+ }
2013
+ async function syncThemes(destDir, templatesRoot) {
2014
+ const shipped = await listShippedThemes(templatesRoot);
2015
+ const themesDir = join3(destDir, "themes");
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
+ }
2027
+ for (const name of shipped) {
2028
+ const src = join3(templatesRoot, name);
2029
+ const dst = join3(themesDir, name);
2030
+ const preservedFiles = await captureUserOwnedFiles(dst);
2031
+ await rm2(dst, { recursive: true, force: true });
2032
+ await cp2(src, dst, {
2033
+ recursive: true,
2034
+ filter: (sourcePath) => !isUserOwnedThemeFile(sourcePath)
2035
+ });
2036
+ await restorePreservedFiles(dst, preservedFiles);
2037
+ }
2038
+ const installed = await discoverInstalledThemes(destDir);
2039
+ const shippedSet = new Set(shipped);
2040
+ const preserved = installed.filter(
2041
+ (t) => !shippedSet.has(t) && !isQuarantinedThemeName(t)
2042
+ );
2043
+ const all = [...shipped, ...preserved];
2044
+ await writeFile3(join3(destDir, "themes-registry.ts"), buildRegistry(all), "utf-8");
2045
+ return { synced: shipped, preserved, quarantined };
2046
+ }
2047
+ async function walkDir(dir, base, out) {
2048
+ const entries = await readdir3(dir, { withFileTypes: true });
2049
+ for (const entry of entries) {
2050
+ const full = join3(dir, entry.name);
2051
+ const rel = relative(base, full);
2052
+ if (entry.isDirectory()) {
2053
+ await walkDir(full, base, out);
2054
+ } else {
2055
+ out.push(rel);
2056
+ }
2057
+ }
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
+ }
2122
+ function substituteVars(content, vars) {
2123
+ return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
2124
+ }
2125
+ async function copyWithSubstitution(src, dst, vars) {
2126
+ const ext = extname2(src) || (src.endsWith(".gitignore") ? ".gitignore" : "");
2127
+ await mkdir2(dirname(dst), { recursive: true });
2128
+ if (TEXT_EXTENSIONS2.has(ext)) {
2129
+ const content = await readFile3(src, "utf-8");
2130
+ await writeFile3(dst, substituteVars(content, vars), "utf-8");
2131
+ } else {
2132
+ const buf = await readFile3(src);
2133
+ await writeFile3(dst, buf);
2134
+ }
2135
+ }
2136
+ function detectIndent(jsonStr) {
2137
+ const m = jsonStr.match(/^{\n(\s+)/);
2138
+ if (!m) return 2;
2139
+ return m[1].length;
2140
+ }
2141
+ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
2142
+ const derivedRoot = opts.templatesRoot ?? join3(sharedDir, "..");
2143
+ const themeSyncEnabled = existsSync5(join3(derivedRoot, "_shared"));
2144
+ const templatesRoot = themeSyncEnabled ? derivedRoot : "";
2145
+ const problem = validateMountableProject(destDir);
2146
+ if (problem) {
2147
+ throw new Error(problem);
2148
+ }
2149
+ const projectPkgPath = join3(destDir, "package.json");
2150
+ const projectPkgRaw = await readFile3(projectPkgPath, "utf-8");
2151
+ const projectPkg = JSON.parse(projectPkgRaw);
2152
+ const projectName = typeof projectPkg.name === "string" && projectPkg.name ? projectPkg.name : "my-ampless-site";
2153
+ const vars = { projectName };
2154
+ const allRelPaths = [];
2155
+ await walkDir(sharedDir, sharedDir, allRelPaths);
2156
+ const classification = { replace: [], merge: [], seed: [], protected: [] };
2157
+ for (const rel of allRelPaths) {
2158
+ if (SEED_IF_MISSING_PATTERN.test(rel)) {
2159
+ classification.seed.push(rel);
2160
+ } else if (isProtected(rel)) {
2161
+ classification.protected.push(rel);
2162
+ } else if (rel === "package.json") {
2163
+ classification.merge.push(rel);
2164
+ } else {
2165
+ classification.replace.push(rel);
2166
+ }
2167
+ }
2168
+ const replaceNew = classification.replace.filter((r) => !existsSync5(join3(destDir, r)));
2169
+ const replaceUpdate = classification.replace.filter((r) => existsSync5(join3(destDir, r)));
2170
+ const seedNew = classification.seed.filter((r) => !existsSync5(join3(destDir, r)));
2171
+ const seedSkipped = classification.seed.filter((r) => existsSync5(join3(destDir, r)));
2172
+ const shippedThemes = themeSyncEnabled ? await listShippedThemes(templatesRoot) : [];
2173
+ const existingThemes = themeSyncEnabled ? await discoverInstalledThemes(destDir) : [];
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);
2179
+ log3.info(
2180
+ `replace: ${pc3.green(`${replaceNew.length} added`)} / ${pc3.yellow(`${replaceUpdate.length} updated`)}`
2181
+ );
2182
+ log3.info(`merge: ${pc3.cyan("package.json: sync ampless deps and managed scripts with the template")}`);
2183
+ if (classification.seed.length > 0) {
2184
+ log3.info(
2185
+ `seed: ${pc3.green(`${seedNew.length} added`)} / ${pc3.dim(`${seedSkipped.length} kept (existing *.custom.ts left untouched)`)}`
2186
+ );
2187
+ }
2188
+ if (themeSyncEnabled) {
2189
+ log3.info(
2190
+ `themes: ${pc3.cyan(`${shippedThemes.length} default themes synced`)} / ${pc3.dim(`${preservedThemes.length} custom (my-*) themes preserved`)}`
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)`);
2200
+ }
2201
+ log3.info(`protected: ${pc3.dim(`${classification.protected.length} template files left untouched`)}`);
2202
+ if (opts.dryRun) {
2203
+ return {
2204
+ added: replaceNew,
2205
+ updated: replaceUpdate,
2206
+ seeded: seedNew,
2207
+ protected: classification.protected,
2208
+ themesSynced: shippedThemes,
2209
+ themesPreserved: preservedThemes,
2210
+ themesQuarantined: quarantinedThemesPreview,
2211
+ packageJsonMerged: false,
2212
+ obsoleteRemoved: obsoleteFiles
2213
+ };
2214
+ }
2215
+ for (const rel of classification.replace) {
2216
+ const src = join3(sharedDir, rel);
2217
+ const dst = join3(destDir, rel);
2218
+ await copyWithSubstitution(src, dst, vars);
2219
+ }
2220
+ for (const rel of seedNew) {
2221
+ const src = join3(sharedDir, rel);
2222
+ const dst = join3(destDir, rel);
2223
+ await copyWithSubstitution(src, dst, vars);
2224
+ }
2225
+ const themeResult = themeSyncEnabled ? await syncThemes(destDir, templatesRoot) : { synced: [], preserved: [], quarantined: [] };
2226
+ await removeObsoleteFiles(destDir, obsoleteFiles);
2227
+ const templatePkgRaw = await readFile3(join3(sharedDir, "package.json"), "utf-8");
2228
+ const templatePkg = JSON.parse(templatePkgRaw);
2229
+ const indent = detectIndent(projectPkgRaw);
2230
+ for (const section of ["dependencies", "devDependencies"]) {
2231
+ const templateDeps = templatePkg[section] ?? {};
2232
+ const projectDeps = projectPkg[section] ?? {};
2233
+ for (const [name, version] of Object.entries(templateDeps)) {
2234
+ if (!AMPLESS_PACKAGES.has(name)) continue;
2235
+ projectDeps[name] = version;
2236
+ }
2237
+ if (Object.keys(projectDeps).length > 0) {
2238
+ projectPkg[section] = projectDeps;
2239
+ }
2240
+ }
2241
+ const templateScripts = templatePkg["scripts"] ?? {};
2242
+ const projectScripts = projectPkg["scripts"] ?? {};
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
+ }
2249
+ }
2250
+ if (Object.keys(projectScripts).length > 0) {
2251
+ projectPkg["scripts"] = projectScripts;
2252
+ }
2253
+ await writeFile3(projectPkgPath, JSON.stringify(projectPkg, null, indent) + "\n", "utf-8");
2254
+ if (!opts.noInstall) {
2255
+ const usePnpm = existsSync5(join3(destDir, "pnpm-lock.yaml"));
2256
+ const pm = usePnpm ? "pnpm" : "npm";
2257
+ await execa4(pm, ["install"], { cwd: destDir, stdio: "inherit" });
2258
+ }
2259
+ return {
2260
+ added: replaceNew,
2261
+ updated: replaceUpdate,
2262
+ seeded: seedNew,
2263
+ protected: classification.protected,
2264
+ themesSynced: themeResult.synced,
2265
+ themesPreserved: themeResult.preserved,
2266
+ themesQuarantined: themeResult.quarantined,
2267
+ packageJsonMerged: true,
2268
+ obsoleteRemoved: obsoleteFiles
2269
+ };
2270
+ }
2271
+ async function runUpgrade(args) {
2272
+ const destDir = process.cwd();
2273
+ try {
2274
+ await runUpgradeIn(destDir, sharedTemplateDir(), {
2275
+ dryRun: args.dryRun,
2276
+ noInstall: args.noInstall
2277
+ });
2278
+ } catch (err) {
2279
+ log3.error(err instanceof Error ? err.message : String(err));
2280
+ process.exit(1);
2281
+ }
2282
+ if (args.dryRun) {
2283
+ outro(`${pc3.dim("(dry-run) No files were changed.")}`);
2284
+ return;
2285
+ }
2286
+ outro(
2287
+ `${pc3.green("\u2714")} Upgrade complete
2288
+
2289
+ Next steps:
2290
+ ${pc3.cyan("git diff")} ${pc3.dim("# review changes")}
2291
+ ${pc3.cyan("git commit && git push")} ${pc3.dim("# deploy via Amplify Hosting")}`
2292
+ );
2293
+ }
2294
+
2295
+ // src/copy-theme.ts
2296
+ import { cp as cp3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2297
+ import { existsSync as existsSync6 } from "fs";
2298
+ import { join as join4 } from "path";
2299
+ import { log as log4, outro as outro2 } from "@clack/prompts";
2300
+ import pc4 from "picocolors";
2301
+ async function runCopyThemeIn(destDir, source, target) {
2302
+ const problem = validateMountableProject(destDir);
2303
+ if (problem) {
2304
+ throw new Error(problem);
2305
+ }
2306
+ if (!isCustomTheme(target)) {
2307
+ throw new Error(
2308
+ `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.`
2309
+ );
2310
+ }
2311
+ if (source === target) {
2312
+ throw new Error(`Source and target are identical (${source}).`);
2313
+ }
2314
+ const themesDir = join4(destDir, "themes");
2315
+ const sourceDir = join4(themesDir, source);
2316
+ const targetDir = join4(themesDir, target);
2317
+ if (!existsSync6(sourceDir)) {
2318
+ throw new Error(`Source theme not found: themes/${source}/`);
2319
+ }
2320
+ if (existsSync6(targetDir)) {
2321
+ throw new Error(`Target theme already exists: themes/${target}/`);
2322
+ }
2323
+ await cp3(sourceDir, targetDir, { recursive: true });
2324
+ const filesRewritten = await rewriteThemeName(targetDir, source, target);
2325
+ const installed = await discoverInstalledThemes(destDir);
2326
+ await writeFile4(join4(destDir, "themes-registry.ts"), buildRegistry(installed), "utf-8");
2327
+ return { source, target, filesRewritten };
2328
+ }
2329
+ async function rewriteThemeName(targetDir, source, target) {
2330
+ const touched = [];
2331
+ await rewriteFile(
2332
+ join4(targetDir, "index.ts"),
2333
+ (s) => s.replace(new RegExp(`name:\\s*'${escapeRegex(source)}'`), `name: '${target}'`).replace(new RegExp(`name:\\s*"${escapeRegex(source)}"`), `name: "${target}"`),
2334
+ touched
2335
+ );
2336
+ await rewriteFile(
2337
+ join4(targetDir, "manifest.ts"),
2338
+ (s) => s.replace(new RegExp(`name:\\s*'${escapeRegex(source)}'`), `name: '${target}'`).replace(new RegExp(`name:\\s*"${escapeRegex(source)}"`), `name: "${target}"`),
2339
+ touched
2340
+ );
2341
+ await rewriteFile(
2342
+ join4(targetDir, "tokens.css"),
2343
+ (s) => s.replace(new RegExp(`\\[data-theme='${escapeRegex(source)}'\\]`, "g"), `[data-theme='${target}']`).replace(new RegExp(`\\[data-theme="${escapeRegex(source)}"\\]`, "g"), `[data-theme="${target}"]`),
2344
+ touched
2345
+ );
2346
+ return touched;
2347
+ }
2348
+ async function rewriteFile(path, transform, touched) {
2349
+ if (!existsSync6(path)) return;
2350
+ const before = await readFile4(path, "utf-8");
2351
+ const after = transform(before);
2352
+ if (after === before) return;
2353
+ await writeFile4(path, after, "utf-8");
2354
+ touched.push(path);
2355
+ }
2356
+ function escapeRegex(s) {
2357
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2358
+ }
2359
+ async function runCopyTheme(args) {
2360
+ const destDir = process.cwd();
2361
+ const source = args.copyThemeSource;
2362
+ const target = args.copyThemeTarget;
2363
+ if (!source || !target) {
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");
2366
+ process.exit(1);
2367
+ }
2368
+ try {
2369
+ const result = await runCopyThemeIn(destDir, source, target);
2370
+ outro2(
2371
+ `${pc4.green("\u2714")} Copied themes/${result.source}/ \u2192 themes/${result.target}/
2372
+
2373
+ Files rewritten:
2374
+ ` + result.filesRewritten.map((f) => ` ${pc4.dim(f)}`).join("\n") + `
2375
+
2376
+ themes-registry.ts updated. Next:
2377
+ ${pc4.cyan(`Open themes/${result.target}/ and start customising`)}
2378
+ ${pc4.cyan(`Activate via /admin/sites/default/theme`)}`
2379
+ );
2380
+ } catch (err) {
2381
+ log4.error(err instanceof Error ? err.message : String(err));
2382
+ process.exit(1);
2383
+ }
2384
+ }
2385
+
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";
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";
2783
+ var DEFAULT_THEMES = VALID_THEMES;
1610
2784
  function buildNonInteractiveOpts(args) {
1611
2785
  const projectName = args.projectName ?? (() => {
1612
- const b = basename3(process.cwd());
2786
+ const b = basename4(process.cwd());
1613
2787
  return b && b !== "/" ? b : "my-ampless-site";
1614
2788
  })();
1615
2789
  const siteName = args.siteName ?? "My Blog";
1616
- const themes = args.themes ?? ["blog"];
2790
+ const themes = args.themes ?? [...DEFAULT_THEMES];
1617
2791
  const plugins = args.plugins ?? ["seo"];
1618
2792
  return {
1619
2793
  projectName,
@@ -1630,18 +2804,18 @@ function warnIgnoredScaffoldFlags(args) {
1630
2804
  if (args.plugins) ignored.push("--plugins");
1631
2805
  if (args.projectName) ignored.push("<project-name> positional");
1632
2806
  if (ignored.length > 0) {
1633
- log3.warn(`--mount mode ignores: ${ignored.join(", ")}`);
2807
+ log7.warn(`--mount mode ignores: ${ignored.join(", ")}`);
1634
2808
  }
1635
2809
  }
1636
2810
  async function runMount(args) {
1637
2811
  const destDir = process.cwd();
1638
- const projectName = basename3(destDir);
2812
+ const projectName = basename4(destDir);
1639
2813
  warnIgnoredScaffoldFlags(args);
1640
2814
  const problem = validateMountableProject(destDir);
1641
2815
  if (problem) {
1642
- log3.error(problem);
1643
- log3.info(
1644
- "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."
1645
2819
  );
1646
2820
  process.exit(1);
1647
2821
  }
@@ -1654,23 +2828,23 @@ async function runMount(args) {
1654
2828
  if (err instanceof PreflightError) {
1655
2829
  process.exit(1);
1656
2830
  }
1657
- log3.error(err instanceof Error ? err.message : String(err));
2831
+ log7.error(err instanceof Error ? err.message : String(err));
1658
2832
  process.exit(1);
1659
2833
  }
1660
2834
  }
1661
2835
  function printDeployResult(result) {
1662
2836
  const lines = [
1663
- `${pc3.green("\u2714")} Project deployed`,
2837
+ `${pc7.green("\u2714")} Project deployed`,
1664
2838
  ``,
1665
- ` GitHub: ${pc3.cyan(result.githubRepoUrl)}`,
1666
- ` Amplify app: ${pc3.cyan(result.amplifyAppId)}`,
1667
- ` Amplify URL: ${pc3.cyan(result.amplifyAppUrl)}`
2839
+ ` GitHub: ${pc7.cyan(result.githubRepoUrl)}`,
2840
+ ` Amplify app: ${pc7.cyan(result.amplifyAppId)}`,
2841
+ ` Amplify URL: ${pc7.cyan(result.amplifyAppUrl)}`
1668
2842
  ];
1669
2843
  if (result.domainUrl) {
1670
- lines.push(` Custom domain: ${pc3.cyan(result.domainUrl)}`);
2844
+ lines.push(` Custom domain: ${pc7.cyan(result.domainUrl)}`);
1671
2845
  }
1672
2846
  if (result.domainVerification && result.domainVerification.length > 0) {
1673
- lines.push("", ` ${pc3.bold("Add these DNS records to verify the domain:")}`);
2847
+ lines.push("", ` ${pc7.bold("Add these DNS records to verify the domain:")}`);
1674
2848
  for (const v of result.domainVerification) {
1675
2849
  lines.push(` ${v.cname} CNAME ${v.target}`);
1676
2850
  }
@@ -1678,9 +2852,9 @@ function printDeployResult(result) {
1678
2852
  lines.push(
1679
2853
  "",
1680
2854
  ` First build is now running in Amplify Hosting.`,
1681
- ` Watch it at ${pc3.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}`)}`
1682
2856
  );
1683
- outro(lines.join("\n"));
2857
+ outro5(lines.join("\n"));
1684
2858
  }
1685
2859
  async function main() {
1686
2860
  const args = parseDeployArgs(process.argv.slice(2));
@@ -1689,7 +2863,23 @@ async function main() {
1689
2863
  return;
1690
2864
  }
1691
2865
  for (const flag of args.unknown) {
1692
- log3.warn(`Unknown argument ignored: ${flag}`);
2866
+ log7.warn(`Unknown argument ignored: ${flag}`);
2867
+ }
2868
+ if (args.upgrade) {
2869
+ await runUpgrade(args);
2870
+ return;
2871
+ }
2872
+ if (args.copyTheme) {
2873
+ await runCopyTheme(args);
2874
+ return;
2875
+ }
2876
+ if (args.createPlugin) {
2877
+ await runCreatePlugin(args);
2878
+ return;
2879
+ }
2880
+ if (args.setupEncryptionKey) {
2881
+ await runSetupEncryptionKey(args);
2882
+ return;
1693
2883
  }
1694
2884
  if (args.mount) {
1695
2885
  await runMount(args);
@@ -1703,8 +2893,8 @@ async function main() {
1703
2893
  }
1704
2894
  if (!opts) return;
1705
2895
  const destDir = resolve5(process.cwd(), opts.projectName);
1706
- if (existsSync4(destDir)) {
1707
- log3.error(`Directory already exists: ${destDir}`);
2896
+ if (existsSync9(destDir)) {
2897
+ log7.error(`Directory already exists: ${destDir}`);
1708
2898
  process.exit(1);
1709
2899
  }
1710
2900
  const sharedDir = sharedTemplateDir();
@@ -1715,7 +2905,7 @@ async function main() {
1715
2905
  s.stop("Done!");
1716
2906
  } catch (err) {
1717
2907
  s.stop("Failed.");
1718
- log3.error(String(err));
2908
+ log7.error(String(err));
1719
2909
  process.exit(1);
1720
2910
  }
1721
2911
  if (args.deploy) {
@@ -1728,19 +2918,18 @@ async function main() {
1728
2918
  if (err instanceof PreflightError) {
1729
2919
  process.exit(1);
1730
2920
  }
1731
- log3.error(err instanceof Error ? err.message : String(err));
2921
+ log7.error(err instanceof Error ? err.message : String(err));
1732
2922
  process.exit(1);
1733
2923
  }
1734
2924
  return;
1735
2925
  }
1736
- outro(
1737
- `${pc3.green("\u2714")} Project created at ${pc3.bold(opts.projectName)}
2926
+ outro5(
2927
+ `${pc7.green("\u2714")} Project created at ${pc7.bold(opts.projectName)}
1738
2928
 
1739
2929
  Next steps:
1740
- ${pc3.cyan("cd")} ${opts.projectName}
1741
- ${pc3.cyan("npm install")}
1742
- ${pc3.cyan("npx ampx sandbox")} ${pc3.dim("# start Amplify backend")}
1743
- ${pc3.cyan("npm run dev")} ${pc3.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")}`
1744
2933
  );
1745
2934
  }
1746
2935
  main().catch((err) => {