@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.
Files changed (239) hide show
  1. package/README.md +139 -0
  2. package/dist/answers.js +332 -0
  3. package/dist/answers.js.map +1 -0
  4. package/dist/cli-notionx.js +388 -0
  5. package/dist/cli-notionx.js.map +1 -0
  6. package/dist/cli-notionx.test.js +277 -0
  7. package/dist/cli-notionx.test.js.map +1 -0
  8. package/dist/diff.js +40 -0
  9. package/dist/diff.js.map +1 -0
  10. package/dist/diff.test.js +90 -0
  11. package/dist/diff.test.js.map +1 -0
  12. package/dist/index.js +99 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/locale-add/apply.js +39 -0
  15. package/dist/locale-add/apply.js.map +1 -0
  16. package/dist/locale-add/format.js +38 -0
  17. package/dist/locale-add/format.js.map +1 -0
  18. package/dist/locale-add/list.js +44 -0
  19. package/dist/locale-add/list.js.map +1 -0
  20. package/dist/locale-add/list.test.js +45 -0
  21. package/dist/locale-add/list.test.js.map +1 -0
  22. package/dist/locale-add/plan.js +128 -0
  23. package/dist/locale-add/plan.js.map +1 -0
  24. package/dist/locale-add/validate.js +46 -0
  25. package/dist/locale-add/validate.js.map +1 -0
  26. package/dist/metadata.js +41 -0
  27. package/dist/metadata.js.map +1 -0
  28. package/dist/notion-translation-sources/apply.js +61 -0
  29. package/dist/notion-translation-sources/apply.js.map +1 -0
  30. package/dist/notion-translation-sources/index.js +3 -0
  31. package/dist/notion-translation-sources/index.js.map +1 -0
  32. package/dist/notion-translation-sources/plan.js +33 -0
  33. package/dist/notion-translation-sources/plan.js.map +1 -0
  34. package/dist/notionx-source.js +142 -0
  35. package/dist/notionx-source.js.map +1 -0
  36. package/dist/notionx-source.test.js +144 -0
  37. package/dist/notionx-source.test.js.map +1 -0
  38. package/dist/password.js +18 -0
  39. package/dist/password.js.map +1 -0
  40. package/dist/presets.js +83 -0
  41. package/dist/presets.js.map +1 -0
  42. package/dist/presets.test.js +50 -0
  43. package/dist/presets.test.js.map +1 -0
  44. package/dist/prompt.js +218 -0
  45. package/dist/prompt.js.map +1 -0
  46. package/dist/provision/cloudflare.js +236 -0
  47. package/dist/provision/cloudflare.js.map +1 -0
  48. package/dist/provision/dependencies.js +219 -0
  49. package/dist/provision/dependencies.js.map +1 -0
  50. package/dist/provision/index.js +681 -0
  51. package/dist/provision/index.js.map +1 -0
  52. package/dist/provision/index.test.js +54 -0
  53. package/dist/provision/index.test.js.map +1 -0
  54. package/dist/provision/inspect.js +109 -0
  55. package/dist/provision/inspect.js.map +1 -0
  56. package/dist/provision/inspect.test.js +75 -0
  57. package/dist/provision/inspect.test.js.map +1 -0
  58. package/dist/provision/notion.js +1981 -0
  59. package/dist/provision/notion.js.map +1 -0
  60. package/dist/provision/notion.test.js +542 -0
  61. package/dist/provision/notion.test.js.map +1 -0
  62. package/dist/provision/ntn-credentials.js +198 -0
  63. package/dist/provision/ntn-credentials.js.map +1 -0
  64. package/dist/provision/options.js +15 -0
  65. package/dist/provision/options.js.map +1 -0
  66. package/dist/provision/password-hash.js +78 -0
  67. package/dist/provision/password-hash.js.map +1 -0
  68. package/dist/provision/prompts.js +115 -0
  69. package/dist/provision/prompts.js.map +1 -0
  70. package/dist/provision/repair.js +48 -0
  71. package/dist/provision/repair.js.map +1 -0
  72. package/dist/provision/repair.test.js +141 -0
  73. package/dist/provision/repair.test.js.map +1 -0
  74. package/dist/provision/shell.js +84 -0
  75. package/dist/provision/shell.js.map +1 -0
  76. package/dist/provision/wire.js +78 -0
  77. package/dist/provision/wire.js.map +1 -0
  78. package/dist/registry/doctor.js +181 -0
  79. package/dist/registry/doctor.js.map +1 -0
  80. package/dist/registry/doctor.test.js +180 -0
  81. package/dist/registry/doctor.test.js.map +1 -0
  82. package/dist/registry/install.js +217 -0
  83. package/dist/registry/install.js.map +1 -0
  84. package/dist/registry/install.test.js +168 -0
  85. package/dist/registry/install.test.js.map +1 -0
  86. package/dist/registry/load-registry.js +24 -0
  87. package/dist/registry/load-registry.js.map +1 -0
  88. package/dist/registry/load-registry.test.js +59 -0
  89. package/dist/registry/load-registry.test.js.map +1 -0
  90. package/dist/registry/migration-planner.js +204 -0
  91. package/dist/registry/migration-planner.js.map +1 -0
  92. package/dist/registry/migration-planner.test.js +340 -0
  93. package/dist/registry/migration-planner.test.js.map +1 -0
  94. package/dist/registry/migrations-store.js +125 -0
  95. package/dist/registry/migrations-store.js.map +1 -0
  96. package/dist/registry/migrations-store.test.js +163 -0
  97. package/dist/registry/migrations-store.test.js.map +1 -0
  98. package/dist/registry/migrations-types.js +25 -0
  99. package/dist/registry/migrations-types.js.map +1 -0
  100. package/dist/registry/project-meta.js +84 -0
  101. package/dist/registry/project-meta.js.map +1 -0
  102. package/dist/registry/registry-items.js +354 -0
  103. package/dist/registry/registry-items.js.map +1 -0
  104. package/dist/registry/registry-items.test.js +99 -0
  105. package/dist/registry/registry-items.test.js.map +1 -0
  106. package/dist/registry/registry-store.js +232 -0
  107. package/dist/registry/registry-store.js.map +1 -0
  108. package/dist/registry/registry-store.test.js +136 -0
  109. package/dist/registry/registry-store.test.js.map +1 -0
  110. package/dist/registry/registry-types.js +18 -0
  111. package/dist/registry/registry-types.js.map +1 -0
  112. package/dist/registry/registry-types.test.js +146 -0
  113. package/dist/registry/registry-types.test.js.map +1 -0
  114. package/dist/registry/render-content-source-files.js +158 -0
  115. package/dist/registry/render-content-source-files.js.map +1 -0
  116. package/dist/registry/render-multi-source.js +296 -0
  117. package/dist/registry/render-multi-source.js.map +1 -0
  118. package/dist/registry/render-multi-source.test.js +110 -0
  119. package/dist/registry/render-multi-source.test.js.map +1 -0
  120. package/dist/registry/text-utils.js +42 -0
  121. package/dist/registry/text-utils.js.map +1 -0
  122. package/dist/registry/uninstall.js +250 -0
  123. package/dist/registry/uninstall.js.map +1 -0
  124. package/dist/registry/uninstall.test.js +264 -0
  125. package/dist/registry/uninstall.test.js.map +1 -0
  126. package/dist/registry/update.js +280 -0
  127. package/dist/registry/update.js.map +1 -0
  128. package/dist/registry/update.test.js +229 -0
  129. package/dist/registry/update.test.js.map +1 -0
  130. package/dist/render.js +549 -0
  131. package/dist/render.js.map +1 -0
  132. package/dist/render.test.js +414 -0
  133. package/dist/render.test.js.map +1 -0
  134. package/dist/templates/.dev.vars.example.tmpl +32 -0
  135. package/dist/templates/.gitignore.tmpl +58 -0
  136. package/dist/templates/README.md.tmpl +417 -0
  137. package/dist/templates/app/[slug]/page.tsx.tmpl +55 -0
  138. package/dist/templates/app/admin/account/page.tsx.tmpl +18 -0
  139. package/dist/templates/app/admin/content-models/page.tsx.tmpl +6 -0
  140. package/dist/templates/app/admin/layout.tsx.tmpl +90 -0
  141. package/dist/templates/app/admin/loading.tsx.tmpl +6 -0
  142. package/dist/templates/app/admin/page.tsx.tmpl +17 -0
  143. package/dist/templates/app/api/auth/google/callback/route.ts.tmpl +3 -0
  144. package/dist/templates/app/api/auth/google/route.ts.tmpl +3 -0
  145. package/dist/templates/app/api/auth/verify-email/route.ts.tmpl +3 -0
  146. package/dist/templates/app/api/auth/viewer/route.ts.tmpl +3 -0
  147. package/dist/templates/app/api/health/route.ts.tmpl +3 -0
  148. package/dist/templates/app/api/{{contentSourceId}}/[slug]/route.ts.tmpl +27 -0
  149. package/dist/templates/app/api/{{contentSourceId}}/route.ts.tmpl +18 -0
  150. package/dist/templates/app/globals.css.tmpl +109 -0
  151. package/dist/templates/app/layout.tsx.tmpl +56 -0
  152. package/dist/templates/app/login/page.tsx.tmpl +154 -0
  153. package/dist/templates/app/page.fallback.tsx.tmpl +31 -0
  154. package/dist/templates/app/page.tsx.tmpl +42 -0
  155. package/dist/templates/app/register/page.tsx.tmpl +138 -0
  156. package/dist/templates/app/{{contentSourceListPath}}/[slug]/page.tsx.tmpl +113 -0
  157. package/dist/templates/app/{{contentSourceListPath}}/page.tsx.tmpl +74 -0
  158. package/dist/templates/components/content/post-card.tsx.tmpl +80 -0
  159. package/dist/templates/components/notion-blocks.tsx.tmpl +668 -0
  160. package/dist/templates/components/page-blocks/feature-grid-block.tsx.tmpl +68 -0
  161. package/dist/templates/components/page-blocks/hero-block.tsx.tmpl +73 -0
  162. package/dist/templates/components/page-blocks/latest-posts-block.tsx.tmpl +59 -0
  163. package/dist/templates/components/page-blocks/story-block.tsx.tmpl +70 -0
  164. package/dist/templates/components/page-blocks.fallback.tsx.tmpl +17 -0
  165. package/dist/templates/components/page-blocks.tsx.tmpl +32 -0
  166. package/dist/templates/components/search/search-dialog.tsx.tmpl +171 -0
  167. package/dist/templates/components/site/locale-switcher.tsx.tmpl +65 -0
  168. package/dist/templates/components/site/site-footer.tsx.tmpl +106 -0
  169. package/dist/templates/components/site/site-header.tsx.tmpl +80 -0
  170. package/dist/templates/components/site/site-shell.tsx.tmpl +20 -0
  171. package/dist/templates/components/site/theme-bootstrap.tsx.tmpl +51 -0
  172. package/dist/templates/components/theme-provider.tsx.tmpl +14 -0
  173. package/dist/templates/components/theme-toggle.tsx.tmpl +38 -0
  174. package/dist/templates/components/ui/accordion.tsx.tmpl +56 -0
  175. package/dist/templates/components/ui/alert.tsx.tmpl +59 -0
  176. package/dist/templates/components/ui/aspect-ratio.tsx.tmpl +8 -0
  177. package/dist/templates/components/ui/avatar.tsx.tmpl +44 -0
  178. package/dist/templates/components/ui/badge.tsx.tmpl +33 -0
  179. package/dist/templates/components/ui/button.tsx.tmpl +56 -0
  180. package/dist/templates/components/ui/card.tsx.tmpl +61 -0
  181. package/dist/templates/components/ui/checkbox.tsx.tmpl +28 -0
  182. package/dist/templates/components/ui/dialog.tsx.tmpl +104 -0
  183. package/dist/templates/components/ui/dropdown-menu.tsx.tmpl +183 -0
  184. package/dist/templates/components/ui/input.tsx.tmpl +21 -0
  185. package/dist/templates/components/ui/label.tsx.tmpl +25 -0
  186. package/dist/templates/components/ui/popover.tsx.tmpl +30 -0
  187. package/dist/templates/components/ui/radio-group.tsx.tmpl +44 -0
  188. package/dist/templates/components/ui/select.tsx.tmpl +150 -0
  189. package/dist/templates/components/ui/separator.tsx.tmpl +30 -0
  190. package/dist/templates/components/ui/sheet.tsx.tmpl +125 -0
  191. package/dist/templates/components/ui/skeleton.tsx.tmpl +15 -0
  192. package/dist/templates/components/ui/sonner.tsx.tmpl +30 -0
  193. package/dist/templates/components/ui/switch.tsx.tmpl +29 -0
  194. package/dist/templates/components/ui/table.tsx.tmpl +107 -0
  195. package/dist/templates/components/ui/tabs.tsx.tmpl +55 -0
  196. package/dist/templates/components/ui/textarea.tsx.tmpl +24 -0
  197. package/dist/templates/components/ui/tooltip.tsx.tmpl +30 -0
  198. package/dist/templates/components.json.tmpl +21 -0
  199. package/dist/templates/env.d.ts.tmpl +32 -0
  200. package/dist/templates/lib/admin/actions.ts.tmpl +43 -0
  201. package/dist/templates/lib/admin/context.tsx.tmpl +209 -0
  202. package/dist/templates/lib/admin/nav.ts.tmpl +23 -0
  203. package/dist/templates/lib/auth.config.fallback.ts.tmpl +10 -0
  204. package/dist/templates/lib/auth.config.ts.tmpl +45 -0
  205. package/dist/templates/lib/blocks/translations.ts.tmpl +44 -0
  206. package/dist/templates/lib/blog/translations.ts.tmpl +52 -0
  207. package/dist/templates/lib/content/models.ts.tmpl +53 -0
  208. package/dist/templates/lib/i18n/config.ts.tmpl +18 -0
  209. package/dist/templates/lib/i18n/index.ts.tmpl +1 -0
  210. package/dist/templates/lib/locale-contract/built-in.ts.tmpl +19 -0
  211. package/dist/templates/lib/locale-contract/index.ts.tmpl +3 -0
  212. package/dist/templates/lib/locale-contract/paths.ts.tmpl +29 -0
  213. package/dist/templates/lib/pages/model.ts.tmpl +16 -0
  214. package/dist/templates/lib/pages/source.ts.tmpl +566 -0
  215. package/dist/templates/lib/pages/translations.ts.tmpl +34 -0
  216. package/dist/templates/lib/search/config.fallback.ts.tmpl +11 -0
  217. package/dist/templates/lib/search/config.ts.tmpl +25 -0
  218. package/dist/templates/lib/site/config.ts.tmpl +120 -0
  219. package/dist/templates/lib/site/request-env.ts.tmpl +71 -0
  220. package/dist/templates/lib/site/settings.fallback.ts.tmpl +21 -0
  221. package/dist/templates/lib/site/settings.ts.tmpl +320 -0
  222. package/dist/templates/lib/site/translations.ts.tmpl +30 -0
  223. package/dist/templates/lib/utils.ts.tmpl +9 -0
  224. package/dist/templates/migrations/0001_init.sql.tmpl +57 -0
  225. package/dist/templates/migrations/0002_admin_seed.sql.tmpl +30 -0
  226. package/dist/templates/migrations/0003_search_index.sql.tmpl +29 -0
  227. package/dist/templates/next.config.ts.tmpl +18 -0
  228. package/dist/templates/package.json.tmpl +40 -0
  229. package/dist/templates/shims/cloudflare-workers-empty.mjs +4 -0
  230. package/dist/templates/shims/next-headers-empty.mjs +4 -0
  231. package/dist/templates/tests/smoke.test.ts.tmpl +83 -0
  232. package/dist/templates/tsconfig.json.tmpl +31 -0
  233. package/dist/templates/vite.config.ts.tmpl +53 -0
  234. package/dist/templates/vitest.config.ts.tmpl +13 -0
  235. package/dist/templates/worker/index.ts.tmpl +52 -0
  236. package/dist/templates/wrangler.jsonc.tmpl +44 -0
  237. package/dist/ui-presets.js +60 -0
  238. package/dist/ui-presets.js.map +1 -0
  239. 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