@notionx/create-notionx-app 1.0.0
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.md +139 -0
- package/dist/answers.js +332 -0
- package/dist/answers.js.map +1 -0
- package/dist/cli-notionx.js +388 -0
- package/dist/cli-notionx.js.map +1 -0
- package/dist/cli-notionx.test.js +277 -0
- package/dist/cli-notionx.test.js.map +1 -0
- package/dist/diff.js +40 -0
- package/dist/diff.js.map +1 -0
- package/dist/diff.test.js +90 -0
- package/dist/diff.test.js.map +1 -0
- package/dist/index.js +99 -0
- package/dist/index.js.map +1 -0
- package/dist/locale-add/apply.js +39 -0
- package/dist/locale-add/apply.js.map +1 -0
- package/dist/locale-add/format.js +38 -0
- package/dist/locale-add/format.js.map +1 -0
- package/dist/locale-add/list.js +44 -0
- package/dist/locale-add/list.js.map +1 -0
- package/dist/locale-add/list.test.js +45 -0
- package/dist/locale-add/list.test.js.map +1 -0
- package/dist/locale-add/plan.js +128 -0
- package/dist/locale-add/plan.js.map +1 -0
- package/dist/locale-add/validate.js +46 -0
- package/dist/locale-add/validate.js.map +1 -0
- package/dist/metadata.js +41 -0
- package/dist/metadata.js.map +1 -0
- package/dist/notion-translation-sources/apply.js +61 -0
- package/dist/notion-translation-sources/apply.js.map +1 -0
- package/dist/notion-translation-sources/index.js +3 -0
- package/dist/notion-translation-sources/index.js.map +1 -0
- package/dist/notion-translation-sources/plan.js +33 -0
- package/dist/notion-translation-sources/plan.js.map +1 -0
- package/dist/notionx-source.js +142 -0
- package/dist/notionx-source.js.map +1 -0
- package/dist/notionx-source.test.js +144 -0
- package/dist/notionx-source.test.js.map +1 -0
- package/dist/password.js +18 -0
- package/dist/password.js.map +1 -0
- package/dist/presets.js +83 -0
- package/dist/presets.js.map +1 -0
- package/dist/presets.test.js +50 -0
- package/dist/presets.test.js.map +1 -0
- package/dist/prompt.js +218 -0
- package/dist/prompt.js.map +1 -0
- package/dist/provision/cloudflare.js +236 -0
- package/dist/provision/cloudflare.js.map +1 -0
- package/dist/provision/dependencies.js +219 -0
- package/dist/provision/dependencies.js.map +1 -0
- package/dist/provision/index.js +681 -0
- package/dist/provision/index.js.map +1 -0
- package/dist/provision/index.test.js +54 -0
- package/dist/provision/index.test.js.map +1 -0
- package/dist/provision/inspect.js +109 -0
- package/dist/provision/inspect.js.map +1 -0
- package/dist/provision/inspect.test.js +75 -0
- package/dist/provision/inspect.test.js.map +1 -0
- package/dist/provision/notion.js +1981 -0
- package/dist/provision/notion.js.map +1 -0
- package/dist/provision/notion.test.js +542 -0
- package/dist/provision/notion.test.js.map +1 -0
- package/dist/provision/ntn-credentials.js +198 -0
- package/dist/provision/ntn-credentials.js.map +1 -0
- package/dist/provision/options.js +15 -0
- package/dist/provision/options.js.map +1 -0
- package/dist/provision/password-hash.js +78 -0
- package/dist/provision/password-hash.js.map +1 -0
- package/dist/provision/prompts.js +115 -0
- package/dist/provision/prompts.js.map +1 -0
- package/dist/provision/repair.js +48 -0
- package/dist/provision/repair.js.map +1 -0
- package/dist/provision/repair.test.js +141 -0
- package/dist/provision/repair.test.js.map +1 -0
- package/dist/provision/shell.js +84 -0
- package/dist/provision/shell.js.map +1 -0
- package/dist/provision/wire.js +78 -0
- package/dist/provision/wire.js.map +1 -0
- package/dist/registry/doctor.js +181 -0
- package/dist/registry/doctor.js.map +1 -0
- package/dist/registry/doctor.test.js +180 -0
- package/dist/registry/doctor.test.js.map +1 -0
- package/dist/registry/install.js +217 -0
- package/dist/registry/install.js.map +1 -0
- package/dist/registry/install.test.js +168 -0
- package/dist/registry/install.test.js.map +1 -0
- package/dist/registry/load-registry.js +24 -0
- package/dist/registry/load-registry.js.map +1 -0
- package/dist/registry/load-registry.test.js +59 -0
- package/dist/registry/load-registry.test.js.map +1 -0
- package/dist/registry/migration-planner.js +204 -0
- package/dist/registry/migration-planner.js.map +1 -0
- package/dist/registry/migration-planner.test.js +340 -0
- package/dist/registry/migration-planner.test.js.map +1 -0
- package/dist/registry/migrations-store.js +125 -0
- package/dist/registry/migrations-store.js.map +1 -0
- package/dist/registry/migrations-store.test.js +163 -0
- package/dist/registry/migrations-store.test.js.map +1 -0
- package/dist/registry/migrations-types.js +25 -0
- package/dist/registry/migrations-types.js.map +1 -0
- package/dist/registry/project-meta.js +84 -0
- package/dist/registry/project-meta.js.map +1 -0
- package/dist/registry/registry-items.js +354 -0
- package/dist/registry/registry-items.js.map +1 -0
- package/dist/registry/registry-items.test.js +99 -0
- package/dist/registry/registry-items.test.js.map +1 -0
- package/dist/registry/registry-store.js +232 -0
- package/dist/registry/registry-store.js.map +1 -0
- package/dist/registry/registry-store.test.js +136 -0
- package/dist/registry/registry-store.test.js.map +1 -0
- package/dist/registry/registry-types.js +18 -0
- package/dist/registry/registry-types.js.map +1 -0
- package/dist/registry/registry-types.test.js +146 -0
- package/dist/registry/registry-types.test.js.map +1 -0
- package/dist/registry/render-content-source-files.js +158 -0
- package/dist/registry/render-content-source-files.js.map +1 -0
- package/dist/registry/render-multi-source.js +296 -0
- package/dist/registry/render-multi-source.js.map +1 -0
- package/dist/registry/render-multi-source.test.js +110 -0
- package/dist/registry/render-multi-source.test.js.map +1 -0
- package/dist/registry/text-utils.js +42 -0
- package/dist/registry/text-utils.js.map +1 -0
- package/dist/registry/uninstall.js +250 -0
- package/dist/registry/uninstall.js.map +1 -0
- package/dist/registry/uninstall.test.js +264 -0
- package/dist/registry/uninstall.test.js.map +1 -0
- package/dist/registry/update.js +280 -0
- package/dist/registry/update.js.map +1 -0
- package/dist/registry/update.test.js +229 -0
- package/dist/registry/update.test.js.map +1 -0
- package/dist/render.js +549 -0
- package/dist/render.js.map +1 -0
- package/dist/render.test.js +414 -0
- package/dist/render.test.js.map +1 -0
- package/dist/templates/.dev.vars.example.tmpl +32 -0
- package/dist/templates/.gitignore.tmpl +58 -0
- package/dist/templates/README.md.tmpl +417 -0
- package/dist/templates/app/[slug]/page.tsx.tmpl +55 -0
- package/dist/templates/app/admin/account/page.tsx.tmpl +18 -0
- package/dist/templates/app/admin/content-models/page.tsx.tmpl +6 -0
- package/dist/templates/app/admin/layout.tsx.tmpl +90 -0
- package/dist/templates/app/admin/loading.tsx.tmpl +6 -0
- package/dist/templates/app/admin/page.tsx.tmpl +17 -0
- package/dist/templates/app/api/auth/google/callback/route.ts.tmpl +3 -0
- package/dist/templates/app/api/auth/google/route.ts.tmpl +3 -0
- package/dist/templates/app/api/auth/verify-email/route.ts.tmpl +3 -0
- package/dist/templates/app/api/auth/viewer/route.ts.tmpl +3 -0
- package/dist/templates/app/api/health/route.ts.tmpl +3 -0
- package/dist/templates/app/api/{{contentSourceId}}/[slug]/route.ts.tmpl +27 -0
- package/dist/templates/app/api/{{contentSourceId}}/route.ts.tmpl +18 -0
- package/dist/templates/app/globals.css.tmpl +109 -0
- package/dist/templates/app/layout.tsx.tmpl +56 -0
- package/dist/templates/app/login/page.tsx.tmpl +154 -0
- package/dist/templates/app/page.fallback.tsx.tmpl +31 -0
- package/dist/templates/app/page.tsx.tmpl +42 -0
- package/dist/templates/app/register/page.tsx.tmpl +138 -0
- package/dist/templates/app/{{contentSourceListPath}}/[slug]/page.tsx.tmpl +113 -0
- package/dist/templates/app/{{contentSourceListPath}}/page.tsx.tmpl +74 -0
- package/dist/templates/components/content/post-card.tsx.tmpl +80 -0
- package/dist/templates/components/notion-blocks.tsx.tmpl +668 -0
- package/dist/templates/components/page-blocks/feature-grid-block.tsx.tmpl +68 -0
- package/dist/templates/components/page-blocks/hero-block.tsx.tmpl +73 -0
- package/dist/templates/components/page-blocks/latest-posts-block.tsx.tmpl +59 -0
- package/dist/templates/components/page-blocks/story-block.tsx.tmpl +70 -0
- package/dist/templates/components/page-blocks.fallback.tsx.tmpl +17 -0
- package/dist/templates/components/page-blocks.tsx.tmpl +32 -0
- package/dist/templates/components/search/search-dialog.tsx.tmpl +171 -0
- package/dist/templates/components/site/locale-switcher.tsx.tmpl +65 -0
- package/dist/templates/components/site/site-footer.tsx.tmpl +106 -0
- package/dist/templates/components/site/site-header.tsx.tmpl +80 -0
- package/dist/templates/components/site/site-shell.tsx.tmpl +20 -0
- package/dist/templates/components/site/theme-bootstrap.tsx.tmpl +51 -0
- package/dist/templates/components/theme-provider.tsx.tmpl +14 -0
- package/dist/templates/components/theme-toggle.tsx.tmpl +38 -0
- package/dist/templates/components/ui/accordion.tsx.tmpl +56 -0
- package/dist/templates/components/ui/alert.tsx.tmpl +59 -0
- package/dist/templates/components/ui/aspect-ratio.tsx.tmpl +8 -0
- package/dist/templates/components/ui/avatar.tsx.tmpl +44 -0
- package/dist/templates/components/ui/badge.tsx.tmpl +33 -0
- package/dist/templates/components/ui/button.tsx.tmpl +56 -0
- package/dist/templates/components/ui/card.tsx.tmpl +61 -0
- package/dist/templates/components/ui/checkbox.tsx.tmpl +28 -0
- package/dist/templates/components/ui/dialog.tsx.tmpl +104 -0
- package/dist/templates/components/ui/dropdown-menu.tsx.tmpl +183 -0
- package/dist/templates/components/ui/input.tsx.tmpl +21 -0
- package/dist/templates/components/ui/label.tsx.tmpl +25 -0
- package/dist/templates/components/ui/popover.tsx.tmpl +30 -0
- package/dist/templates/components/ui/radio-group.tsx.tmpl +44 -0
- package/dist/templates/components/ui/select.tsx.tmpl +150 -0
- package/dist/templates/components/ui/separator.tsx.tmpl +30 -0
- package/dist/templates/components/ui/sheet.tsx.tmpl +125 -0
- package/dist/templates/components/ui/skeleton.tsx.tmpl +15 -0
- package/dist/templates/components/ui/sonner.tsx.tmpl +30 -0
- package/dist/templates/components/ui/switch.tsx.tmpl +29 -0
- package/dist/templates/components/ui/table.tsx.tmpl +107 -0
- package/dist/templates/components/ui/tabs.tsx.tmpl +55 -0
- package/dist/templates/components/ui/textarea.tsx.tmpl +24 -0
- package/dist/templates/components/ui/tooltip.tsx.tmpl +30 -0
- package/dist/templates/components.json.tmpl +21 -0
- package/dist/templates/env.d.ts.tmpl +32 -0
- package/dist/templates/lib/admin/actions.ts.tmpl +43 -0
- package/dist/templates/lib/admin/context.tsx.tmpl +209 -0
- package/dist/templates/lib/admin/nav.ts.tmpl +23 -0
- package/dist/templates/lib/auth.config.fallback.ts.tmpl +10 -0
- package/dist/templates/lib/auth.config.ts.tmpl +45 -0
- package/dist/templates/lib/blocks/translations.ts.tmpl +44 -0
- package/dist/templates/lib/blog/translations.ts.tmpl +52 -0
- package/dist/templates/lib/content/models.ts.tmpl +53 -0
- package/dist/templates/lib/i18n/config.ts.tmpl +18 -0
- package/dist/templates/lib/i18n/index.ts.tmpl +1 -0
- package/dist/templates/lib/locale-contract/built-in.ts.tmpl +19 -0
- package/dist/templates/lib/locale-contract/index.ts.tmpl +3 -0
- package/dist/templates/lib/locale-contract/paths.ts.tmpl +29 -0
- package/dist/templates/lib/pages/model.ts.tmpl +16 -0
- package/dist/templates/lib/pages/source.ts.tmpl +566 -0
- package/dist/templates/lib/pages/translations.ts.tmpl +34 -0
- package/dist/templates/lib/search/config.fallback.ts.tmpl +11 -0
- package/dist/templates/lib/search/config.ts.tmpl +25 -0
- package/dist/templates/lib/site/config.ts.tmpl +120 -0
- package/dist/templates/lib/site/request-env.ts.tmpl +71 -0
- package/dist/templates/lib/site/settings.fallback.ts.tmpl +21 -0
- package/dist/templates/lib/site/settings.ts.tmpl +320 -0
- package/dist/templates/lib/site/translations.ts.tmpl +30 -0
- package/dist/templates/lib/utils.ts.tmpl +9 -0
- package/dist/templates/migrations/0001_init.sql.tmpl +57 -0
- package/dist/templates/migrations/0002_admin_seed.sql.tmpl +30 -0
- package/dist/templates/migrations/0003_search_index.sql.tmpl +29 -0
- package/dist/templates/next.config.ts.tmpl +18 -0
- package/dist/templates/package.json.tmpl +40 -0
- package/dist/templates/shims/cloudflare-workers-empty.mjs +4 -0
- package/dist/templates/shims/next-headers-empty.mjs +4 -0
- package/dist/templates/tests/smoke.test.ts.tmpl +83 -0
- package/dist/templates/tsconfig.json.tmpl +31 -0
- package/dist/templates/vite.config.ts.tmpl +53 -0
- package/dist/templates/vitest.config.ts.tmpl +13 -0
- package/dist/templates/worker/index.ts.tmpl +52 -0
- package/dist/templates/wrangler.jsonc.tmpl +44 -0
- package/dist/ui-presets.js +60 -0
- package/dist/ui-presets.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
// packages/create-notionx-app/src/provision/index.ts
|
|
2
|
+
//
|
|
3
|
+
// Orchestrates the post-render provisioning flow:
|
|
4
|
+
// 1. Verify wrangler auth (required)
|
|
5
|
+
// 2. Create / reuse D1, KV, R2 (idempotent)
|
|
6
|
+
// 3. Wire real bindings, then create D1 tables locally via
|
|
7
|
+
// `d1 migrations apply --local`
|
|
8
|
+
// 4. Turnstile, Resend, and Google OAuth are intentionally
|
|
9
|
+
// skipped here — users wire them up manually after scaffold.
|
|
10
|
+
// 5. Create Notion data source if `NOTION_API_TOKEN` is set,
|
|
11
|
+
// otherwise prompt (interactive mode only)
|
|
12
|
+
// 6. Wire everything into `wrangler.jsonc` + `.dev.vars` +
|
|
13
|
+
// `wrangler secret put` for secrets
|
|
14
|
+
// 7. Print a status card with ✅ / ⚠️ per item + repair commands
|
|
15
|
+
//
|
|
16
|
+
// Every step is best-effort and degrades gracefully. The scaffolded
|
|
17
|
+
// project is usable even if all optional steps are skipped — the user
|
|
18
|
+
// can re-run individual commands from the printed status card.
|
|
19
|
+
import * as p from "@clack/prompts";
|
|
20
|
+
import { runOrThrow, run, runInteractive } from "./shell.js";
|
|
21
|
+
import { defaultProvisionMode } from "./options.js";
|
|
22
|
+
import { requireWranglerAuth, ensureD1, ensureKV, ensureR2, setWorkerSecret, } from "./cloudflare.js";
|
|
23
|
+
import { isNtnAvailable, verifyNotionToken, ensureNotionDatabase, ensurePagesDatabase, ensureBlocksDatabase, ensureSiteSettingsDatabase, } from "./notion.js";
|
|
24
|
+
import { promptNotion } from "./prompts.js";
|
|
25
|
+
import { patchSiteUrl, patchWranglerJsonc, writeDevVars, } from "./wire.js";
|
|
26
|
+
import { ensureDependencies } from "./dependencies.js";
|
|
27
|
+
import { readNtnToken, isNtnLoggedIn, describeNtnSource, } from "./ntn-credentials.js";
|
|
28
|
+
export async function provision(answers, projectDir, options) {
|
|
29
|
+
const mode = options.mode ?? defaultProvisionMode("create");
|
|
30
|
+
const result = {
|
|
31
|
+
d1: { ok: false },
|
|
32
|
+
kv: { ok: false },
|
|
33
|
+
vinextKv: { ok: false },
|
|
34
|
+
r2: { ok: false },
|
|
35
|
+
turnstile: { ok: false },
|
|
36
|
+
notion: { ok: false },
|
|
37
|
+
siteSettings: { ok: false, skipped: true },
|
|
38
|
+
resend: { ok: false, enabled: false },
|
|
39
|
+
google: { ok: false, enabled: false },
|
|
40
|
+
migrationsApplied: false,
|
|
41
|
+
deploy: { ok: false, skipped: true },
|
|
42
|
+
admin: {
|
|
43
|
+
ok: true,
|
|
44
|
+
email: answers.adminEmail,
|
|
45
|
+
message: "seed migration generated",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
// The project uses kebab-case for resource names.
|
|
49
|
+
const slug = answers.projectName.toLowerCase();
|
|
50
|
+
const d1Name = `${slug}-db`;
|
|
51
|
+
const r2Name = `${slug}-assets`;
|
|
52
|
+
// ---- 0. External CLI tools (wrangler, ntn) ----
|
|
53
|
+
// Make sure wrangler/ntn are on PATH at a usable version before we
|
|
54
|
+
// try to drive them. Missing tools get installed (with a prompt in
|
|
55
|
+
// interactive mode).
|
|
56
|
+
const deps = await ensureDependencies(undefined, {
|
|
57
|
+
interactive: options.interactive,
|
|
58
|
+
});
|
|
59
|
+
for (const dep of deps) {
|
|
60
|
+
if (!dep.available) {
|
|
61
|
+
p.log.warn(`${dep.name}: unavailable — related steps will be skipped.`);
|
|
62
|
+
}
|
|
63
|
+
else if (dep.needsUpgrade) {
|
|
64
|
+
p.log.warn(`${dep.name} ${dep.version} is older than required ${dep.minVersion} — some steps may fail.`);
|
|
65
|
+
}
|
|
66
|
+
else if (dep.installedNow) {
|
|
67
|
+
p.log.success(`${dep.name} ${dep.version ?? ""} ready${dep.installedNow ? " (just installed)" : ""}.`);
|
|
68
|
+
}
|
|
69
|
+
else if (dep.version) {
|
|
70
|
+
p.log.success(`${dep.name} ${dep.version} ready.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ---- 1. Wrangler auth ----
|
|
74
|
+
try {
|
|
75
|
+
const acc = await requireWranglerAuthWithOptionalLogin(options.interactive);
|
|
76
|
+
p.log.success(`Cloudflare: logged in (account ${acc.id.slice(0, 8)}…)`);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
80
|
+
p.log.error(`Cloudflare: ${message}`);
|
|
81
|
+
p.log.info("Re-run after `wrangler login`. You can still use the project; just create D1/KV/R2 by hand.");
|
|
82
|
+
result.turnstile = {
|
|
83
|
+
ok: false,
|
|
84
|
+
skipped: true,
|
|
85
|
+
message: "skipped until Cloudflare login",
|
|
86
|
+
};
|
|
87
|
+
result.notion = {
|
|
88
|
+
ok: false,
|
|
89
|
+
skipped: true,
|
|
90
|
+
message: "skipped because Cloudflare provisioning did not start",
|
|
91
|
+
};
|
|
92
|
+
return finalize(result, projectDir, slug);
|
|
93
|
+
}
|
|
94
|
+
// ---- 2-3. D1 / KV / R2 ----
|
|
95
|
+
try {
|
|
96
|
+
const r = await ensureD1(d1Name);
|
|
97
|
+
result.d1 = { ok: true, id: r.databaseId, created: r.created };
|
|
98
|
+
p.log.success(`D1: ${r.created ? "created" : "reused"} ${d1Name} (${r.databaseId.slice(0, 8)}…)`);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
result.d1 = {
|
|
102
|
+
ok: false,
|
|
103
|
+
message: err instanceof Error ? err.message : String(err),
|
|
104
|
+
};
|
|
105
|
+
p.log.error(`D1: ${result.d1.message}`);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const r = await ensureKV("CONTENT_CACHE");
|
|
109
|
+
result.kv = { ok: true, id: r.namespaceId, created: r.created };
|
|
110
|
+
p.log.success(`KV: ${r.created ? "created" : "reused"} CONTENT_CACHE (${r.namespaceId.slice(0, 8)}…)`);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
result.kv = {
|
|
114
|
+
ok: false,
|
|
115
|
+
message: err instanceof Error ? err.message : String(err),
|
|
116
|
+
};
|
|
117
|
+
p.log.error(`KV: ${result.kv.message}`);
|
|
118
|
+
}
|
|
119
|
+
// Second KV namespace: vinext@0.1.1's deploy check requires a
|
|
120
|
+
// `VINEXT_KV_CACHE` binding whenever a route uses ISR / `revalidate`.
|
|
121
|
+
// Skipping this would surface a hard deploy-time error from
|
|
122
|
+
// `vinext deploy` even though `pnpm install` would have succeeded.
|
|
123
|
+
try {
|
|
124
|
+
const r = await ensureKV("VINEXT_KV_CACHE");
|
|
125
|
+
result.vinextKv = { ok: true, id: r.namespaceId, created: r.created };
|
|
126
|
+
p.log.success(`KV: ${r.created ? "created" : "reused"} VINEXT_KV_CACHE (${r.namespaceId.slice(0, 8)}…)`);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
result.vinextKv = {
|
|
130
|
+
ok: false,
|
|
131
|
+
message: err instanceof Error ? err.message : String(err),
|
|
132
|
+
};
|
|
133
|
+
p.log.error(`KV (vinext cache): ${result.vinextKv.message}`);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const r = await ensureR2(r2Name);
|
|
137
|
+
result.r2 = { ok: true, name: r.bucketName, created: r.created };
|
|
138
|
+
p.log.success(`R2: ${r.created ? "created" : "reused"} ${r2Name}`);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
result.r2 = {
|
|
142
|
+
ok: false,
|
|
143
|
+
message: err instanceof Error ? err.message : String(err),
|
|
144
|
+
};
|
|
145
|
+
p.log.error(`R2: ${result.r2.message}`);
|
|
146
|
+
}
|
|
147
|
+
// ---- 4. Turnstile ----
|
|
148
|
+
// Skipped silently during scaffolding. The generated project still
|
|
149
|
+
// ships with full Turnstile support — an unset secret is a no-op
|
|
150
|
+
// in the auth flow. Users can wire the widget manually later (see
|
|
151
|
+
// README) or set CLOUDFLARE_API_TOKEN and re-run the relevant
|
|
152
|
+
// helper. No log, no prompt, no auto-create here.
|
|
153
|
+
result.turnstile = {
|
|
154
|
+
ok: false,
|
|
155
|
+
skipped: true,
|
|
156
|
+
message: "skipped during scaffolding (configure manually later)",
|
|
157
|
+
};
|
|
158
|
+
// ---- 5. Notion ----
|
|
159
|
+
// Token resolution order:
|
|
160
|
+
// 1. `NOTION_API_TOKEN` env var (explicit, highest priority)
|
|
161
|
+
// 2. `ntn` CLI's local credentials (keychain / auth.json)
|
|
162
|
+
// 3. Interactive `secret_…` paste (only when interactive and no
|
|
163
|
+
// auto-source found)
|
|
164
|
+
// Regardless of source, we still need a parent page id, which we
|
|
165
|
+
// always prompt for interactively.
|
|
166
|
+
try {
|
|
167
|
+
const envToken = process.env.NOTION_API_TOKEN?.trim();
|
|
168
|
+
let autoToken = null;
|
|
169
|
+
let resolvedToken = envToken || null;
|
|
170
|
+
if (!resolvedToken) {
|
|
171
|
+
autoToken = await readNtnToken();
|
|
172
|
+
if (autoToken) {
|
|
173
|
+
resolvedToken = autoToken.token;
|
|
174
|
+
p.log.success(`Notion: auto-detected credentials (${describeNtnSource(autoToken.source)})`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
// Give a useful hint about the fastest path forward.
|
|
178
|
+
const ntnLoggedIn = await isNtnLoggedIn();
|
|
179
|
+
if (!ntnLoggedIn) {
|
|
180
|
+
p.log.info("Notion: no credentials detected. Run `ntn login` once to skip the token prompt, or paste a `secret_…` token below.");
|
|
181
|
+
autoToken = await promptNtnLogin(options.interactive);
|
|
182
|
+
if (autoToken) {
|
|
183
|
+
resolvedToken = autoToken.token;
|
|
184
|
+
p.log.success(`Notion: auto-detected credentials (${describeNtnSource(autoToken.source)})`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (resolvedToken) {
|
|
190
|
+
const ok = await verifyNotionToken(resolvedToken);
|
|
191
|
+
if (!ok)
|
|
192
|
+
throw new Error("Notion token failed verification");
|
|
193
|
+
const ntn = await isNtnAvailable();
|
|
194
|
+
if (!ntn) {
|
|
195
|
+
throw new Error("`ntn` CLI not installed. Run: npm i -g ntn@latest");
|
|
196
|
+
}
|
|
197
|
+
// Resolution order for parent page + seed count:
|
|
198
|
+
// 1. `answers.notionParentPage` (--notion-parent-page flag)
|
|
199
|
+
// 2. Interactive prompt (only when stdin is a TTY)
|
|
200
|
+
// 3. Skip silently
|
|
201
|
+
let notionInputs = null;
|
|
202
|
+
if (answers.notionParentPage) {
|
|
203
|
+
notionInputs = {
|
|
204
|
+
apiToken: resolvedToken,
|
|
205
|
+
parentPageId: answers.notionParentPage,
|
|
206
|
+
seedCount: answers.notionSeedCount,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
notionInputs = await promptNotion({ interactive: options.interactive }, answers.contentSource.fields, resolvedToken, answers.notionSeedCount);
|
|
211
|
+
}
|
|
212
|
+
if (notionInputs) {
|
|
213
|
+
const { content, pages, blocks } = await provisionNotionContentAndPages({
|
|
214
|
+
answers,
|
|
215
|
+
apiToken: notionInputs.apiToken,
|
|
216
|
+
parentPageId: notionInputs.parentPageId,
|
|
217
|
+
seedCount: notionInputs.seedCount,
|
|
218
|
+
});
|
|
219
|
+
result.notion = {
|
|
220
|
+
ok: true,
|
|
221
|
+
dataSourceId: content.dataSourceId,
|
|
222
|
+
pagesDataSourceId: pages.dataSourceId,
|
|
223
|
+
blocksDataSourceId: blocks.dataSourceId,
|
|
224
|
+
seeded: content.seeded,
|
|
225
|
+
pagesSeeded: pages.seeded,
|
|
226
|
+
blocksSeeded: blocks.seeded,
|
|
227
|
+
...(autoToken
|
|
228
|
+
? { message: `token from ${describeNtnSource(autoToken.source)}` }
|
|
229
|
+
: {}),
|
|
230
|
+
};
|
|
231
|
+
p.log.success(`Notion: content ${content.dataSourceId.slice(0, 8)}… seeded ${content.seeded}; Pages ${pages.dataSourceId.slice(0, 8)}… seeded ${pages.seeded}; Blocks ${blocks.dataSourceId.slice(0, 8)}… seeded ${blocks.seeded}.`);
|
|
232
|
+
result._notionToken = resolvedToken;
|
|
233
|
+
result._blocksDataSourceId = blocks.dataSourceId;
|
|
234
|
+
// Site settings: separate data source for site-level config
|
|
235
|
+
// (name, tagline, description, default locale, social image).
|
|
236
|
+
// Created alongside the main content source — same parent
|
|
237
|
+
// page, same Notion token, separate `NOTION_SITE_SETTINGS_…`
|
|
238
|
+
// env var. Disable with `--no-site-settings`.
|
|
239
|
+
if (answers.enableSiteSettings) {
|
|
240
|
+
const settings = await ensureSiteSettingsDatabase({
|
|
241
|
+
apiToken: notionInputs.apiToken,
|
|
242
|
+
parentPageId: notionInputs.parentPageId,
|
|
243
|
+
projectName: answers.projectName,
|
|
244
|
+
description: "A Notion-powered site built on @notionx/core, running on Cloudflare Workers with D1, R2, and Cloudflare Images.",
|
|
245
|
+
defaultLocale: answers.defaultLocale,
|
|
246
|
+
});
|
|
247
|
+
result.siteSettings = {
|
|
248
|
+
ok: true,
|
|
249
|
+
dataSourceId: settings.dataSourceId,
|
|
250
|
+
url: settings.url,
|
|
251
|
+
seeded: settings.seeded,
|
|
252
|
+
reused: settings.reused,
|
|
253
|
+
};
|
|
254
|
+
result._siteSettingsDataSourceId = settings.dataSourceId;
|
|
255
|
+
p.log.success(`Notion site settings: ${settings.reused ? "reused" : "created"} (${settings.dataSourceId.slice(0, 8)}…), seeded ${settings.seeded} page.`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
result.notion = {
|
|
260
|
+
ok: false,
|
|
261
|
+
skipped: true,
|
|
262
|
+
message: "Notion: token present but no parent page provided.",
|
|
263
|
+
};
|
|
264
|
+
p.log.warn("Notion: skipped (no parent page id).");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// No env, no auto-detected ntn credentials. Fall back to the
|
|
269
|
+
// interactive paste prompt.
|
|
270
|
+
const notion = await promptNotion({ interactive: options.interactive }, answers.contentSource.fields, undefined, answers.notionSeedCount);
|
|
271
|
+
if (notion) {
|
|
272
|
+
const { content, pages, blocks } = await provisionNotionContentAndPages({
|
|
273
|
+
answers,
|
|
274
|
+
apiToken: notion.apiToken,
|
|
275
|
+
parentPageId: notion.parentPageId,
|
|
276
|
+
seedCount: notion.seedCount,
|
|
277
|
+
});
|
|
278
|
+
result.notion = {
|
|
279
|
+
ok: true,
|
|
280
|
+
dataSourceId: content.dataSourceId,
|
|
281
|
+
pagesDataSourceId: pages.dataSourceId,
|
|
282
|
+
blocksDataSourceId: blocks.dataSourceId,
|
|
283
|
+
seeded: content.seeded,
|
|
284
|
+
pagesSeeded: pages.seeded,
|
|
285
|
+
blocksSeeded: blocks.seeded,
|
|
286
|
+
};
|
|
287
|
+
p.log.success(`Notion: content ${content.dataSourceId.slice(0, 8)}… seeded ${content.seeded}; Pages ${pages.dataSourceId.slice(0, 8)}… seeded ${pages.seeded}; Blocks ${blocks.dataSourceId.slice(0, 8)}… seeded ${blocks.seeded}.`);
|
|
288
|
+
result._notionToken = notion.apiToken;
|
|
289
|
+
result._blocksDataSourceId = blocks.dataSourceId;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
result.notion = {
|
|
293
|
+
ok: false,
|
|
294
|
+
skipped: true,
|
|
295
|
+
message: "Notion: set NOTION_API_TOKEN (and rerun), or run `ntn login` once to skip the prompt.",
|
|
296
|
+
};
|
|
297
|
+
p.log.warn("Notion: skipped.");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
result.notion = {
|
|
303
|
+
ok: false,
|
|
304
|
+
message: err instanceof Error ? err.message : String(err),
|
|
305
|
+
};
|
|
306
|
+
p.log.error(`Notion: ${result.notion.message}`);
|
|
307
|
+
}
|
|
308
|
+
// ---- 6. Optional Resend + Google ----
|
|
309
|
+
// Skipped during scaffolding — no prompt, no auto-create. Users
|
|
310
|
+
// wire these up manually after the project is generated (see the
|
|
311
|
+
// project README). Result rows are still surfaced in the status
|
|
312
|
+
// card so the operator knows the integration is intentionally
|
|
313
|
+
// disabled.
|
|
314
|
+
result.resend = { ok: true, enabled: false, message: "skipped (configure manually later)" };
|
|
315
|
+
result.google = { ok: true, enabled: false, message: "skipped (configure manually later)" };
|
|
316
|
+
// ---- 7. Wire everything into wrangler.jsonc + .dev.vars ----
|
|
317
|
+
let wireInputs = null;
|
|
318
|
+
if (result.d1.ok && result.kv.ok && result.vinextKv.ok) {
|
|
319
|
+
const currentWireInputs = {
|
|
320
|
+
d1DatabaseId: result.d1.id,
|
|
321
|
+
kvNamespaceId: result.kv.id,
|
|
322
|
+
vinextKvNamespaceId: result.vinextKv.id,
|
|
323
|
+
turnstileSitekey: result.turnstile.sitekey,
|
|
324
|
+
turnstileSecret: result.turnstile.ok ? result.turnstile.secret : undefined,
|
|
325
|
+
notionToken: result._notionToken,
|
|
326
|
+
notionDataSourceId: result.notion.dataSourceId,
|
|
327
|
+
notionPagesDataSourceId: result.notion.pagesDataSourceId,
|
|
328
|
+
notionSiteSettingsDataSourceId: result._siteSettingsDataSourceId,
|
|
329
|
+
notionBlocksDataSourceId: result._blocksDataSourceId,
|
|
330
|
+
};
|
|
331
|
+
wireInputs = currentWireInputs;
|
|
332
|
+
try {
|
|
333
|
+
await patchWranglerJsonc(projectDir, currentWireInputs);
|
|
334
|
+
await writeDevVars(projectDir, currentWireInputs);
|
|
335
|
+
p.log.success(`Wired: wrangler.jsonc + .dev.vars updated.`);
|
|
336
|
+
if (result.d1.ok && result.d1.id) {
|
|
337
|
+
try {
|
|
338
|
+
await runOrThrow("wrangler", ["d1", "migrations", "apply", d1Name, "--local"], { cwd: projectDir });
|
|
339
|
+
result.migrationsApplied = true;
|
|
340
|
+
p.log.success(`D1 migrations: applied to local store`);
|
|
341
|
+
await runOrThrow("wrangler", [
|
|
342
|
+
"d1",
|
|
343
|
+
"execute",
|
|
344
|
+
d1Name,
|
|
345
|
+
"--local",
|
|
346
|
+
"--file",
|
|
347
|
+
"migrations/0002_admin_seed.sql",
|
|
348
|
+
], { cwd: projectDir });
|
|
349
|
+
p.log.success(`D1 admin seed: refreshed locally`);
|
|
350
|
+
}
|
|
351
|
+
catch (migrationErr) {
|
|
352
|
+
const msg = migrationErr instanceof Error
|
|
353
|
+
? migrationErr.message
|
|
354
|
+
: String(migrationErr);
|
|
355
|
+
p.log.warn(`D1 migrations: ${msg}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
p.log.error(`Wiring failed: ${err instanceof Error ? err.message : err}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// ---- 6. Admin account ----
|
|
364
|
+
// The admin user is seeded by `migrations/0002_admin_seed.sql`,
|
|
365
|
+
// which we hash-rendered at template time. The migration runs as
|
|
366
|
+
// part of step 3 (D1 migrations apply), so all we do here is mark
|
|
367
|
+
// the row "ok" and surface the email in the status card. If
|
|
368
|
+
// migrations were not applied (e.g. wrangler missing), we still
|
|
369
|
+
// treat this as ok — the SQL is on disk and will run on the user's
|
|
370
|
+
// first `wrangler d1 migrations apply` after `pnpm install`.
|
|
371
|
+
result.admin = {
|
|
372
|
+
ok: true,
|
|
373
|
+
email: answers.adminEmail,
|
|
374
|
+
message: "seed refreshed via 0002_admin_seed.sql",
|
|
375
|
+
};
|
|
376
|
+
if (!mode.deploy) {
|
|
377
|
+
result.deploy = {
|
|
378
|
+
ok: false,
|
|
379
|
+
workerName: slug,
|
|
380
|
+
skipped: true,
|
|
381
|
+
message: "deploy disabled for this provisioning mode",
|
|
382
|
+
};
|
|
383
|
+
return finalize(result, projectDir, slug);
|
|
384
|
+
}
|
|
385
|
+
// ---- 7. Deploy ----
|
|
386
|
+
// Goal: one scaffolder command = a live `https://<name>.<subdomain>.workers.dev`
|
|
387
|
+
// URL the user can visit. Steps in order:
|
|
388
|
+
// a. `pnpm install` — produces node_modules + the worker bundle
|
|
389
|
+
// that wrangler can upload. Skip on failure (caller will need
|
|
390
|
+
// to run it manually anyway).
|
|
391
|
+
// b. `wrangler d1 migrations apply <db> --remote` — pushes
|
|
392
|
+
// 0001_init.sql + 0002_admin_seed.sql to the live D1 database
|
|
393
|
+
// we just created. Without this, the deployed worker has no
|
|
394
|
+
// schema and the admin user cannot log in.
|
|
395
|
+
// c. `vinext deploy` — the project's own deploy command. It
|
|
396
|
+
// builds the bundle and calls `wrangler deploy` under the
|
|
397
|
+
// hood. We capture stdout to find the workers.dev URL.
|
|
398
|
+
// If any step fails, we don't try the next one — surface a hint
|
|
399
|
+
// in the status card so the user can run them by hand.
|
|
400
|
+
try {
|
|
401
|
+
const install = await run("pnpm", ["install", "--prefer-offline"], {
|
|
402
|
+
cwd: projectDir,
|
|
403
|
+
});
|
|
404
|
+
if (install.code !== 0) {
|
|
405
|
+
throw new Error(`pnpm install failed (exit ${install.code}); run it manually inside ${projectDir}`);
|
|
406
|
+
}
|
|
407
|
+
p.log.success("pnpm install: done.");
|
|
408
|
+
const d1Id = result.d1.id;
|
|
409
|
+
if (!d1Id)
|
|
410
|
+
throw new Error("no D1 id available; cannot apply migrations");
|
|
411
|
+
const migrate = await run("pnpm", ["exec", "wrangler", "d1", "migrations", "apply", d1Name, "--remote"], { cwd: projectDir });
|
|
412
|
+
if (migrate.code !== 0) {
|
|
413
|
+
const tail = (migrate.stderr || migrate.stdout).trim().split("\n").slice(-6).join("\n");
|
|
414
|
+
throw new Error(`wrangler d1 migrations apply --remote failed (exit ${migrate.code}):\n${tail}`);
|
|
415
|
+
}
|
|
416
|
+
p.log.success("D1 migrations: applied to remote.");
|
|
417
|
+
const remoteAdminSeed = await run("pnpm", [
|
|
418
|
+
"exec",
|
|
419
|
+
"wrangler",
|
|
420
|
+
"d1",
|
|
421
|
+
"execute",
|
|
422
|
+
d1Name,
|
|
423
|
+
"--remote",
|
|
424
|
+
"--file",
|
|
425
|
+
"migrations/0002_admin_seed.sql",
|
|
426
|
+
], { cwd: projectDir });
|
|
427
|
+
if (remoteAdminSeed.code !== 0) {
|
|
428
|
+
const tail = (remoteAdminSeed.stderr || remoteAdminSeed.stdout)
|
|
429
|
+
.trim()
|
|
430
|
+
.split("\n")
|
|
431
|
+
.slice(-6)
|
|
432
|
+
.join("\n");
|
|
433
|
+
throw new Error(`wrangler d1 execute admin seed --remote failed (exit ${remoteAdminSeed.code}):\n${tail}`);
|
|
434
|
+
}
|
|
435
|
+
p.log.success("D1 admin seed: refreshed on remote.");
|
|
436
|
+
const deploy = await run("pnpm", ["exec", "vinext", "deploy"], {
|
|
437
|
+
cwd: projectDir,
|
|
438
|
+
});
|
|
439
|
+
if (deploy.code !== 0) {
|
|
440
|
+
const tail = (deploy.stderr || deploy.stdout).trim().split("\n").slice(-8).join("\n");
|
|
441
|
+
throw new Error(`vinext deploy failed (exit ${deploy.code}):\n${tail}`);
|
|
442
|
+
}
|
|
443
|
+
const firstDeployUrl = parseWorkerUrl(deploy.stdout + "\n" + deploy.stderr);
|
|
444
|
+
const secretsChanged = await setProvisionedWorkerSecrets({
|
|
445
|
+
projectDir,
|
|
446
|
+
wireInputs,
|
|
447
|
+
requireNotionSecrets: result.notion.ok,
|
|
448
|
+
});
|
|
449
|
+
let finalUrl = firstDeployUrl;
|
|
450
|
+
if (firstDeployUrl) {
|
|
451
|
+
await patchSiteUrl(projectDir, firstDeployUrl);
|
|
452
|
+
p.log.success(`SITE_URL: ${firstDeployUrl}`);
|
|
453
|
+
}
|
|
454
|
+
if (firstDeployUrl || secretsChanged) {
|
|
455
|
+
const redeploy = await run("pnpm", ["exec", "vinext", "deploy"], {
|
|
456
|
+
cwd: projectDir,
|
|
457
|
+
});
|
|
458
|
+
if (redeploy.code !== 0) {
|
|
459
|
+
const tail = (redeploy.stderr || redeploy.stdout).trim().split("\n").slice(-8).join("\n");
|
|
460
|
+
throw new Error(`vinext deploy failed after secrets/SITE_URL update (exit ${redeploy.code}):\n${tail}`);
|
|
461
|
+
}
|
|
462
|
+
finalUrl = parseWorkerUrl(redeploy.stdout + "\n" + redeploy.stderr) ?? finalUrl;
|
|
463
|
+
}
|
|
464
|
+
result.deploy = {
|
|
465
|
+
ok: true,
|
|
466
|
+
url: finalUrl,
|
|
467
|
+
workerName: slug,
|
|
468
|
+
};
|
|
469
|
+
p.log.success(`Worker deployed: ${finalUrl ?? "(url not detected in output)"}`);
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
473
|
+
result.deploy = {
|
|
474
|
+
ok: false,
|
|
475
|
+
workerName: slug,
|
|
476
|
+
message,
|
|
477
|
+
skipped: false,
|
|
478
|
+
};
|
|
479
|
+
p.log.warn(`Deploy skipped — ${message.split("\n")[0]}\n (Run \`pnpm exec vinext deploy\` inside ${projectDir} to retry.)`);
|
|
480
|
+
}
|
|
481
|
+
return finalize(result, projectDir, slug);
|
|
482
|
+
}
|
|
483
|
+
async function requireWranglerAuthWithOptionalLogin(interactive) {
|
|
484
|
+
try {
|
|
485
|
+
return await requireWranglerAuth();
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
489
|
+
if (!interactive || !/wrangler.*not logged in|wrangler login/i.test(message)) {
|
|
490
|
+
throw err;
|
|
491
|
+
}
|
|
492
|
+
const login = await p.confirm({
|
|
493
|
+
message: "Cloudflare is not logged in. Run `wrangler login` now and continue provisioning?",
|
|
494
|
+
initialValue: true,
|
|
495
|
+
});
|
|
496
|
+
if (p.isCancel(login) || !login)
|
|
497
|
+
throw err;
|
|
498
|
+
const loginResult = await runInteractive("wrangler", ["login"]);
|
|
499
|
+
if (loginResult.code !== 0) {
|
|
500
|
+
throw new Error(`wrangler login failed (exit ${loginResult.code ?? "unknown"})`);
|
|
501
|
+
}
|
|
502
|
+
return requireWranglerAuth();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function promptNtnLogin(interactive) {
|
|
506
|
+
if (!interactive)
|
|
507
|
+
return null;
|
|
508
|
+
const login = await p.confirm({
|
|
509
|
+
message: "Notion is not logged in. Run `ntn login` now so the scaffolder can create and seed the content database?",
|
|
510
|
+
initialValue: true,
|
|
511
|
+
});
|
|
512
|
+
if (p.isCancel(login) || !login)
|
|
513
|
+
return null;
|
|
514
|
+
const start = await run("ntn", ["login", "--no-browser"]);
|
|
515
|
+
if (start.code !== 0) {
|
|
516
|
+
p.log.warn(`ntn login --no-browser failed (exit ${start.code ?? "unknown"}).`);
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const output = start.stdout.trim();
|
|
520
|
+
if (output) {
|
|
521
|
+
console.log("");
|
|
522
|
+
console.log(output);
|
|
523
|
+
console.log("");
|
|
524
|
+
}
|
|
525
|
+
const url = output.match(/https?:\/\/\S+/)?.[0];
|
|
526
|
+
if (url) {
|
|
527
|
+
p.log.info(`Opening Notion login URL in your browser:\n ${url}`);
|
|
528
|
+
// Best-effort macOS/browser launcher. The URL is printed above so
|
|
529
|
+
// users still have a manual path when `open` is unavailable.
|
|
530
|
+
await run("open", [url]).catch(() => null);
|
|
531
|
+
}
|
|
532
|
+
const poll = await runInteractive("ntn", ["login", "poll"]);
|
|
533
|
+
if (poll.code !== 0) {
|
|
534
|
+
p.log.warn(`ntn login poll failed (exit ${poll.code ?? "unknown"}).`);
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const token = await readNtnToken();
|
|
538
|
+
if (token)
|
|
539
|
+
return token;
|
|
540
|
+
p.log.warn("ntn login finished, but the scaffolder could not read the saved token. You can paste a `secret_…` integration token instead.");
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
function parseWorkerUrl(text) {
|
|
544
|
+
// wrangler prints lines like:
|
|
545
|
+
// Published <name> (X.XX sec)
|
|
546
|
+
// https://<name>.<subdomain>.workers.dev
|
|
547
|
+
const clean = text.replace(/\u001b\[[0-9;]*m/g, "");
|
|
548
|
+
return clean.match(/https:\/\/[a-zA-Z0-9._-]+\.workers\.dev/)?.[0];
|
|
549
|
+
}
|
|
550
|
+
async function provisionNotionContentAndPages({ answers, apiToken, parentPageId, seedCount, }) {
|
|
551
|
+
const content = await ensureNotionDatabase({
|
|
552
|
+
apiToken,
|
|
553
|
+
parentPageId,
|
|
554
|
+
title: `${answers.projectName} ${answers.contentSource.title}`,
|
|
555
|
+
stableKey: `content:${answers.contentSource.id}`,
|
|
556
|
+
locale: answers.defaultLocale,
|
|
557
|
+
fields: answers.contentSource.fields,
|
|
558
|
+
seedCount,
|
|
559
|
+
});
|
|
560
|
+
// Pages data source is optional — skip provisioning entirely
|
|
561
|
+
// when `--no-pages` is set. The fallback `app/page.tsx` doesn't
|
|
562
|
+
// read from Notion, so no data source is needed.
|
|
563
|
+
const pages = answers.enablePages
|
|
564
|
+
? await ensurePagesDatabase({
|
|
565
|
+
apiToken,
|
|
566
|
+
parentPageId,
|
|
567
|
+
projectName: answers.projectName,
|
|
568
|
+
contentSourceId: answers.contentSource.id,
|
|
569
|
+
contentSourceTitle: answers.contentSource.title,
|
|
570
|
+
contentSourceListPath: `/${answers.contentSource.id}`,
|
|
571
|
+
locale: answers.defaultLocale,
|
|
572
|
+
})
|
|
573
|
+
: {
|
|
574
|
+
ok: true,
|
|
575
|
+
skipped: true,
|
|
576
|
+
dataSourceId: "",
|
|
577
|
+
seeded: 0,
|
|
578
|
+
};
|
|
579
|
+
// Blocks data source is optional — skip provisioning entirely
|
|
580
|
+
// when `--no-blocks` is set. The fallback `components/page-blocks.tsx`
|
|
581
|
+
// doesn't read from Notion, so no data source is needed.
|
|
582
|
+
const blocks = answers.enableBlocks
|
|
583
|
+
? await ensureBlocksDatabase({
|
|
584
|
+
apiToken,
|
|
585
|
+
parentPageId,
|
|
586
|
+
projectName: answers.projectName,
|
|
587
|
+
contentSourceId: answers.contentSource.id,
|
|
588
|
+
contentSourceTitle: answers.contentSource.title,
|
|
589
|
+
contentSourceListPath: `/${answers.contentSource.id}`,
|
|
590
|
+
locale: answers.defaultLocale,
|
|
591
|
+
})
|
|
592
|
+
: {
|
|
593
|
+
ok: true,
|
|
594
|
+
skipped: true,
|
|
595
|
+
dataSourceId: "",
|
|
596
|
+
seeded: 0,
|
|
597
|
+
};
|
|
598
|
+
return { content, pages, blocks };
|
|
599
|
+
}
|
|
600
|
+
async function setProvisionedWorkerSecrets({ projectDir, wireInputs, requireNotionSecrets, }) {
|
|
601
|
+
if (!wireInputs)
|
|
602
|
+
return false;
|
|
603
|
+
let changed = false;
|
|
604
|
+
const putSecret = async (name, value, required) => {
|
|
605
|
+
if (!value) {
|
|
606
|
+
if (required) {
|
|
607
|
+
throw new Error(`failed to set ${name}; production content will be empty until this secret is set:\nmissing local value`);
|
|
608
|
+
}
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
await setWorkerSecret(name, value, projectDir, [value]);
|
|
613
|
+
p.log.success(`Worker secret: ${name} set.`);
|
|
614
|
+
changed = true;
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
618
|
+
if (required) {
|
|
619
|
+
throw new Error(`failed to set ${name}; production content will be empty until this secret is set:\n${message}`);
|
|
620
|
+
}
|
|
621
|
+
p.log.info(`Worker secret: ${name} skipped (${message.split("\n")[0]}).`);
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
await putSecret("TURNSTILE_SECRET_KEY", wireInputs.turnstileSecret, false);
|
|
625
|
+
await putSecret("NOTION_TOKEN", wireInputs.notionToken, requireNotionSecrets);
|
|
626
|
+
await putSecret("NOTION_DATA_SOURCE_ID", wireInputs.notionDataSourceId, requireNotionSecrets);
|
|
627
|
+
await putSecret("NOTION_PAGES_DATA_SOURCE_ID", wireInputs.notionPagesDataSourceId, requireNotionSecrets);
|
|
628
|
+
return changed;
|
|
629
|
+
}
|
|
630
|
+
export const _internal = {
|
|
631
|
+
setProvisionedWorkerSecrets,
|
|
632
|
+
};
|
|
633
|
+
function finalize(result, _projectDir, slug) {
|
|
634
|
+
// ---- 8. Status card ----
|
|
635
|
+
p.outro("Provisioning summary");
|
|
636
|
+
console.log("");
|
|
637
|
+
const row = (label, status, detail) => {
|
|
638
|
+
const icon = status === "ok" ? "✅" : status === "warn" ? "⚠️ " : "❌";
|
|
639
|
+
console.log(` ${icon} ${label.padEnd(20)} ${detail}`);
|
|
640
|
+
};
|
|
641
|
+
row("D1", result.d1.ok ? "ok" : "fail", result.d1.ok ? `${slug}-db (${result.d1.id?.slice(0, 8)}…)` : (result.d1.message ?? "failed"));
|
|
642
|
+
row("KV", result.kv.ok ? "ok" : "fail", result.kv.ok ? `CONTENT_CACHE (${result.kv.id?.slice(0, 8)}…)` : (result.kv.message ?? "failed"));
|
|
643
|
+
row("KV (cache)", result.vinextKv.ok ? "ok" : "fail", result.vinextKv.ok
|
|
644
|
+
? `VINEXT_KV_CACHE (${result.vinextKv.id?.slice(0, 8)}…)`
|
|
645
|
+
: (result.vinextKv.message ?? "failed"));
|
|
646
|
+
row("R2", result.r2.ok ? "ok" : "fail", result.r2.ok ? `${slug}-assets` : (result.r2.message ?? "failed"));
|
|
647
|
+
row("Migrations", result.migrationsApplied ? "ok" : "warn", result.migrationsApplied ? "applied locally" : "skipped or failed (run `pnpm run migrate:local` after install)");
|
|
648
|
+
row("Turnstile", result.turnstile.ok ? "ok" : result.turnstile.skipped ? "warn" : "fail", result.turnstile.ok
|
|
649
|
+
? `${result.turnstile.sitekey?.slice(0, 12)}…`
|
|
650
|
+
: result.turnstile.skipped
|
|
651
|
+
? "skipped (configure manually — see README)"
|
|
652
|
+
: (result.turnstile.message ?? "failed"));
|
|
653
|
+
row("Notion", result.notion.ok ? "ok" : result.notion.skipped ? "warn" : "fail", result.notion.ok
|
|
654
|
+
? `content ${result.notion.dataSourceId?.slice(0, 8)}… (${result.notion.seeded ?? 0} posts), pages ${result.notion.pagesDataSourceId?.slice(0, 8)}… (${result.notion.pagesSeeded ?? 0} pages), blocks ${result.notion.blocksDataSourceId?.slice(0, 8)}… (${result.notion.blocksSeeded ?? 0} blocks)${result.notion.message ? " (" + result.notion.message + ")" : ""}`
|
|
655
|
+
: result.notion.skipped
|
|
656
|
+
? "skipped (set NOTION_API_TOKEN or run `ntn login` to auto-create)"
|
|
657
|
+
: (result.notion.message ?? "failed"));
|
|
658
|
+
row("Site Settings", result.siteSettings.ok
|
|
659
|
+
? "ok"
|
|
660
|
+
: result.siteSettings.skipped
|
|
661
|
+
? "warn"
|
|
662
|
+
: "fail", result.siteSettings.ok
|
|
663
|
+
? `${result.siteSettings.reused ? "♻️" : "🆕"} data source ${result.siteSettings.dataSourceId?.slice(0, 8)}…${result.siteSettings.reused ? " (reused existing)" : " (created new)"}, seeded ${result.siteSettings.seeded ?? 0} page (editable in Notion; see README "Site settings")`
|
|
664
|
+
: result.siteSettings.skipped
|
|
665
|
+
? result.notion.ok
|
|
666
|
+
? "skipped (--no-site-settings)"
|
|
667
|
+
: "skipped (requires Notion to be wired up)"
|
|
668
|
+
: (result.siteSettings.message ?? "failed"));
|
|
669
|
+
row("Resend", result.resend.enabled ? "ok" : "warn", result.resend.enabled ? "enabled" : "skipped (configure manually — see README)");
|
|
670
|
+
row("Google", result.google.enabled ? "ok" : "warn", result.google.enabled ? "enabled" : "skipped (configure manually — see README)");
|
|
671
|
+
row("Admin", result.admin.ok ? "ok" : "fail", result.admin.ok
|
|
672
|
+
? `${result.admin.email} (${result.admin.message ?? "ok"})`
|
|
673
|
+
: (result.admin.message ?? "failed"));
|
|
674
|
+
row("Worker", result.deploy.ok ? "ok" : "fail", result.deploy.ok
|
|
675
|
+
? result.deploy.url ?? `deployed (${result.deploy.workerName})`
|
|
676
|
+
: result.deploy.skipped
|
|
677
|
+
? "skipped (run `pnpm exec vinext deploy` inside the project to deploy)"
|
|
678
|
+
: (result.deploy.message ?? "failed"));
|
|
679
|
+
return result;
|
|
680
|
+
}
|
|
681
|
+
//# sourceMappingURL=index.js.map
|