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.
- package/README.ja.md +77 -0
- package/README.md +6 -3
- package/dist/index.js +1282 -93
- package/dist/templates/_shared/AGENTS.ja.md +94 -0
- package/dist/templates/_shared/AGENTS.md +94 -0
- package/dist/templates/_shared/README.ja.md +239 -0
- package/dist/templates/_shared/README.md +239 -0
- package/dist/templates/_shared/RUNBOOK.ja.md +120 -0
- package/dist/templates/_shared/RUNBOOK.md +32 -58
- package/dist/templates/_shared/THEMES.ja.md +495 -0
- package/dist/templates/_shared/THEMES.md +523 -0
- package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +7 -0
- package/dist/templates/_shared/amplify/backend.custom.ts +41 -0
- package/dist/templates/_shared/amplify/backend.ts +20 -2
- package/dist/templates/_shared/amplify/data/get-media-by-src.js +45 -0
- package/dist/templates/_shared/amplify/data/get-published-post.js +9 -12
- package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +6 -9
- package/dist/templates/_shared/amplify/data/list-published-posts.js +10 -11
- package/dist/templates/_shared/amplify/data/resource.custom.ts +32 -0
- package/dist/templates/_shared/amplify/data/resource.ts +32 -15
- package/dist/templates/_shared/amplify/events/dispatcher/resource.ts +1 -0
- package/dist/templates/_shared/amplify/events/processor-trusted/resource.ts +1 -0
- package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +5 -0
- package/dist/templates/_shared/amplify/functions/api-key-renewer/resource.ts +1 -0
- package/dist/templates/_shared/amplify/functions/mcp-handler/handler.ts +1 -0
- package/dist/templates/_shared/amplify/functions/mcp-handler/resource.ts +12 -0
- package/dist/templates/_shared/amplify/functions/plugin-secret-handler/handler.ts +11 -0
- package/dist/templates/_shared/amplify/functions/plugin-secret-handler/resource.ts +12 -0
- package/dist/templates/_shared/amplify/functions/user-admin/handler.ts +4 -0
- package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +13 -0
- package/dist/templates/_shared/amplify/secrets/.gitkeep +0 -0
- package/dist/templates/_shared/amplify/secrets/encryption-key.ts +9 -0
- package/dist/templates/_shared/app/(admin)/admin/mcp-tokens/page.tsx +5 -0
- package/dist/templates/_shared/app/(admin)/admin/plugins/page.tsx +5 -0
- package/dist/templates/_shared/app/(admin)/admin/users/page.tsx +5 -0
- package/dist/templates/_shared/app/globals.css +55 -39
- package/dist/templates/_shared/app/layout.tsx +51 -16
- package/dist/templates/_shared/app/providers.tsx +0 -2
- package/dist/templates/_shared/app/raw/[slug]/route.ts +10 -0
- package/dist/templates/_shared/app/static/[slug]/[[...path]]/route.ts +14 -0
- package/dist/templates/_shared/cms.config.ts +64 -23
- package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +3 -4
- package/dist/templates/_shared/components/tag-list.tsx +9 -2
- package/dist/templates/_shared/components.json +1 -1
- package/dist/templates/_shared/docs/plugin-author-guide.ja.md +934 -0
- package/dist/templates/_shared/docs/plugin-author-guide.md +1336 -0
- package/dist/templates/_shared/lib/admin.ts +12 -12
- package/dist/templates/_shared/lib/ampless.ts +2 -8
- package/dist/templates/_shared/lib/amplify.ts +0 -5
- package/dist/templates/_shared/package.json +51 -40
- package/dist/templates/_shared/plugins/README.ja.md +139 -0
- package/dist/templates/_shared/plugins/README.md +143 -0
- package/dist/templates/_shared/proxy.ts +23 -8
- package/dist/templates/blog/README.ja.md +22 -0
- package/dist/templates/blog/README.md +17 -47
- package/dist/templates/blog/manifest.ts +1 -1
- package/dist/templates/blog/pages/feed.ts +4 -5
- package/dist/templates/blog/pages/home.tsx +10 -15
- package/dist/templates/blog/pages/post.tsx +42 -14
- package/dist/templates/blog/pages/sitemap.ts +4 -5
- package/dist/templates/blog/pages/tag.tsx +8 -10
- package/dist/templates/blog/tokens.css +26 -40
- package/dist/templates/corporate/README.ja.md +18 -0
- package/dist/templates/corporate/README.md +12 -14
- package/dist/templates/corporate/pages/feed.ts +3 -4
- package/dist/templates/corporate/pages/home.tsx +7 -12
- package/dist/templates/corporate/pages/post.tsx +20 -14
- package/dist/templates/corporate/pages/sitemap.ts +3 -4
- package/dist/templates/corporate/pages/tag.tsx +8 -10
- package/dist/templates/corporate/tokens.css +17 -39
- package/dist/templates/dads/README.ja.md +31 -0
- package/dist/templates/dads/README.md +13 -17
- package/dist/templates/dads/pages/feed.ts +3 -4
- package/dist/templates/dads/pages/home.tsx +7 -12
- package/dist/templates/dads/pages/post.tsx +20 -14
- package/dist/templates/dads/pages/sitemap.ts +3 -4
- package/dist/templates/dads/pages/tag.tsx +8 -10
- package/dist/templates/dads/tokens.css +22 -42
- package/dist/templates/docs/README.ja.md +24 -0
- package/dist/templates/docs/README.md +10 -13
- package/dist/templates/docs/pages/feed.ts +3 -4
- package/dist/templates/docs/pages/home.tsx +6 -9
- package/dist/templates/docs/pages/post.tsx +19 -13
- package/dist/templates/docs/pages/sitemap.ts +3 -4
- package/dist/templates/docs/pages/tag.tsx +8 -10
- package/dist/templates/docs/tokens.css +17 -39
- package/dist/templates/landing/README.ja.md +20 -0
- package/dist/templates/landing/README.md +14 -19
- package/dist/templates/landing/pages/feed.ts +4 -5
- package/dist/templates/landing/pages/home.tsx +7 -12
- package/dist/templates/landing/pages/post.tsx +20 -14
- package/dist/templates/landing/pages/sitemap.ts +3 -4
- package/dist/templates/landing/pages/tag.tsx +8 -10
- package/dist/templates/landing/tokens.css +17 -39
- package/dist/templates/minimal/README.ja.md +14 -0
- package/dist/templates/minimal/README.md +9 -47
- package/dist/templates/minimal/pages/feed.ts +4 -5
- package/dist/templates/minimal/pages/home.tsx +6 -8
- package/dist/templates/minimal/pages/post.tsx +19 -12
- package/dist/templates/minimal/pages/sitemap.ts +4 -5
- package/dist/templates/minimal/pages/tag.tsx +7 -8
- package/dist/templates/minimal/tokens.css +17 -39
- package/dist/templates/plugin-local/README.md +34 -0
- package/dist/templates/plugin-local/index.ts +39 -0
- package/dist/templates/plugin-standalone/CHANGELOG.md +1 -0
- package/dist/templates/plugin-standalone/README.ja.md +43 -0
- package/dist/templates/plugin-standalone/README.md +43 -0
- package/dist/templates/plugin-standalone/package.json +47 -0
- package/dist/templates/plugin-standalone/src/index.test.ts +16 -0
- package/dist/templates/plugin-standalone/src/index.ts +29 -0
- package/dist/templates/plugin-standalone/tsconfig.json +16 -0
- package/dist/templates/plugin-standalone/tsup.config.ts +8 -0
- package/package.json +1 -1
- package/dist/templates/_shared/app/(admin)/admin/sites/page.tsx +0 -5
- package/dist/templates/_shared/app/site/[siteId]/raw/[slug]/route.ts +0 -5
- package/dist/templates/_shared/components/i18n-provider.tsx +0 -15
- package/dist/templates/_shared/lib/admin-site-client.ts +0 -10
- package/dist/templates/_shared/lib/admin-site.ts +0 -12
- package/dist/templates/_shared/lib/amplify-server.ts +0 -7
- package/dist/templates/_shared/lib/auth-server.ts +0 -15
- package/dist/templates/_shared/lib/cn.ts +0 -5
- package/dist/templates/_shared/lib/i18n.ts +0 -35
- package/dist/templates/_shared/lib/kv-provider.ts +0 -7
- package/dist/templates/_shared/lib/media.ts +0 -6
- package/dist/templates/_shared/lib/posts-provider.ts +0 -7
- package/dist/templates/_shared/lib/posts-public.ts +0 -27
- package/dist/templates/_shared/lib/posts.ts +0 -12
- package/dist/templates/_shared/lib/seo.ts +0 -11
- package/dist/templates/_shared/lib/site-settings.ts +0 -11
- package/dist/templates/_shared/lib/storage.ts +0 -10
- package/dist/templates/_shared/lib/theme-actions.ts +0 -5
- package/dist/templates/_shared/lib/theme-active.ts +0 -10
- package/dist/templates/_shared/lib/theme-config.ts +0 -12
- package/dist/templates/_shared/lib/upload.ts +0 -6
- /package/dist/templates/_shared/app/{site/[siteId]/[slug] → [slug]}/page.tsx +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/feed.xml → feed.xml}/route.ts +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/og → og}/[slug]/route.ts +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/page.tsx → page.tsx} +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/sitemap.xml → sitemap.xml}/route.ts +0 -0
- /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
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { basename as
|
|
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
|
|
234
|
+
const entries = await readdir2(dirPath, { withFileTypes: true });
|
|
135
235
|
await Promise.all(
|
|
136
236
|
entries.map(async (entry) => {
|
|
137
|
-
const fullPath =
|
|
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@
|
|
332
|
-
npx create-ampless@
|
|
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
|
|
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
|
|
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 (!
|
|
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) =>
|
|
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 (
|
|
1030
|
-
await writeFile2(target,
|
|
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/
|
|
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 =
|
|
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 ?? [
|
|
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
|
-
|
|
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 =
|
|
2812
|
+
const projectName = basename4(destDir);
|
|
1639
2813
|
warnIgnoredScaffoldFlags(args);
|
|
1640
2814
|
const problem = validateMountableProject(destDir);
|
|
1641
2815
|
if (problem) {
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
"Run `npx create-ampless@
|
|
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
|
-
|
|
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
|
-
`${
|
|
2837
|
+
`${pc7.green("\u2714")} Project deployed`,
|
|
1664
2838
|
``,
|
|
1665
|
-
` GitHub: ${
|
|
1666
|
-
` Amplify app: ${
|
|
1667
|
-
` Amplify URL: ${
|
|
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: ${
|
|
2844
|
+
lines.push(` Custom domain: ${pc7.cyan(result.domainUrl)}`);
|
|
1671
2845
|
}
|
|
1672
2846
|
if (result.domainVerification && result.domainVerification.length > 0) {
|
|
1673
|
-
lines.push("", ` ${
|
|
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 ${
|
|
2855
|
+
` Watch it at ${pc7.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
|
|
1682
2856
|
);
|
|
1683
|
-
|
|
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
|
-
|
|
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 (
|
|
1707
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2921
|
+
log7.error(err instanceof Error ? err.message : String(err));
|
|
1732
2922
|
process.exit(1);
|
|
1733
2923
|
}
|
|
1734
2924
|
return;
|
|
1735
2925
|
}
|
|
1736
|
-
|
|
1737
|
-
`${
|
|
2926
|
+
outro5(
|
|
2927
|
+
`${pc7.green("\u2714")} Project created at ${pc7.bold(opts.projectName)}
|
|
1738
2928
|
|
|
1739
2929
|
Next steps:
|
|
1740
|
-
${
|
|
1741
|
-
${
|
|
1742
|
-
${
|
|
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) => {
|