create-feedbax 0.1.0 → 0.2.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 (210) hide show
  1. package/LICENSE +201 -21
  2. package/README.md +11 -1
  3. package/dist/cli.js +1261 -26
  4. package/dist/index.js +1402 -160
  5. package/package.json +40 -14
  6. package/template/.feedbax-template.sha256 +1 -0
  7. package/template/.gitignore.template +6 -0
  8. package/template/.npmignore.template +5 -0
  9. package/template/.prettierignore.template +4 -0
  10. package/template/LICENSE +201 -0
  11. package/template/components.json +25 -0
  12. package/template/eslint.config.mjs +25 -0
  13. package/template/package.json +70 -14
  14. package/template/public/feedbax-mark.svg +4 -0
  15. package/template/public/robots.txt +3 -0
  16. package/template/src/authorized-draft-post.test.tsx +212 -0
  17. package/template/src/authorized-draft-post.tsx +204 -0
  18. package/template/src/authorized-draft-query.ts +74 -0
  19. package/template/src/browser-post-state.test.ts +120 -0
  20. package/template/src/browser-post-state.ts +134 -0
  21. package/template/src/browser-vote-state.test.ts +14 -0
  22. package/template/src/browser-vote-state.ts +27 -0
  23. package/template/src/capability-authorization-error.ts +3 -0
  24. package/template/src/changelog-page.test.tsx +139 -0
  25. package/template/src/changelog-page.tsx +219 -0
  26. package/template/src/changelog-query.ts +13 -0
  27. package/template/src/changelog-runtime.ts +27 -0
  28. package/template/src/cloudflare-turnstile.ts +31 -0
  29. package/template/src/cloudflare-workers-test.ts +3 -0
  30. package/template/src/cloudflare-workers.d.ts +18 -0
  31. package/template/src/comment-creation.test.tsx +270 -0
  32. package/template/src/comment-profile.ts +5 -0
  33. package/template/src/comment-thread-page.ts +18 -0
  34. package/template/src/components/command-palette-context.tsx +71 -0
  35. package/template/src/components/command-palette.tsx +246 -0
  36. package/template/src/components/create-post-trigger.tsx +44 -0
  37. package/template/src/components/device-profile-provider.tsx +309 -0
  38. package/template/src/components/header.tsx +95 -0
  39. package/template/src/components/loader.tsx +10 -0
  40. package/template/src/components/profile-menu.tsx +89 -0
  41. package/template/src/components/ui/components/alert-dialog.tsx +185 -0
  42. package/template/src/components/ui/components/attachment.tsx +207 -0
  43. package/template/src/components/ui/components/avatar.tsx +107 -0
  44. package/template/src/components/ui/components/badge.tsx +52 -0
  45. package/template/src/components/ui/components/bubble.tsx +128 -0
  46. package/template/src/components/ui/components/button-group.tsx +87 -0
  47. package/template/src/components/ui/components/button.tsx +56 -0
  48. package/template/src/components/ui/components/card.tsx +103 -0
  49. package/template/src/components/ui/components/checkbox.tsx +26 -0
  50. package/template/src/components/ui/components/command.tsx +194 -0
  51. package/template/src/components/ui/components/dialog.tsx +154 -0
  52. package/template/src/components/ui/components/drawer.tsx +229 -0
  53. package/template/src/components/ui/components/dropdown-menu.tsx +270 -0
  54. package/template/src/components/ui/components/empty.tsx +101 -0
  55. package/template/src/components/ui/components/field.tsx +236 -0
  56. package/template/src/components/ui/components/input-group.tsx +155 -0
  57. package/template/src/components/ui/components/input.tsx +20 -0
  58. package/template/src/components/ui/components/item.tsx +201 -0
  59. package/template/src/components/ui/components/kbd.tsx +26 -0
  60. package/template/src/components/ui/components/label.tsx +18 -0
  61. package/template/src/components/ui/components/marker.tsx +71 -0
  62. package/template/src/components/ui/components/message-scroller.tsx +128 -0
  63. package/template/src/components/ui/components/message.tsx +92 -0
  64. package/template/src/components/ui/components/native-select.tsx +65 -0
  65. package/template/src/components/ui/components/popover.tsx +88 -0
  66. package/template/src/components/ui/components/portal-primitives.test.tsx +85 -0
  67. package/template/src/components/ui/components/radio-group.tsx +36 -0
  68. package/template/src/components/ui/components/scroll-area.tsx +52 -0
  69. package/template/src/components/ui/components/secondary-form-actions.test.tsx +23 -0
  70. package/template/src/components/ui/components/select.tsx +199 -0
  71. package/template/src/components/ui/components/separator.tsx +23 -0
  72. package/template/src/components/ui/components/sheet.tsx +135 -0
  73. package/template/src/components/ui/components/skeleton.tsx +13 -0
  74. package/template/src/components/ui/components/sonner.tsx +43 -0
  75. package/template/src/components/ui/components/spinner.tsx +16 -0
  76. package/template/src/components/ui/components/tabs.tsx +80 -0
  77. package/template/src/components/ui/components/textarea.tsx +18 -0
  78. package/template/src/components/ui/components/tooltip.tsx +64 -0
  79. package/template/src/components/ui/hooks/.gitkeep +0 -0
  80. package/template/src/components/ui/lib/utils.ts +6 -0
  81. package/template/src/components/ui/styles/globals.css +151 -0
  82. package/template/src/create-post-overlay.tsx +82 -0
  83. package/template/src/draft-post-list.tsx +39 -0
  84. package/template/src/feedback-cache-invalidation.test.ts +185 -0
  85. package/template/src/feedback-cache-invalidation.ts +97 -0
  86. package/template/src/feedback-runtime.ts +38 -0
  87. package/template/src/feedbax/changelog/index.test.ts +156 -0
  88. package/template/src/feedbax/changelog/index.ts +206 -0
  89. package/template/src/feedbax/changelog/notion-changelog.test.ts +395 -0
  90. package/template/src/feedbax/changelog/notion-changelog.ts +400 -0
  91. package/template/src/feedbax/config/index.test.ts +111 -0
  92. package/template/src/feedbax/config/index.ts +271 -0
  93. package/template/src/feedbax/feedback/feedback-storage.ts +43 -0
  94. package/template/src/feedbax/feedback/index.test.ts +900 -0
  95. package/template/src/feedbax/feedback/index.ts +942 -0
  96. package/template/src/feedbax/feedback/notion-comments.test.ts +171 -0
  97. package/template/src/feedbax/feedback/notion-comments.ts +231 -0
  98. package/template/src/feedbax/feedback/notion-data-source.test.ts +364 -0
  99. package/template/src/feedbax/feedback/notion-data-source.ts +458 -0
  100. package/template/src/feedbax/feedback/notion-feedback.test.ts +998 -0
  101. package/template/src/feedbax/feedback/notion-feedback.ts +651 -0
  102. package/template/src/feedbax/feedback/notion-request.ts +74 -0
  103. package/template/src/feedbax.test-fixture.ts +64 -0
  104. package/template/src/feedbax.ts +69 -0
  105. package/template/src/index.css +2138 -0
  106. package/template/src/lazy-create-post-overlay.tsx +6 -0
  107. package/template/src/lazy-post-detail-overlay.tsx +10 -0
  108. package/template/src/masked-post-link.tsx +27 -0
  109. package/template/src/overlay-post-detail.test.tsx +130 -0
  110. package/template/src/overlay-post-detail.tsx +92 -0
  111. package/template/src/portal-comments.test.ts +375 -0
  112. package/template/src/portal-comments.ts +384 -0
  113. package/template/src/portal-features.test.tsx +150 -0
  114. package/template/src/portal-feedback-form.tsx +515 -0
  115. package/template/src/portal-feedback-runtime.test.ts +32 -0
  116. package/template/src/portal-feedback-runtime.ts +55 -0
  117. package/template/src/portal-feedback-server-function.ts +93 -0
  118. package/template/src/portal-feedback-submission.test.ts +307 -0
  119. package/template/src/portal-feedback-submission.ts +214 -0
  120. package/template/src/portal-head.ts +58 -0
  121. package/template/src/portal-server-entry.ts +64 -0
  122. package/template/src/portal-voting.test.ts +106 -0
  123. package/template/src/portal-voting.ts +149 -0
  124. package/template/src/post-badges.tsx +27 -0
  125. package/template/src/post-creation-flow.test.tsx +230 -0
  126. package/template/src/post-creation-flow.tsx +88 -0
  127. package/template/src/post-detail-overlay.test.tsx +114 -0
  128. package/template/src/post-detail-overlay.tsx +67 -0
  129. package/template/src/post-feed-controls.test.tsx +239 -0
  130. package/template/src/post-feed-controls.tsx +298 -0
  131. package/template/src/post-presentation.ts +37 -0
  132. package/template/src/post-queries.test.ts +158 -0
  133. package/template/src/post-queries.ts +26 -0
  134. package/template/src/post-results.tsx +165 -0
  135. package/template/src/public-cache-policy.test.ts +141 -0
  136. package/template/src/public-cache-policy.ts +119 -0
  137. package/template/src/public-cache-tags.ts +7 -0
  138. package/template/src/public-changelog-page.ts +15 -0
  139. package/template/src/public-changelog-server-function.ts +27 -0
  140. package/template/src/public-date.ts +11 -0
  141. package/template/src/public-failure-state.test.tsx +69 -0
  142. package/template/src/public-feedback-index.tsx +124 -0
  143. package/template/src/public-feedback-page.test.tsx +290 -0
  144. package/template/src/public-feedback-page.ts +75 -0
  145. package/template/src/public-feedback-server-function.ts +22 -0
  146. package/template/src/public-portal-error.tsx +27 -0
  147. package/template/src/public-post-detail.tsx +682 -0
  148. package/template/src/public-post-page.test.tsx +173 -0
  149. package/template/src/public-post-page.ts +15 -0
  150. package/template/src/public-post-route.test.tsx +214 -0
  151. package/template/src/public-post-route.tsx +171 -0
  152. package/template/src/public-post-server-function.ts +48 -0
  153. package/template/src/public-post-unavailable.tsx +10 -0
  154. package/template/src/public-roadmap-page.test.tsx +139 -0
  155. package/template/src/public-roadmap-page.ts +24 -0
  156. package/template/src/public-roadmap-route.test.tsx +218 -0
  157. package/template/src/public-roadmap-route.tsx +50 -0
  158. package/template/src/public-roadmap-server-function.ts +44 -0
  159. package/template/src/public-roadmap-view.tsx +378 -0
  160. package/template/src/public-shell.test.tsx +518 -0
  161. package/template/src/roadmap-query.ts +21 -0
  162. package/template/src/root-branding.test.ts +53 -0
  163. package/template/src/routeTree.gen.ts +131 -0
  164. package/template/src/routed-post-creation-form.tsx +28 -0
  165. package/template/src/router.tsx +25 -0
  166. package/template/src/routes/__root.tsx +116 -0
  167. package/template/src/routes/changelog.tsx +39 -0
  168. package/template/src/routes/index.tsx +61 -0
  169. package/template/src/routes/p.$slug.tsx +16 -0
  170. package/template/src/routes/roadmap.tsx +11 -0
  171. package/template/src/routes/submit.tsx +28 -0
  172. package/template/src/safe-public-failure.ts +45 -0
  173. package/template/src/server.ts +111 -0
  174. package/template/src/similar-posts.tsx +45 -0
  175. package/template/src/submit-route.test.tsx +134 -0
  176. package/template/src/trusted-feedback-handler.test.ts +425 -0
  177. package/template/src/trusted-feedback-handler.ts +181 -0
  178. package/template/src/trusted-feedback-runtime.ts +17 -0
  179. package/template/src/turnstile-configuration.test.ts +37 -0
  180. package/template/src/turnstile-configuration.ts +19 -0
  181. package/template/src/use-media-query.ts +17 -0
  182. package/template/src/vote-toggle.test.tsx +171 -0
  183. package/template/src/vote-toggle.tsx +211 -0
  184. package/template/tsconfig.json +45 -0
  185. package/template/vite.config.ts +20 -0
  186. package/template/vitest.config.ts +4 -0
  187. package/template/vitest.setup.ts +3 -0
  188. package/template/wrangler.jsonc +35 -0
  189. package/dist/cli.d.ts +0 -3
  190. package/dist/cli.d.ts.map +0 -1
  191. package/dist/cli.js.map +0 -1
  192. package/dist/index.d.ts +0 -21
  193. package/dist/index.d.ts.map +0 -1
  194. package/dist/index.js.map +0 -1
  195. package/template/.env.example +0 -4
  196. package/template/README.md +0 -7
  197. package/template/apps/portal/components.json +0 -20
  198. package/template/apps/portal/package.json +0 -31
  199. package/template/apps/portal/src/lib/utils.ts +0 -6
  200. package/template/apps/portal/src/routeTree.gen.ts +0 -59
  201. package/template/apps/portal/src/router.tsx +0 -10
  202. package/template/apps/portal/src/routes/__root.tsx +0 -31
  203. package/template/apps/portal/src/routes/index.tsx +0 -35
  204. package/template/apps/portal/src/styles.css +0 -91
  205. package/template/apps/portal/tsconfig.json +0 -15
  206. package/template/apps/portal/vite.config.ts +0 -13
  207. package/template/feedbax.config.ts +0 -13
  208. package/template/feedbax.jsonc +0 -11
  209. package/template/pnpm-workspace.yaml +0 -3
  210. package/template/turbo.json +0 -8
package/dist/index.js CHANGED
@@ -1,173 +1,1415 @@
1
- import { cp, mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
2
- import { spawn } from 'node:child_process';
3
- import { dirname, join, resolve } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- const valueFlags = new Set([
6
- '--connector',
7
- '--identity',
8
- '--storage',
9
- '--deploy',
10
- '--preset',
11
- '--package-manager',
12
- ]);
13
- const booleanFlags = new Set(['--yes', '--no-install', '--no-git']);
14
- const allowedDeployments = new Set(['cloudflare', 'vercel', 'node']);
15
- const allowedPackageManagers = new Set(['npm', 'pnpm']);
16
- const identityMode = (mode) => {
17
- if (mode === 'anonymous' || mode === 'email' || mode === 'handoff')
18
- return mode;
19
- throw new Error(`Identity mode "${mode}" is not supported in Feedbax 0.1.0.`);
1
+ // src/index.ts
2
+ import { createHash as createHash2, randomBytes } from "node:crypto";
3
+ import { readFile as readFile2, readdir as readdir2, writeFile as writeFile2 } from "node:fs/promises";
4
+
5
+ // ../config/src/index.ts
6
+ var rootKeys = [
7
+ "product",
8
+ "theme",
9
+ "notion",
10
+ "changelog",
11
+ "features",
12
+ "submissions",
13
+ "turnstile"
14
+ ];
15
+ var featureKeys = ["voting", "comments", "changelog"];
16
+ var feedbackPropertyKeys = [
17
+ "title",
18
+ "slug",
19
+ "description",
20
+ "type",
21
+ "status",
22
+ "published",
23
+ "voteCount",
24
+ "teamResponse",
25
+ "releaseNotesUrl",
26
+ "submitterName",
27
+ "submitterEmail",
28
+ "source",
29
+ "externalId",
30
+ "editTokenHash",
31
+ "createdAt",
32
+ "updatedAt"
33
+ ];
34
+ var changelogPropertyKeys = [
35
+ "title",
36
+ "slug",
37
+ "date",
38
+ "summary",
39
+ "body",
40
+ "labels",
41
+ "image",
42
+ "published",
43
+ "createdAt",
44
+ "updatedAt"
45
+ ];
46
+ function defineFeedbax(config) {
47
+ const value = object(config, "Feedbax configuration");
48
+ rejectUnknown(value, rootKeys, "Feedbax configuration");
49
+ const product = object(value.product, "product");
50
+ rejectUnknown(
51
+ product,
52
+ ["name", "description", "logo", "siteUrl", "supportUrl", "privacyUrl"],
53
+ "product"
54
+ );
55
+ const validatedProduct = {
56
+ name: text(product.name, "product.name"),
57
+ description: text(product.description, "product.description"),
58
+ logo: publicUrl(product.logo, "product.logo", true),
59
+ ...optionalPublicUrl(product, "siteUrl"),
60
+ ...optionalPublicUrl(product, "supportUrl"),
61
+ ...optionalPublicUrl(product, "privacyUrl")
62
+ };
63
+ const theme = object(value.theme, "theme");
64
+ rejectUnknown(theme, ["preset"], "theme");
65
+ const notion = storage(
66
+ value.notion,
67
+ feedbackPropertyKeys,
68
+ "notion"
69
+ );
70
+ const features = object(value.features, "features");
71
+ rejectUnknown(features, featureKeys, "features");
72
+ const validatedFeatures = Object.fromEntries(
73
+ featureKeys.map((key) => {
74
+ if (typeof features[key] !== "boolean")
75
+ throw new Error(`features.${key} must be true or false.`);
76
+ return [key, features[key]];
77
+ })
78
+ );
79
+ const changelog = value.changelog === void 0 ? void 0 : storage(
80
+ value.changelog,
81
+ changelogPropertyKeys,
82
+ "changelog"
83
+ );
84
+ if (validatedFeatures.changelog && !changelog)
85
+ throw new Error(
86
+ "Changelog is enabled but changelog storage is not configured. Add changelog identifiers and property mappings, or set features.changelog to false."
87
+ );
88
+ const submissions = object(value.submissions, "submissions");
89
+ rejectUnknown(
90
+ submissions,
91
+ ["moderation", "maxTitleLength", "maxDescriptionLength"],
92
+ "submissions"
93
+ );
94
+ if (submissions.moderation !== "required")
95
+ throw new Error('submissions.moderation must be "required".');
96
+ const turnstile = object(value.turnstile, "turnstile");
97
+ rejectUnknown(turnstile, ["enabled", "siteKey"], "turnstile");
98
+ if (typeof turnstile.enabled !== "boolean")
99
+ throw new Error("turnstile.enabled must be true or false.");
100
+ const siteKey = turnstile.siteKey === void 0 ? void 0 : text(turnstile.siteKey, "turnstile.siteKey");
101
+ if (turnstile.enabled && !siteKey)
102
+ throw new Error("Turnstile is enabled but turnstile.siteKey is missing.");
103
+ return {
104
+ product: validatedProduct,
105
+ theme: { preset: text(theme.preset, "theme.preset") },
106
+ notion,
107
+ ...changelog ? { changelog } : {},
108
+ features: validatedFeatures,
109
+ submissions: {
110
+ moderation: "required",
111
+ maxTitleLength: positiveInteger(
112
+ submissions.maxTitleLength,
113
+ "submissions.maxTitleLength"
114
+ ),
115
+ maxDescriptionLength: positiveInteger(
116
+ submissions.maxDescriptionLength,
117
+ "submissions.maxDescriptionLength"
118
+ )
119
+ },
120
+ turnstile: { enabled: turnstile.enabled, ...siteKey ? { siteKey } : {} }
121
+ };
122
+ }
123
+ function storage(value, keys, label) {
124
+ const input = object(value, label);
125
+ rejectUnknown(input, ["databaseId", "dataSourceId", "properties"], label);
126
+ const properties = object(input.properties, `${label}.properties`);
127
+ rejectUnknown(properties, keys, `${label}.properties`);
128
+ for (const key of keys) text(properties[key], `${label}.properties.${key}`);
129
+ return {
130
+ databaseId: text(input.databaseId, `${label}.databaseId`),
131
+ dataSourceId: text(input.dataSourceId, `${label}.dataSourceId`),
132
+ properties: { ...properties }
133
+ };
134
+ }
135
+ function object(value, label) {
136
+ if (!value || typeof value !== "object" || Array.isArray(value))
137
+ throw new Error(`${label} must be an object.`);
138
+ return value;
139
+ }
140
+ function text(value, label) {
141
+ if (typeof value !== "string" || !value.trim())
142
+ throw new Error(`${label} must be a non-empty string.`);
143
+ return value.trim();
144
+ }
145
+ function positiveInteger(value, label) {
146
+ if (!Number.isInteger(value) || Number(value) <= 0)
147
+ throw new Error(`${label} must be a positive integer.`);
148
+ return Number(value);
149
+ }
150
+ function rejectUnknown(value, allowed, label) {
151
+ const key = Object.keys(value).find(
152
+ (candidate) => !allowed.includes(candidate)
153
+ );
154
+ if (key) throw new Error(`Unknown ${label} key "${key}".`);
155
+ }
156
+ function optionalPublicUrl(value, key) {
157
+ return value[key] === void 0 ? {} : { [key]: publicUrl(value[key], `product.${key}`, false) };
158
+ }
159
+ function publicUrl(value, label, allowRelative) {
160
+ const url = text(value, label);
161
+ if (allowRelative && url.startsWith("/") && !url.startsWith("//") && !url.includes("\\") && !Array.from(url).some((character) => character.charCodeAt(0) <= 32))
162
+ return url;
163
+ try {
164
+ if (new URL(url).protocol === "https:") return url;
165
+ } catch {
166
+ }
167
+ throw new Error(
168
+ `${label} must be ${allowRelative ? "root-relative or " : ""}an HTTPS URL.`
169
+ );
170
+ }
171
+
172
+ // src/index.ts
173
+ import { basename, join as join2 } from "node:path";
174
+
175
+ // src/template.ts
176
+ import { createHash } from "node:crypto";
177
+ import { parse } from "jsonc-parser";
178
+ import {
179
+ cp,
180
+ mkdir,
181
+ readFile,
182
+ readdir,
183
+ rm,
184
+ stat,
185
+ writeFile
186
+ } from "node:fs/promises";
187
+ import { dirname, join, relative, resolve } from "node:path";
188
+ import { fileURLToPath } from "node:url";
189
+ var ignored = ["node_modules", "dist", ".wrangler", ".dev.vars", ".env"];
190
+ var packagedTemplateDirectory = resolve(
191
+ dirname(fileURLToPath(import.meta.url)),
192
+ "../template"
193
+ );
194
+ async function assembleFlatTemplate(coreRoot, destination) {
195
+ const target = resolve(destination);
196
+ await rm(target, { recursive: true, force: true });
197
+ await mkdir(join(target, "src/feedbax"), { recursive: true });
198
+ await copy(join(coreRoot, "apps/portal/src"), join(target, "src"));
199
+ await copy(join(coreRoot, "apps/portal/public"), join(target, "public"));
200
+ await copy(
201
+ join(coreRoot, "packages/feedback/src"),
202
+ join(target, "src/feedbax/feedback")
203
+ );
204
+ await copy(
205
+ join(coreRoot, "packages/changelog/src"),
206
+ join(target, "src/feedbax/changelog")
207
+ );
208
+ await copy(
209
+ join(coreRoot, "packages/config/src"),
210
+ join(target, "src/feedbax/config")
211
+ );
212
+ await copy(
213
+ join(coreRoot, "packages/ui/src"),
214
+ join(target, "src/components/ui")
215
+ );
216
+ for (const name of ["vite.config.ts"])
217
+ await copy(join(coreRoot, "apps/portal", name), join(target, name));
218
+ const components = parse(
219
+ await readFile(join(coreRoot, "apps/portal/components.json"), "utf8")
220
+ );
221
+ components.tailwind.css = "src/components/ui/styles/globals.css";
222
+ await writeFile(
223
+ join(target, "components.json"),
224
+ JSON.stringify(components, null, 2) + "\n"
225
+ );
226
+ const wrangler = parse(
227
+ await readFile(join(coreRoot, "apps/portal/wrangler.jsonc"), "utf8")
228
+ );
229
+ delete wrangler.routes;
230
+ delete wrangler.vars;
231
+ delete wrangler.account_id;
232
+ wrangler.name = "feedbax-installation";
233
+ await writeFile(
234
+ join(target, "wrangler.jsonc"),
235
+ JSON.stringify(wrangler, null, 2) + "\n"
236
+ );
237
+ await copy(join(coreRoot, "LICENSE"), join(target, "LICENSE"));
238
+ await copy(
239
+ join(coreRoot, "eslint.config.mjs"),
240
+ join(target, "eslint.config.mjs")
241
+ );
242
+ await writeFile(
243
+ join(target, "package.json"),
244
+ JSON.stringify(await flatManifest(coreRoot), null, 2) + "\n"
245
+ );
246
+ await writeFile(
247
+ join(target, "tsconfig.json"),
248
+ JSON.stringify(flatTsconfig, null, 2) + "\n"
249
+ );
250
+ await writeFile(
251
+ join(target, "vitest.config.ts"),
252
+ 'import { fileURLToPath } from "node:url";\nimport { defineConfig } from "vitest/config";\nconst src = fileURLToPath(new URL("./src/", import.meta.url));\nexport default defineConfig({ resolve: { alias: [{ find: "@feedbax/feedback", replacement: `${src}feedbax/feedback/index.ts` }, { find: "@feedbax/changelog", replacement: `${src}feedbax/changelog/index.ts` }, { find: "@feedbax/config", replacement: `${src}feedbax/config/index.ts` }, { find: "cloudflare:workers", replacement: `${src}cloudflare-workers-test.ts` }, { find: /^@feedbax\\/ui\\/(.*)$/, replacement: `${src}components/ui/$1` }] } });\n'
253
+ );
254
+ const feedbackKeys = [
255
+ "title",
256
+ "slug",
257
+ "description",
258
+ "type",
259
+ "status",
260
+ "published",
261
+ "voteCount",
262
+ "teamResponse",
263
+ "releaseNotesUrl",
264
+ "submitterName",
265
+ "submitterEmail",
266
+ "source",
267
+ "externalId",
268
+ "editTokenHash",
269
+ "createdAt",
270
+ "updatedAt"
271
+ ];
272
+ const changelogKeys = [
273
+ "title",
274
+ "slug",
275
+ "date",
276
+ "summary",
277
+ "body",
278
+ "labels",
279
+ "image",
280
+ "published",
281
+ "createdAt",
282
+ "updatedAt"
283
+ ];
284
+ const fixture = {
285
+ product: {
286
+ name: "Feedbax",
287
+ description: "Share feedback and follow product progress.",
288
+ logo: "/feedbax-mark.svg"
289
+ },
290
+ theme: { preset: "lyra-neutral" },
291
+ notion: {
292
+ databaseId: "test-database",
293
+ dataSourceId: "test-feedback",
294
+ properties: Object.fromEntries(
295
+ feedbackKeys.map((key) => [key, `test-${key}`])
296
+ )
297
+ },
298
+ changelog: {
299
+ databaseId: "test-database",
300
+ dataSourceId: "test-changelog",
301
+ properties: Object.fromEntries(
302
+ changelogKeys.map((key) => [key, `test-${key}`])
303
+ )
304
+ },
305
+ features: { voting: true, comments: true, changelog: true },
306
+ submissions: {
307
+ moderation: "required",
308
+ maxTitleLength: 120,
309
+ maxDescriptionLength: 5e3
310
+ },
311
+ turnstile: { enabled: false }
312
+ };
313
+ await writeFile(
314
+ join(target, "src/feedbax.test-fixture.ts"),
315
+ `import { defineFeedbax } from "@feedbax/config";
316
+
317
+ // prettier-ignore
318
+ export default defineFeedbax(${JSON.stringify(fixture, null, 2)});
319
+ `
320
+ );
321
+ await writeFile(
322
+ join(target, "vitest.setup.ts"),
323
+ 'import { vi } from "vitest";\n\nvi.mock("./src/feedbax", () => import("./src/feedbax.test-fixture"));\n'
324
+ );
325
+ const testConfigPath = join(target, "vitest.config.ts");
326
+ await writeFile(
327
+ testConfigPath,
328
+ (await readFile(testConfigPath, "utf8")).replace(
329
+ "defineConfig({ resolve:",
330
+ 'defineConfig({ test: { setupFiles: ["./vitest.setup.ts"] }, resolve:'
331
+ )
332
+ );
333
+ await writeFile(
334
+ join(target, ".gitignore.template"),
335
+ ".dev.vars\n.env*\ndist/\n.wrangler/\n*.log\nnode_modules/\n"
336
+ );
337
+ await writeFile(
338
+ join(target, ".npmignore.template"),
339
+ ".dev.vars\n.env*\ndist/\n.wrangler/\n*.log\n"
340
+ );
341
+ await writeFile(
342
+ join(target, ".prettierignore.template"),
343
+ "pnpm-lock.yaml\nsrc/routeTree.gen.ts\ntsconfig.json\nvitest.config.ts\n"
344
+ );
345
+ const checksum = await templateChecksum(target);
346
+ await writeFile(join(target, ".feedbax-template.sha256"), `${checksum}
347
+ `);
348
+ return checksum;
349
+ }
350
+ async function templateChecksum(root) {
351
+ const hash = createHash("sha256");
352
+ for (const file of await files(root)) {
353
+ const name = relative(root, file);
354
+ if (name === ".feedbax-template.sha256") continue;
355
+ hash.update(name).update("\0").update(await readFile(file)).update("\0");
356
+ }
357
+ return hash.digest("hex");
358
+ }
359
+ async function verifyPackagedTemplate(coreRoot, packaged = packagedTemplateDirectory) {
360
+ const temporary = `${packaged}.verify-${process.pid}`;
361
+ try {
362
+ const expected = await assembleFlatTemplate(coreRoot, temporary);
363
+ const actual = (await readFile(join(packaged, ".feedbax-template.sha256"), "utf8")).trim();
364
+ if (actual !== expected || await templateChecksum(packaged) !== actual)
365
+ throw new Error(
366
+ "Packaged create-feedbax template is out of sync. Run pnpm template:sync."
367
+ );
368
+ } finally {
369
+ await rm(temporary, { recursive: true, force: true });
370
+ }
371
+ }
372
+ async function copyPackagedTemplate(destination, packaged = packagedTemplateDirectory) {
373
+ const target = resolve(destination);
374
+ await mkdir(target, { recursive: true });
375
+ if ((await readdir(target)).length)
376
+ throw new Error(
377
+ `Refusing to generate into non-empty destination: ${target}`
378
+ );
379
+ await cp(packaged, target, { recursive: true });
380
+ await renameIgnoreTemplates(target);
381
+ }
382
+ async function renameIgnoreTemplates(target) {
383
+ const { rename } = await import("node:fs/promises");
384
+ for (const name of [".gitignore", ".npmignore", ".prettierignore"])
385
+ await rename(join(target, `${name}.template`), join(target, name));
386
+ }
387
+ async function flatManifest(coreRoot) {
388
+ const packageFiles = [
389
+ "apps/portal",
390
+ "packages/ui",
391
+ "packages/config",
392
+ "packages/feedback",
393
+ "packages/changelog"
394
+ ];
395
+ const manifests = await Promise.all(
396
+ packageFiles.map(
397
+ async (path) => JSON.parse(await readFile(join(coreRoot, path, "package.json"), "utf8"))
398
+ )
399
+ );
400
+ const workspace = await readFile(
401
+ join(coreRoot, "pnpm-workspace.yaml"),
402
+ "utf8"
403
+ );
404
+ const catalog = Object.fromEntries(
405
+ [...workspace.matchAll(/^ {2}("?[^"\s:]+"?): (.+)$/gm)].map((match) => [
406
+ match[1].replaceAll('"', ""),
407
+ match[2]
408
+ ])
409
+ );
410
+ const collect = (section) => Object.fromEntries(
411
+ manifests.flatMap((manifest) => Object.entries(manifest[section] ?? {})).filter(([name]) => !name.startsWith("@feedbax/")).map(([name, version]) => [
412
+ name,
413
+ version === "catalog:" ? catalog[name] : version
414
+ ])
415
+ );
416
+ return {
417
+ name: "feedbax-installation",
418
+ version: JSON.parse(
419
+ await readFile(join(coreRoot, "packages/feedbax/package.json"), "utf8")
420
+ ).version,
421
+ private: true,
422
+ type: "module",
423
+ scripts: {
424
+ dev: "vite dev",
425
+ build: "vite build",
426
+ test: "vitest run",
427
+ "type-check": "tsr generate && tsc --noEmit",
428
+ lint: "eslint .",
429
+ "format:check": "prettier --check .",
430
+ deploy: "feedbax deploy",
431
+ "deploy:dry-run": "wrangler deploy --dry-run"
432
+ },
433
+ dependencies: collect("dependencies"),
434
+ devDependencies: {
435
+ ...collect("devDependencies"),
436
+ "@eslint/js": "10.0.1",
437
+ "@types/node": "22.20.1",
438
+ feedbax: JSON.parse(
439
+ await readFile(join(coreRoot, "packages/feedbax/package.json"), "utf8")
440
+ ).version,
441
+ eslint: "10.8.1",
442
+ globals: "17.11.0",
443
+ prettier: "3.9.6",
444
+ "typescript-eslint": "8.67.0",
445
+ vitest: "4.1.11"
446
+ },
447
+ pnpm: { onlyBuiltDependencies: ["esbuild"] },
448
+ engines: { node: ">=22.14.0" },
449
+ packageManager: "pnpm@10.29.3",
450
+ license: "Apache-2.0"
451
+ };
452
+ }
453
+ var flatTsconfig = {
454
+ include: ["src/**/*.ts", "src/**/*.tsx"],
455
+ compilerOptions: {
456
+ target: "ES2022",
457
+ jsx: "react-jsx",
458
+ module: "ESNext",
459
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
460
+ types: ["vite/client", "node"],
461
+ moduleResolution: "bundler",
462
+ allowImportingTsExtensions: true,
463
+ noEmit: true,
464
+ skipLibCheck: true,
465
+ strict: true,
466
+ paths: {
467
+ "@/*": ["./src/*"],
468
+ "@feedbax/feedback": ["./src/feedbax/feedback/index.ts"],
469
+ "@feedbax/changelog": ["./src/feedbax/changelog/index.ts"],
470
+ "@feedbax/config": ["./src/feedbax/config/index.ts"],
471
+ "@feedbax/ui/*": ["./src/components/ui/*"],
472
+ "@feedbax/ui/globals.css": ["./src/components/ui/styles/globals.css"]
473
+ }
474
+ }
20
475
  };
21
- export function resolveCreateOptions(argv) {
22
- const values = new Map();
23
- let directory;
24
- for (let index = 0; index < argv.length; index += 1) {
25
- const entry = argv[index];
26
- if (valueFlags.has(entry)) {
27
- const next = argv[index + 1];
28
- if (!next || next.startsWith('-'))
29
- throw new Error(`Option ${entry} requires a value.`);
30
- values.set(entry, next);
31
- index += 1;
32
- }
33
- else if (booleanFlags.has(entry))
34
- continue;
35
- else if (entry.startsWith('-'))
36
- throw new Error(`Unknown option ${entry}.`);
37
- else if (directory)
38
- throw new Error(`Unexpected argument ${entry}.`);
39
- else
40
- directory = entry;
476
+ async function copy(from, to) {
477
+ await cp(from, to, {
478
+ recursive: true,
479
+ filter: (source) => !ignored.some((entry) => source.includes(`/${entry}`))
480
+ });
481
+ }
482
+ async function files(root) {
483
+ const entries = await readdir(root);
484
+ const result = [];
485
+ for (const entry of entries.sort()) {
486
+ const path = join(root, entry);
487
+ if ((await stat(path)).isDirectory()) result.push(...await files(path));
488
+ else result.push(path);
489
+ }
490
+ return result;
491
+ }
492
+
493
+ // ../feedback/src/notion-request.ts
494
+ var MAX_ATTEMPTS = 3;
495
+ var BASE_BACKOFF_MILLISECONDS = 250;
496
+ var MAX_BACKOFF_MILLISECONDS = 2e3;
497
+ async function requestNotion(input, init = {}, {
498
+ request,
499
+ retry = {},
500
+ operation
501
+ }) {
502
+ const sleep = retry.sleep ?? defaultSleep;
503
+ const random = retry.random ?? Math.random;
504
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
505
+ const response = await request(input, init);
506
+ if (!isRetryableNotionFailure(response.status, operation)) return response;
507
+ if (attempt === MAX_ATTEMPTS) {
508
+ throw new Error(
509
+ `Notion request failed after ${MAX_ATTEMPTS} attempts (${response.status}).`
510
+ );
511
+ }
512
+ await sleep(retryDelayMilliseconds(response, attempt, random));
513
+ }
514
+ throw new Error("Notion request retry loop ended unexpectedly.");
515
+ }
516
+ function isRetryableNotionFailure(status, operation) {
517
+ return status === 429 || status === 529 && operation === "idempotent";
518
+ }
519
+ function retryDelayMilliseconds(response, attempt, random) {
520
+ const retryAfter = response.headers.get("Retry-After");
521
+ if (retryAfter) {
522
+ const seconds = Number(retryAfter);
523
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
524
+ }
525
+ return exponentialDelayMilliseconds(attempt, random);
526
+ }
527
+ function exponentialDelayMilliseconds(attempt, random) {
528
+ const ceiling = Math.min(
529
+ BASE_BACKOFF_MILLISECONDS * 2 ** (attempt - 1),
530
+ MAX_BACKOFF_MILLISECONDS
531
+ );
532
+ return Math.round(ceiling / 2 + random() * (ceiling / 2));
533
+ }
534
+ async function defaultSleep(milliseconds) {
535
+ await new Promise((resolve2) => setTimeout(resolve2, milliseconds));
536
+ }
537
+
538
+ // ../feedback/src/notion-data-source.ts
539
+ var NOTION_API_URL = "https://api.notion.com/v1";
540
+ var NOTION_API_VERSION = "2026-03-11";
541
+ var canonicalProperties = {
542
+ title: { name: "Title", type: "title", schema: { title: {} } },
543
+ slug: { name: "Slug", type: "rich_text", schema: { rich_text: {} } },
544
+ description: {
545
+ name: "Description",
546
+ type: "rich_text",
547
+ schema: { rich_text: {} }
548
+ },
549
+ type: {
550
+ name: "Type",
551
+ type: "select",
552
+ schema: {
553
+ select: {
554
+ options: [
555
+ { name: "Feature Request" },
556
+ { name: "Bug Report" },
557
+ { name: "General Feedback" }
558
+ ]
559
+ }
560
+ }
561
+ },
562
+ status: {
563
+ name: "Status",
564
+ type: "select",
565
+ schema: {
566
+ select: {
567
+ options: [
568
+ { name: "New" },
569
+ { name: "Reviewing" },
570
+ { name: "Planned" },
571
+ { name: "In Progress" },
572
+ { name: "Shipped" },
573
+ { name: "Closed" }
574
+ ]
575
+ }
41
576
  }
42
- const value = (name) => values.get(name);
43
- directory ??= 'my-feedback';
44
- const connector = value('--connector') ?? 'notion';
45
- const storage = value('--storage') ?? 'notion';
46
- if (connector !== 'notion')
47
- throw new Error(`Connector "${connector}" is not supported in Feedbax 0.1.0.`);
48
- if (storage !== 'notion')
49
- throw new Error(`Storage "${storage}" is deferred until after Feedbax 0.1.0.`);
50
- const identity = identityMode(value('--identity') ?? 'email');
51
- const deploy = (value('--deploy') ?? 'cloudflare');
52
- if (!allowedDeployments.has(deploy))
53
- throw new Error(`Deployment "${deploy}" is not supported.`);
54
- const packageManager = (value('--package-manager') ??
55
- 'pnpm');
56
- if (!allowedPackageManagers.has(packageManager))
57
- throw new Error(`Package manager "${packageManager}" is not supported.`);
58
- return {
59
- directory,
60
- identity,
61
- deploy,
62
- packageManager,
63
- preset: value('--preset') ?? 'feedbax-default',
64
- install: !argv.includes('--no-install'),
65
- git: !argv.includes('--no-git'),
66
- };
67
- }
68
- export async function resolveInteractiveCreateOptions(argv, prompt) {
69
- if (argv.includes('--yes'))
70
- return resolveCreateOptions(argv);
71
- const prompted = [...argv];
72
- const ask = async (flag, question, defaultValue) => {
73
- if (argv.includes(flag))
74
- return;
75
- const answer = (await prompt(question, defaultValue)).trim() || defaultValue;
76
- prompted.push(flag, answer);
77
- };
78
- await ask('--identity', 'Identity (anonymous, email, handoff)', 'email');
79
- await ask('--deploy', 'Deployment (cloudflare, vercel, node)', 'cloudflare');
80
- await ask('--package-manager', 'Package manager (pnpm, npm)', 'pnpm');
81
- await ask('--preset', 'shadcn preset', 'feedbax-default');
82
- return resolveCreateOptions(prompted);
83
- }
84
- async function assertEmpty(path) {
85
- try {
86
- if ((await readdir(path)).length > 0)
87
- throw new Error(`Target directory is not empty: ${path}`);
577
+ },
578
+ published: {
579
+ name: "Published",
580
+ type: "checkbox",
581
+ schema: { checkbox: {} }
582
+ },
583
+ voteCount: {
584
+ name: "Vote Count",
585
+ type: "number",
586
+ schema: { number: { format: "number" } }
587
+ },
588
+ teamResponse: {
589
+ name: "Team Response",
590
+ type: "rich_text",
591
+ schema: { rich_text: {} }
592
+ },
593
+ releaseNotesUrl: {
594
+ name: "Release Notes URL",
595
+ type: "url",
596
+ schema: { url: {} }
597
+ },
598
+ submitterName: {
599
+ name: "Submitter Name",
600
+ type: "rich_text",
601
+ schema: { rich_text: {} }
602
+ },
603
+ submitterEmail: {
604
+ name: "Submitter Email",
605
+ type: "email",
606
+ schema: { email: {} }
607
+ },
608
+ source: {
609
+ name: "Source",
610
+ type: "select",
611
+ schema: {
612
+ select: {
613
+ options: [{ name: "Portal" }, { name: "API" }, { name: "Team" }]
614
+ }
88
615
  }
89
- catch (error) {
90
- if (error.code !== 'ENOENT')
91
- throw error;
616
+ },
617
+ externalId: {
618
+ name: "External ID",
619
+ type: "rich_text",
620
+ schema: { rich_text: {} }
621
+ },
622
+ editTokenHash: {
623
+ name: "Edit Token Hash",
624
+ type: "rich_text",
625
+ schema: { rich_text: {} }
626
+ },
627
+ createdAt: {
628
+ name: "Created At",
629
+ type: "created_time",
630
+ schema: { created_time: {} }
631
+ },
632
+ updatedAt: {
633
+ name: "Updated At",
634
+ type: "last_edited_time",
635
+ schema: { last_edited_time: {} }
636
+ }
637
+ };
638
+ var feedbackWorkflowProperties = {
639
+ Owner: { people: {} },
640
+ Priority: {
641
+ select: {
642
+ options: [
643
+ { name: "Urgent" },
644
+ { name: "High" },
645
+ { name: "Medium" },
646
+ { name: "Low" }
647
+ ]
648
+ }
649
+ },
650
+ "Strategic Theme": { multi_select: { options: [] } },
651
+ "Participant Segment": { multi_select: { options: [] } },
652
+ Impact: {
653
+ select: {
654
+ options: [{ name: "High" }, { name: "Medium" }, { name: "Low" }]
92
655
  }
656
+ },
657
+ Effort: {
658
+ select: {
659
+ options: [{ name: "Small" }, { name: "Medium" }, { name: "Large" }]
660
+ }
661
+ },
662
+ "Decision Note": { rich_text: {} },
663
+ "Needs Follow-up": { checkbox: {} }
664
+ };
665
+ async function createNotionFeedbackDataSource({
666
+ token,
667
+ parentPageId,
668
+ request = fetch,
669
+ retry
670
+ }) {
671
+ const response = await requestNotion(
672
+ `${NOTION_API_URL}/databases`,
673
+ {
674
+ method: "POST",
675
+ headers: notionHeaders(token),
676
+ body: JSON.stringify({
677
+ parent: { type: "page_id", page_id: parentPageId },
678
+ title: [{ type: "text", text: { content: "Feedback" } }],
679
+ initial_data_source: {
680
+ properties: {
681
+ ...Object.fromEntries(
682
+ Object.values(canonicalProperties).map(({ name, schema }) => [
683
+ name,
684
+ schema
685
+ ])
686
+ ),
687
+ ...feedbackWorkflowProperties
688
+ }
689
+ }
690
+ })
691
+ },
692
+ { request, retry, operation: "create" }
693
+ );
694
+ const database = await readNotionResponse(response);
695
+ if (typeof database.id !== "string") {
696
+ throw new Error("Notion did not return the created Feedback database.");
697
+ }
698
+ const dataSourceId = firstDataSourceId(database);
699
+ const dataSourceResponse = await requestNotion(
700
+ `${NOTION_API_URL}/data_sources/${dataSourceId}`,
701
+ { method: "GET", headers: notionHeaders(token) },
702
+ { request, retry, operation: "idempotent" }
703
+ );
704
+ const dataSource = await readNotionResponse(dataSourceResponse);
705
+ return configurationFromCanonicalNames(dataSource, database.id);
93
706
  }
94
- async function replaceInTree(path, replacements) {
95
- for (const entry of await readdir(path)) {
96
- const target = join(path, entry);
97
- if ((await stat(target)).isDirectory())
98
- await replaceInTree(target, replacements);
99
- else {
100
- let content = await readFile(target, 'utf8');
101
- for (const [token, replacement] of Object.entries(replacements))
102
- content = content.replaceAll(token, replacement);
103
- await writeFile(target, content);
707
+ async function createFeedbackWorkflowViews({
708
+ token,
709
+ databaseId,
710
+ dataSourceId,
711
+ propertyIds,
712
+ request = fetch
713
+ }) {
714
+ const views = [
715
+ {
716
+ name: "Inbox",
717
+ type: "table",
718
+ filter: selectFilter(propertyIds.status, "New"),
719
+ sorts: [{ property: propertyIds.createdAt, direction: "descending" }]
720
+ },
721
+ {
722
+ name: "Reviewing",
723
+ type: "table",
724
+ filter: selectFilter(propertyIds.status, "Reviewing"),
725
+ sorts: [{ property: propertyIds.updatedAt, direction: "descending" }]
726
+ },
727
+ {
728
+ name: "Prioritization",
729
+ type: "table",
730
+ filter: {
731
+ or: [
732
+ selectFilter(propertyIds.status, "Reviewing"),
733
+ selectFilter(propertyIds.status, "Planned")
734
+ ]
735
+ },
736
+ sorts: [
737
+ { property: "Priority", direction: "ascending" },
738
+ { property: propertyIds.voteCount, direction: "descending" }
739
+ ]
740
+ },
741
+ {
742
+ name: "Follow-up",
743
+ type: "table",
744
+ filter: { property: "Needs Follow-up", checkbox: { equals: true } }
745
+ },
746
+ {
747
+ name: "Roadmap",
748
+ type: "board",
749
+ filter: {
750
+ or: ["Planned", "In Progress", "Shipped"].map(
751
+ (status) => selectFilter(propertyIds.status, status)
752
+ )
753
+ },
754
+ configuration: {
755
+ type: "board",
756
+ group_by: {
757
+ type: "select",
758
+ property_id: propertyIds.status,
759
+ group_by: "option",
760
+ sort: { type: "manual" }
104
761
  }
762
+ }
763
+ },
764
+ {
765
+ name: "Shipped",
766
+ type: "table",
767
+ filter: selectFilter(propertyIds.status, "Shipped"),
768
+ sorts: [{ property: propertyIds.updatedAt, direction: "descending" }]
105
769
  }
770
+ ];
771
+ for (const view of views) {
772
+ const response = await requestNotion(
773
+ `${NOTION_API_URL}/views`,
774
+ {
775
+ method: "POST",
776
+ headers: notionHeaders(token),
777
+ body: JSON.stringify({
778
+ database_id: databaseId,
779
+ data_source_id: dataSourceId,
780
+ ...view
781
+ })
782
+ },
783
+ { request, operation: "create" }
784
+ );
785
+ await readNotionResponse(response);
786
+ }
106
787
  }
107
- const run = (command, args, cwd) => new Promise((resolvePromise, reject) => {
108
- const child = spawn(command, args, { cwd, stdio: 'inherit', shell: false });
109
- child.once('error', reject);
110
- child.once('exit', (code) => code === 0
111
- ? resolvePromise()
112
- : reject(new Error(`${command} ${args.join(' ')} failed with ${code}.`)));
113
- });
114
- export async function createProject(options) {
115
- const destination = resolve(options.directory);
116
- await assertEmpty(destination);
117
- await mkdir(dirname(destination), { recursive: true });
118
- const packageDirectory = dirname(fileURLToPath(import.meta.url));
119
- const candidates = [
120
- resolve(packageDirectory, '../template'),
121
- resolve(packageDirectory, '../../../templates/default'),
122
- ];
123
- let template;
124
- for (const candidate of candidates) {
125
- try {
126
- if ((await stat(candidate)).isDirectory()) {
127
- template = candidate;
128
- break;
129
- }
130
- }
131
- catch {
132
- continue;
788
+ function selectFilter(property, value) {
789
+ return { property, select: { equals: value } };
790
+ }
791
+ async function validateNotionFeedbackDataSource({
792
+ token,
793
+ dataSourceId,
794
+ propertyIds,
795
+ request = fetch,
796
+ retry
797
+ }) {
798
+ const response = await requestNotion(
799
+ `${NOTION_API_URL}/data_sources/${dataSourceId}`,
800
+ {
801
+ method: "GET",
802
+ headers: notionHeaders(token)
803
+ },
804
+ { request, retry, operation: "idempotent" }
805
+ );
806
+ const dataSource = await readNotionResponse(response);
807
+ const properties = dataSource.properties;
808
+ if (typeof dataSource.id !== "string" || !isPropertyMap(properties)) {
809
+ throw new Error(
810
+ "Notion returned an invalid Feedback Data Source response."
811
+ );
812
+ }
813
+ const propertiesById = new Map(
814
+ Object.values(properties).filter(
815
+ (property) => Boolean(property) && typeof property.id === "string"
816
+ ).map((property) => [property.id, property])
817
+ );
818
+ const repairs = [];
819
+ for (const [key, { name, type }] of Object.entries(canonicalProperties)) {
820
+ const configuredId = propertyIds[key];
821
+ const property = propertiesById.get(configuredId);
822
+ if (!property) {
823
+ repairs.push(
824
+ `Restore the "${name}" property with ID "${configuredId}" and type "${type}", then update the configured property ID if Notion assigns a new one.`
825
+ );
826
+ } else if (property.type !== type) {
827
+ repairs.push(
828
+ `Change the property configured as "${name}" (ID "${configuredId}") from "${String(property.type)}" to "${type}", or restore a ${type} property and configure its ID.`
829
+ );
830
+ }
831
+ }
832
+ if (repairs.length > 0) {
833
+ throw new Error(repairs.join("\n"));
834
+ }
835
+ return {
836
+ databaseId: databaseIdFromDataSource(dataSource),
837
+ dataSourceId,
838
+ propertyIds: { ...propertyIds }
839
+ };
840
+ }
841
+ function notionHeaders(token) {
842
+ return {
843
+ Authorization: `Bearer ${token}`,
844
+ "Content-Type": "application/json",
845
+ "Notion-Version": NOTION_API_VERSION
846
+ };
847
+ }
848
+ async function readNotionResponse(response) {
849
+ const body = await response.json();
850
+ if (!response.ok) {
851
+ throw new Error(
852
+ `Notion rejected the Feedback Data Source request (${response.status}).`
853
+ );
854
+ }
855
+ if (!body || typeof body !== "object") {
856
+ throw new Error(
857
+ "Notion returned an invalid Feedback Data Source response."
858
+ );
859
+ }
860
+ return body;
861
+ }
862
+ function firstDataSourceId(database) {
863
+ const dataSources = Reflect.get(database, "data_sources");
864
+ const firstDataSource = Array.isArray(dataSources) ? dataSources[0] : void 0;
865
+ const id = firstDataSource && typeof firstDataSource === "object" ? Reflect.get(firstDataSource, "id") : void 0;
866
+ if (typeof id !== "string") {
867
+ throw new Error("Notion did not return the created Feedback Data Source.");
868
+ }
869
+ return id;
870
+ }
871
+ function configurationFromCanonicalNames(dataSource, databaseId) {
872
+ const properties = dataSource.properties;
873
+ if (typeof dataSource.id !== "string" || !isPropertyMap(properties)) {
874
+ throw new Error(
875
+ "Notion returned an invalid Feedback Data Source response."
876
+ );
877
+ }
878
+ return {
879
+ databaseId,
880
+ dataSourceId: dataSource.id,
881
+ propertyIds: Object.fromEntries(
882
+ Object.entries(canonicalProperties).map(([key, { name }]) => {
883
+ const property = properties[name];
884
+ if (!property || typeof property.id !== "string") {
885
+ throw new Error(
886
+ `Notion did not return the required ${name} property.`
887
+ );
133
888
  }
889
+ return [key, property.id];
890
+ })
891
+ )
892
+ };
893
+ }
894
+ function databaseIdFromDataSource(dataSource) {
895
+ const parent = dataSource.parent;
896
+ const databaseId = parent && typeof parent === "object" ? Reflect.get(parent, "database_id") : void 0;
897
+ if (typeof databaseId !== "string") {
898
+ throw new Error(
899
+ "Notion did not return the Feedback Data Source parent database."
900
+ );
901
+ }
902
+ return databaseId;
903
+ }
904
+ function isPropertyMap(value) {
905
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
906
+ }
907
+
908
+ // ../feedback/src/index.ts
909
+ var postCreationQueue = Promise.resolve();
910
+
911
+ // ../changelog/src/notion-changelog.ts
912
+ var NOTION_API_URL2 = "https://api.notion.com/v1";
913
+ var NOTION_API_VERSION2 = "2026-03-11";
914
+ var canonicalProperties2 = {
915
+ title: { name: "Title", type: "title", schema: { title: {} } },
916
+ slug: { name: "Slug", type: "rich_text", schema: { rich_text: {} } },
917
+ date: { name: "Date", type: "date", schema: { date: {} } },
918
+ summary: {
919
+ name: "Summary",
920
+ type: "rich_text",
921
+ schema: { rich_text: {} }
922
+ },
923
+ body: { name: "Body", type: "rich_text", schema: { rich_text: {} } },
924
+ labels: {
925
+ name: "Labels",
926
+ type: "multi_select",
927
+ schema: { multi_select: { options: [] } }
928
+ },
929
+ image: { name: "Image", type: "files", schema: { files: {} } },
930
+ published: {
931
+ name: "Published",
932
+ type: "checkbox",
933
+ schema: { checkbox: {} }
934
+ },
935
+ createdAt: {
936
+ name: "Created At",
937
+ type: "created_time",
938
+ schema: { created_time: {} }
939
+ },
940
+ updatedAt: {
941
+ name: "Updated At",
942
+ type: "last_edited_time",
943
+ schema: { last_edited_time: {} }
944
+ }
945
+ };
946
+ async function createNotionChangelogDataSource({
947
+ token,
948
+ databaseId,
949
+ request = fetch
950
+ }) {
951
+ const response = await request(`${NOTION_API_URL2}/data_sources`, {
952
+ method: "POST",
953
+ headers: notionHeaders2(token),
954
+ body: JSON.stringify({
955
+ parent: { database_id: databaseId },
956
+ title: [{ type: "text", text: { content: "Changelog" } }],
957
+ properties: Object.fromEntries(
958
+ Object.values(canonicalProperties2).map(({ name, schema }) => [
959
+ name,
960
+ schema
961
+ ])
962
+ )
963
+ })
964
+ });
965
+ return configurationFromResponse(
966
+ await readObject(response, "Changelog Data Source"),
967
+ databaseId
968
+ );
969
+ }
970
+ async function validateNotionChangelogDataSource({
971
+ token,
972
+ dataSourceId,
973
+ propertyIds,
974
+ request = fetch
975
+ }) {
976
+ const response = await request(
977
+ `${NOTION_API_URL2}/data_sources/${dataSourceId}`,
978
+ { headers: notionHeaders2(token) }
979
+ );
980
+ const dataSource = await readObject(response, "Changelog Data Source");
981
+ const properties = recordAt(dataSource, "properties");
982
+ const propertiesById = new Map(
983
+ Object.values(properties).flatMap((property) => {
984
+ if (!isRecord(property) || typeof property.id !== "string") return [];
985
+ return [[property.id, property]];
986
+ })
987
+ );
988
+ const repairs = [];
989
+ for (const [key, { name, type }] of Object.entries(canonicalProperties2)) {
990
+ const configuredId = propertyIds[key];
991
+ const property = propertiesById.get(configuredId);
992
+ if (!property) {
993
+ repairs.push(
994
+ `Restore the "${name}" property with ID "${configuredId}" and type "${type}", then update the configured property ID if Notion assigns a new one.`
995
+ );
996
+ } else if (property.type !== type) {
997
+ repairs.push(
998
+ `Change the property configured as "${name}" (ID "${configuredId}") from "${String(property.type)}" to "${type}", or restore a ${type} property and configure its ID.`
999
+ );
134
1000
  }
135
- if (!template)
136
- throw new Error('The Feedbax project template is missing from this package.');
137
- await cp(template, destination, { recursive: true });
138
- await replaceInTree(destination, {
139
- __GENERATOR_VERSION__: '0.1.0',
140
- __IDENTITY__: options.identity,
141
- __DEPLOYMENT__: options.deploy,
142
- __PACKAGE_MANAGER__: options.packageManager,
143
- __PACKAGE_MANAGER_SPEC__: options.packageManager === 'pnpm' ? 'pnpm@10.29.3' : 'npm@11.4.2',
144
- __PRESET__: options.preset,
145
- __NITRO_PRESET__: options.deploy === 'vercel'
146
- ? 'vercel'
147
- : options.deploy === 'cloudflare'
148
- ? 'cloudflare-module'
149
- : 'node-server',
1001
+ }
1002
+ if (repairs.length) throw new Error(repairs.join("\n"));
1003
+ return {
1004
+ databaseId: databaseIdFromResponse(dataSource),
1005
+ dataSourceId,
1006
+ propertyIds: { ...propertyIds }
1007
+ };
1008
+ }
1009
+ function configurationFromResponse(dataSource, databaseId) {
1010
+ const properties = recordAt(dataSource, "properties");
1011
+ return {
1012
+ databaseId,
1013
+ dataSourceId: requiredString(dataSource.id),
1014
+ propertyIds: Object.fromEntries(
1015
+ Object.entries(canonicalProperties2).map(([key, { name }]) => [
1016
+ key,
1017
+ requiredString(recordAt(properties, name).id)
1018
+ ])
1019
+ )
1020
+ };
1021
+ }
1022
+ function databaseIdFromResponse(dataSource) {
1023
+ return requiredString(recordAt(dataSource, "parent").database_id);
1024
+ }
1025
+ function notionHeaders2(token) {
1026
+ return {
1027
+ Authorization: `Bearer ${token}`,
1028
+ "Content-Type": "application/json",
1029
+ "Notion-Version": NOTION_API_VERSION2
1030
+ };
1031
+ }
1032
+ async function readObject(response, subject) {
1033
+ const body = await response.json();
1034
+ if (!response.ok) {
1035
+ throw new Error(
1036
+ `Notion rejected the ${subject} request (${response.status}).`
1037
+ );
1038
+ }
1039
+ if (!isRecord(body))
1040
+ throw new Error(`Notion returned an invalid ${subject} response.`);
1041
+ return body;
1042
+ }
1043
+ function recordAt(value, key) {
1044
+ return record(value[key]);
1045
+ }
1046
+ function record(value) {
1047
+ if (!isRecord(value))
1048
+ throw new Error("Notion returned an invalid Changelog response.");
1049
+ return value;
1050
+ }
1051
+ function requiredString(value) {
1052
+ if (typeof value !== "string")
1053
+ throw new Error("Notion returned an invalid Changelog response.");
1054
+ return value;
1055
+ }
1056
+ function isRecord(value) {
1057
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1058
+ }
1059
+
1060
+ // src/index.ts
1061
+ async function provisionGuidedInstallation({
1062
+ destination,
1063
+ product,
1064
+ themePreset,
1065
+ workerName,
1066
+ commentProbePageId,
1067
+ features,
1068
+ feedbackSelection,
1069
+ changelogSelection,
1070
+ notionToken,
1071
+ turnstile,
1072
+ preview,
1073
+ request = fetch
1074
+ }) {
1075
+ try {
1076
+ if ((await readdir2(destination)).length)
1077
+ throw new Error(
1078
+ `Refusing to generate into non-empty destination: ${destination}`
1079
+ );
1080
+ } catch (error) {
1081
+ if (error.code !== "ENOENT") throw error;
1082
+ }
1083
+ if (product.siteUrl && new URL(product.siteUrl).protocol !== "https:")
1084
+ throw new Error("product.siteUrl must be an HTTPS URL.");
1085
+ const targetName = workerName ?? basename(destination).toLowerCase().replace(/[^a-z0-9-]/g, "-");
1086
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(targetName))
1087
+ throw new Error(
1088
+ "Worker name must contain 1\u201363 lowercase letters, numbers, or hyphens."
1089
+ );
1090
+ const approved = await preview(
1091
+ feedbackSelection.kind === "create" ? `Create the canonical Feedback Data Source inside Notion page ${feedbackSelection.parentPageId}.` : `Validate existing Feedback Data Source ${feedbackSelection.configuration.dataSourceId} without changing it.`
1092
+ );
1093
+ if (!approved)
1094
+ throw new Error(
1095
+ "Feedback setup was not approved. No local files were created."
1096
+ );
1097
+ const feedback = feedbackSelection.kind === "create" ? await createNotionFeedbackDataSource({
1098
+ token: notionToken,
1099
+ parentPageId: feedbackSelection.parentPageId,
1100
+ request
1101
+ }) : await validateNotionFeedbackDataSource({
1102
+ token: notionToken,
1103
+ ...feedbackSelection.configuration,
1104
+ request
1105
+ });
1106
+ if (feedbackSelection.kind === "create") {
1107
+ if (!await preview(
1108
+ "Create six Notion team views: Inbox, Reviewing, Prioritization, Follow-up, Roadmap, and Shipped."
1109
+ ))
1110
+ throw new Error("Workflow view creation was not approved.");
1111
+ await createFeedbackWorkflowViews({
1112
+ token: notionToken,
1113
+ ...feedback,
1114
+ request
150
1115
  });
151
- try {
152
- if (options.install) {
153
- await run(options.packageManager, ['install'], destination);
154
- await run(options.packageManager, ['run', 'type-check'], destination);
155
- }
156
- if (options.git)
157
- await run('git', ['init'], destination);
1116
+ }
1117
+ await verifySelectedCommentCapabilities(features, {
1118
+ token: notionToken,
1119
+ pageId: commentProbePageId ?? (feedbackSelection.kind === "create" ? feedbackSelection.parentPageId : feedback.databaseId),
1120
+ request,
1121
+ command: "setup"
1122
+ });
1123
+ const changelog = await configureChangelogStorage({
1124
+ features,
1125
+ selection: changelogSelection ?? (features.changelog ? { kind: "create", databaseId: feedback.databaseId } : void 0),
1126
+ token: notionToken,
1127
+ request,
1128
+ preview
1129
+ });
1130
+ await copyPackagedTemplate(destination);
1131
+ const wranglerPath = join2(destination, "wrangler.jsonc");
1132
+ const wrangler = JSON.parse(await readFile2(wranglerPath, "utf8"));
1133
+ await writeFile2(
1134
+ wranglerPath,
1135
+ JSON.stringify({ ...wrangler, name: targetName }, null, 2) + "\n"
1136
+ );
1137
+ const apiKey = randomBytes(32).toString("base64url");
1138
+ const signingSecret = randomBytes(32).toString("base64url");
1139
+ const values = {
1140
+ NOTION_TOKEN: notionToken,
1141
+ NOTION_FEEDBACK_DATABASE_ID: feedback.databaseId,
1142
+ NOTION_FEEDBACK_DATA_SOURCE_ID: feedback.dataSourceId,
1143
+ ...prefixProperties("NOTION_FEEDBACK", feedback.propertyIds),
1144
+ FEEDBAX_API_KEY_HASH: createHash2("sha256").update(apiKey).digest("base64url"),
1145
+ PARTICIPATION_SIGNING_SECRET: signingSecret,
1146
+ FEEDBAX_VOTING_ENABLED: String(features.voting),
1147
+ FEEDBAX_COMMENTS_ENABLED: String(features.comments),
1148
+ FEEDBAX_CHANGELOG_ENABLED: String(features.changelog),
1149
+ ...turnstile.siteKey ? { VITE_TURNSTILE_SITE_KEY: turnstile.siteKey } : {},
1150
+ ...turnstile.secretKey ? { TURNSTILE_SECRET_KEY: turnstile.secretKey } : {},
1151
+ ...changelog ? {
1152
+ NOTION_CHANGELOG_DATABASE_ID: changelog.databaseId,
1153
+ NOTION_CHANGELOG_DATA_SOURCE_ID: changelog.dataSourceId,
1154
+ ...prefixProperties("NOTION_CHANGELOG", changelog.propertyIds)
1155
+ } : {}
1156
+ };
1157
+ await writeFile2(
1158
+ join2(destination, ".dev.vars"),
1159
+ Object.entries(values).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join("\n") + "\n",
1160
+ { mode: 384 }
1161
+ );
1162
+ const configuration = defineFeedbax({
1163
+ product: { ...product, logo: "/feedbax-mark.svg" },
1164
+ theme: { preset: themePreset },
1165
+ notion: {
1166
+ databaseId: feedback.databaseId,
1167
+ dataSourceId: feedback.dataSourceId,
1168
+ properties: feedback.propertyIds
1169
+ },
1170
+ ...changelog ? {
1171
+ changelog: {
1172
+ databaseId: changelog.databaseId,
1173
+ dataSourceId: changelog.dataSourceId,
1174
+ properties: changelog.propertyIds
1175
+ }
1176
+ } : {},
1177
+ features,
1178
+ submissions: {
1179
+ moderation: "required",
1180
+ maxTitleLength: 160,
1181
+ maxDescriptionLength: 5e3
1182
+ },
1183
+ turnstile: {
1184
+ enabled: Boolean(turnstile.siteKey),
1185
+ ...turnstile.siteKey ? { siteKey: turnstile.siteKey } : {}
158
1186
  }
159
- catch (error) {
160
- await writeFile(join(destination, '.feedbax-recovery'), `Generation stopped: ${error instanceof Error ? error.message : String(error)}\nRerun dependency installation and type checking after resolving the reported issue.\n`);
161
- throw error;
1187
+ });
1188
+ await writeFile2(
1189
+ join2(destination, "src/feedbax.ts"),
1190
+ `import { defineFeedbax } from "@feedbax/config";
1191
+
1192
+ export default defineFeedbax(${JSON.stringify(configuration, null, 2)});
1193
+ `
1194
+ );
1195
+ const probePage = commentProbePageId ?? (feedbackSelection.kind === "create" ? feedbackSelection.parentPageId : feedback.databaseId);
1196
+ await writeFile2(
1197
+ join2(destination, ".dev.vars"),
1198
+ `NOTION_COMMENT_CAPABILITY_PROBE_PAGE_ID=${JSON.stringify(probePage)}
1199
+ `,
1200
+ { flag: "a", mode: 384 }
1201
+ );
1202
+ return { apiKey, feedback, changelog };
1203
+ }
1204
+ function prefixProperties(prefix, properties) {
1205
+ return Object.fromEntries(
1206
+ Object.entries(properties).map(([key, value]) => [
1207
+ `${prefix}_${key.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase()}_PROPERTY_ID`,
1208
+ value
1209
+ ])
1210
+ );
1211
+ }
1212
+ var featureChoices = [
1213
+ { key: "voting", label: "Voting", initialValue: true },
1214
+ { key: "comments", label: "Comments", initialValue: true },
1215
+ { key: "changelog", label: "Changelog", initialValue: true }
1216
+ ];
1217
+ async function collectFeatureSelection(prompter) {
1218
+ const selections = [];
1219
+ for (const { key, label, initialValue } of featureChoices) {
1220
+ selections.push([
1221
+ key,
1222
+ await prompter.confirm({
1223
+ message: `Enable ${label}?`,
1224
+ initialValue
1225
+ })
1226
+ ]);
1227
+ }
1228
+ return Object.fromEntries(selections);
1229
+ }
1230
+ function renderFeatureConfiguration(features) {
1231
+ return `features: {
1232
+ voting: ${features.voting},
1233
+ comments: ${features.comments},
1234
+ changelog: ${features.changelog},
1235
+ },`;
1236
+ }
1237
+ function renderChangelogConfiguration(configuration) {
1238
+ const properties = Object.entries(configuration.propertyIds).map(([key, value]) => ` ${key}: ${JSON.stringify(value)},`).join("\n");
1239
+ return `changelog: {
1240
+ databaseId: ${JSON.stringify(configuration.databaseId)},
1241
+ dataSourceId: ${JSON.stringify(configuration.dataSourceId)},
1242
+ propertyIds: {
1243
+ ${properties}
1244
+ },
1245
+ },`;
1246
+ }
1247
+ async function verifySelectedCommentCapabilities(features, {
1248
+ token,
1249
+ pageId,
1250
+ request = fetch,
1251
+ command = "setup"
1252
+ }) {
1253
+ if (!features.comments) return;
1254
+ const headers = {
1255
+ Authorization: `Bearer ${token}`,
1256
+ "Notion-Version": "2026-03-11"
1257
+ };
1258
+ const readResponse = await request(
1259
+ `https://api.notion.com/v1/comments?block_id=${encodeURIComponent(pageId)}&page_size=1`,
1260
+ { headers }
1261
+ );
1262
+ if (!readResponse.ok) {
1263
+ if (readResponse.status === 403)
1264
+ assertEnabledCommentCapabilities({
1265
+ commentsEnabled: true,
1266
+ capabilities: { readComments: false, insertComments: true },
1267
+ command
1268
+ });
1269
+ throw new Error(
1270
+ `Notion Comment read check failed (HTTP ${readResponse.status}). Verify a shared page ID and connection access.`
1271
+ );
1272
+ }
1273
+ if (command === "doctor") {
1274
+ assertEnabledCommentCapabilities({
1275
+ commentsEnabled: true,
1276
+ capabilities: {
1277
+ readComments: readResponse.status !== 403,
1278
+ // Setup proves Insert comments before enabling the capability. Doctor
1279
+ // deliberately avoids a write-method probe and verifies the live read path.
1280
+ insertComments: true
1281
+ },
1282
+ command
1283
+ });
1284
+ return;
1285
+ }
1286
+ const insertResponse = await request("https://api.notion.com/v1/comments", {
1287
+ method: "POST",
1288
+ headers: { ...headers, "Content-Type": "application/json" },
1289
+ body: "{}"
1290
+ });
1291
+ if (insertResponse.status !== 400 && insertResponse.status !== 403)
1292
+ throw new Error(
1293
+ `Notion Comment insert capability check failed (HTTP ${insertResponse.status}).`
1294
+ );
1295
+ const capabilities = {
1296
+ readComments: readResponse.status !== 403,
1297
+ insertComments: insertResponse.status !== 403
1298
+ };
1299
+ assertEnabledCommentCapabilities({
1300
+ commentsEnabled: true,
1301
+ capabilities,
1302
+ command
1303
+ });
1304
+ }
1305
+ function assertEnabledCommentCapabilities({
1306
+ commentsEnabled,
1307
+ capabilities,
1308
+ command = "setup"
1309
+ }) {
1310
+ if (!commentsEnabled) return;
1311
+ const missing = [
1312
+ ...capabilities.readComments ? [] : ["Read comments"],
1313
+ ...capabilities.insertComments ? [] : ["Insert comments"]
1314
+ ];
1315
+ if (missing.length === 0) return;
1316
+ const retry = command === "doctor" ? "rerun doctor. No changes were made." : "retry setup.";
1317
+ throw new Error(
1318
+ `Enable ${missing.join(" and ")} in the Notion connection settings, then ${retry}`
1319
+ );
1320
+ }
1321
+ async function configureChangelogStorage({
1322
+ features,
1323
+ selection,
1324
+ token,
1325
+ request = fetch,
1326
+ preview
1327
+ }) {
1328
+ if (!features.changelog) return;
1329
+ if (!selection) {
1330
+ throw new Error(
1331
+ "Choose an existing compatible Changelog Data Source or create one beside Feedback."
1332
+ );
1333
+ }
1334
+ if (selection.kind === "existing") {
1335
+ return validateNotionChangelogDataSource({
1336
+ token,
1337
+ dataSourceId: selection.dataSourceId,
1338
+ propertyIds: selection.propertyIds,
1339
+ request
1340
+ });
1341
+ }
1342
+ const approved = await preview(
1343
+ `Create an empty Changelog Data Source beside Feedback in Feedbax Database ${selection.databaseId}. No sample entries will be published.`
1344
+ );
1345
+ if (!approved) throw new Error("Changelog creation was not approved.");
1346
+ return createNotionChangelogDataSource({
1347
+ token,
1348
+ databaseId: selection.databaseId,
1349
+ request
1350
+ });
1351
+ }
1352
+ async function doctorChangelogStorage({
1353
+ enabled,
1354
+ configuration,
1355
+ token,
1356
+ request = fetch
1357
+ }) {
1358
+ if (!enabled) return;
1359
+ if (!configuration) {
1360
+ throw new Error(
1361
+ "Changelog is enabled without identifiers and property mappings. Create or select a compatible Changelog Data Source, configure it, then rerun doctor. No changes were made."
1362
+ );
1363
+ }
1364
+ await validateNotionChangelogDataSource({
1365
+ token,
1366
+ dataSourceId: configuration.dataSourceId,
1367
+ propertyIds: configuration.propertyIds,
1368
+ request
1369
+ });
1370
+ }
1371
+ async function doctorInstallation({
1372
+ features,
1373
+ feedback,
1374
+ changelog,
1375
+ commentProbePageId,
1376
+ token,
1377
+ request = fetch
1378
+ }) {
1379
+ await validateNotionFeedbackDataSource({ token, ...feedback, request });
1380
+ if (features.comments) {
1381
+ if (!commentProbePageId) {
1382
+ throw new Error(
1383
+ "Comments are enabled but no Comment capability probe page is configured. Configure one, then rerun doctor. No changes were made."
1384
+ );
162
1385
  }
163
- return {
164
- destination,
165
- nextSteps: [
166
- `cd ${options.directory}`,
167
- 'Copy .env.example to .env and configure Notion',
168
- `${options.packageManager === 'pnpm' ? 'pnpm' : 'npx'} feedbax doctor`,
169
- `${options.packageManager} run dev`,
170
- ],
171
- };
172
- }
173
- //# sourceMappingURL=index.js.map
1386
+ await verifySelectedCommentCapabilities(features, {
1387
+ token,
1388
+ pageId: commentProbePageId,
1389
+ request,
1390
+ command: "doctor"
1391
+ });
1392
+ }
1393
+ await doctorChangelogStorage({
1394
+ enabled: features.changelog,
1395
+ configuration: changelog,
1396
+ token,
1397
+ request
1398
+ });
1399
+ }
1400
+ export {
1401
+ assembleFlatTemplate,
1402
+ assertEnabledCommentCapabilities,
1403
+ collectFeatureSelection,
1404
+ configureChangelogStorage,
1405
+ copyPackagedTemplate,
1406
+ doctorChangelogStorage,
1407
+ doctorInstallation,
1408
+ featureChoices,
1409
+ provisionGuidedInstallation,
1410
+ renderChangelogConfiguration,
1411
+ renderFeatureConfiguration,
1412
+ templateChecksum,
1413
+ verifyPackagedTemplate,
1414
+ verifySelectedCommentCapabilities
1415
+ };