create-feedbax 0.2.1 → 0.2.2

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 (78) hide show
  1. package/README.md +17 -5
  2. package/dist/cli.js +502 -209
  3. package/dist/index.js +251 -68
  4. package/package.json +7 -7
  5. package/template/.feedbax-template.sha256 +1 -1
  6. package/template/.gitignore.template +1 -0
  7. package/template/.npmignore.template +1 -0
  8. package/template/.nvmrc +1 -0
  9. package/template/AGENTS.md +1 -1
  10. package/template/CONNECTED.md +16 -2
  11. package/template/README.md +52 -13
  12. package/template/feedbax-vite.ts +79 -0
  13. package/template/migrations/0005_github.sql +4 -0
  14. package/template/migrations/0006_integration.sql +3 -0
  15. package/template/migrations/0007_ownership.sql +2 -0
  16. package/template/migrations/0008_integration_recovery.sql +5 -0
  17. package/template/migrations/0009_observed_history.sql +3 -0
  18. package/template/package.json +34 -33
  19. package/template/src/changelog-runtime.ts +2 -1
  20. package/template/src/cloudflare-workers.d.ts +3 -0
  21. package/template/src/comment-creation.test.tsx +53 -6
  22. package/template/src/components/connected-identity-provider.tsx +44 -17
  23. package/template/src/components/follow-post-button.tsx +2 -13
  24. package/template/src/components/post-history.tsx +101 -0
  25. package/template/src/components/profile-menu.tsx +7 -0
  26. package/template/src/configured-feedback.test.ts +82 -0
  27. package/template/src/configured-feedback.ts +61 -0
  28. package/template/src/connected-identity.test.ts +7 -3
  29. package/template/src/connected-identity.ts +6 -1
  30. package/template/src/connected-runtime.test.ts +5 -2
  31. package/template/src/feedback-cache-invalidation.ts +2 -0
  32. package/template/src/feedback-runtime.ts +61 -18
  33. package/template/src/feedbax/changelog/index.test.ts +35 -0
  34. package/template/src/feedbax/changelog/index.ts +5 -1
  35. package/template/src/feedbax/changelog/notion-changelog.test.ts +1 -1
  36. package/template/src/feedbax/changelog/notion-changelog.ts +1 -1
  37. package/template/src/feedbax/config/index.test.ts +45 -1
  38. package/template/src/feedbax/config/index.ts +112 -9
  39. package/template/src/feedbax/feedback/comment-storage.ts +38 -0
  40. package/template/src/feedbax/feedback/connected-feedback.ts +41 -12
  41. package/template/src/feedbax/feedback/connected.ts +15 -5
  42. package/template/src/feedbax/feedback/feedback-storage.ts +1 -0
  43. package/template/src/feedbax/feedback/github-comments.ts +390 -0
  44. package/template/src/feedbax/feedback/github-feedback.test.ts +1138 -0
  45. package/template/src/feedbax/feedback/github-feedback.ts +581 -0
  46. package/template/src/feedbax/feedback/github-request.ts +120 -0
  47. package/template/src/feedbax/feedback/github-service-client.ts +56 -0
  48. package/template/src/feedbax/feedback/image-feedback.ts +23 -10
  49. package/template/src/feedbax/feedback/images.ts +26 -0
  50. package/template/src/feedbax/feedback/index.ts +172 -16
  51. package/template/src/feedbax/feedback/integration-auth.ts +138 -0
  52. package/template/src/feedbax/feedback/notion-comments.ts +2 -35
  53. package/template/src/feedbax/feedback/observed-history.test.ts +150 -0
  54. package/template/src/feedbax/feedback/observed-history.ts +219 -0
  55. package/template/src/feedbax/feedback/ownership.ts +170 -0
  56. package/template/src/history-runtime.test.ts +80 -0
  57. package/template/src/history-runtime.ts +37 -0
  58. package/template/src/index.css +6 -0
  59. package/template/src/integration-runtime.test.ts +365 -0
  60. package/template/src/integration-runtime.ts +289 -0
  61. package/template/src/my-feedback-page.test.tsx +152 -0
  62. package/template/src/my-feedback-page.tsx +398 -0
  63. package/template/src/my-feedback-runtime.test.ts +93 -0
  64. package/template/src/my-feedback-runtime.ts +154 -0
  65. package/template/src/participation-runtime.ts +3 -3
  66. package/template/src/portal-comments.ts +11 -0
  67. package/template/src/portal-feedback-form.tsx +11 -3
  68. package/template/src/portal-feedback-submission.ts +9 -0
  69. package/template/src/portal-server-entry.ts +9 -0
  70. package/template/src/public-portal-error.tsx +1 -1
  71. package/template/src/public-post-detail.tsx +332 -256
  72. package/template/src/public-release-links.test.ts +120 -0
  73. package/template/src/public-release-links.ts +130 -0
  74. package/template/src/routeTree.gen.ts +21 -0
  75. package/template/src/routes/my-feedback.tsx +16 -0
  76. package/template/src/server.ts +46 -1
  77. package/template/src/submission-intent.ts +26 -0
  78. package/template/vite.config.ts +2 -0
package/dist/cli.js CHANGED
@@ -3,15 +3,18 @@
3
3
  // src/cli.ts
4
4
  import * as prompt from "@clack/prompts";
5
5
  import { spawn } from "node:child_process";
6
- import { readFile as readFile3 } from "node:fs/promises";
6
+ import { readFile as readFile4 } from "node:fs/promises";
7
7
  import { basename as basename2, resolve as resolve2 } from "node:path";
8
8
 
9
9
  // src/index.ts
10
- import { createHash, randomBytes } from "node:crypto";
11
- import { readFile as readFile2, readdir as readdir2, writeFile as writeFile2 } from "node:fs/promises";
10
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "node:crypto";
11
+ import { readFile as readFile3, readdir as readdir2, writeFile as writeFile3 } from "node:fs/promises";
12
12
 
13
13
  // ../config/src/index.ts
14
14
  var rootKeys = [
15
+ "history",
16
+ "provider",
17
+ "github",
15
18
  "connected",
16
19
  "product",
17
20
  "theme",
@@ -55,6 +58,8 @@ var changelogPropertyKeys = [
55
58
  function defineFeedbax(config) {
56
59
  const value = object(config, "Feedbax configuration");
57
60
  rejectUnknown(value, rootKeys, "Feedbax configuration");
61
+ if (value.history !== void 0 && typeof value.history !== "boolean")
62
+ throw new Error("history must be true or false.");
58
63
  const product = object(value.product, "product");
59
64
  rejectUnknown(
60
65
  product,
@@ -131,11 +136,19 @@ function defineFeedbax(config) {
131
136
  }
132
137
  const theme = object(value.theme, "theme");
133
138
  rejectUnknown(theme, ["preset"], "theme");
134
- const notion = storage(
135
- value.notion,
136
- feedbackPropertyKeys,
137
- "notion"
138
- );
139
+ if (value.provider !== void 0 && !["notion", "github"].includes(String(value.provider)))
140
+ throw new Error("provider must be notion or github.");
141
+ const githubMode = value.provider === "github";
142
+ if (githubMode && value.notion !== void 0 || !githubMode && value.github !== void 0)
143
+ throw new Error("Select exactly one Content Provider: notion or github.");
144
+ const content = githubMode ? { provider: "github", github: githubConfiguration(value.github) } : {
145
+ ...value.provider ? { provider: "notion" } : {},
146
+ notion: storage(
147
+ value.notion,
148
+ feedbackPropertyKeys,
149
+ "notion"
150
+ )
151
+ };
139
152
  const features = object(value.features, "features");
140
153
  rejectUnknown(features, featureKeys, "features");
141
154
  const validatedFeatures = Object.fromEntries(
@@ -145,6 +158,10 @@ function defineFeedbax(config) {
145
158
  return [key, features[key]];
146
159
  })
147
160
  );
161
+ if (githubMode && (validatedFeatures.changelog || value.changelog !== void 0))
162
+ throw new Error(
163
+ "GitHub Installations must disable Changelog (features.changelog: false) and remove Notion Changelog configuration."
164
+ );
148
165
  const changelog = value.changelog === void 0 ? void 0 : storage(
149
166
  value.changelog,
150
167
  changelogPropertyKeys,
@@ -171,9 +188,10 @@ function defineFeedbax(config) {
171
188
  throw new Error("Turnstile is enabled but turnstile.siteKey is missing.");
172
189
  return {
173
190
  product: validatedProduct,
191
+ ...value.history !== void 0 ? { history: value.history } : {},
174
192
  ...connected ? { connected } : {},
175
193
  theme: { preset: text(theme.preset, "theme.preset") },
176
- notion,
194
+ ...content,
177
195
  ...changelog ? { changelog } : {},
178
196
  features: validatedFeatures,
179
197
  submissions: {
@@ -238,9 +256,44 @@ function publicUrl(value, label, allowRelative) {
238
256
  `${label} must be ${allowRelative ? "root-relative or " : ""}an HTTPS URL.`
239
257
  );
240
258
  }
259
+ function githubConfiguration(value) {
260
+ const input = object(value, "github");
261
+ rejectUnknown(
262
+ input,
263
+ ["installationId", "serviceUrl", "repository", "botUserId"],
264
+ "github"
265
+ );
266
+ const installationId = text(input.installationId, "github.installationId");
267
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(installationId))
268
+ throw new Error(
269
+ "github.installationId must be a stable Installation identifier."
270
+ );
271
+ const serviceUrl = publicUrl(input.serviceUrl, "github.serviceUrl", false);
272
+ const url = new URL(serviceUrl);
273
+ if (url.origin !== serviceUrl || url.username || url.password)
274
+ throw new Error(
275
+ "github.serviceUrl must be an HTTPS origin without a path or credentials."
276
+ );
277
+ const repository = object(input.repository, "github.repository");
278
+ rejectUnknown(repository, ["id", "owner", "name"], "github.repository");
279
+ const owner = text(repository.owner, "github.repository.owner");
280
+ const name = text(repository.name, "github.repository.name");
281
+ if (!/^[A-Za-z0-9-]+$/.test(owner) || !/^[A-Za-z0-9_.-]+$/.test(name))
282
+ throw new Error("Use a valid GitHub repository owner and name.");
283
+ return {
284
+ installationId,
285
+ serviceUrl,
286
+ repository: {
287
+ id: positiveInteger(repository.id, "github.repository.id"),
288
+ owner,
289
+ name
290
+ },
291
+ botUserId: positiveInteger(input.botUserId, "github.botUserId")
292
+ };
293
+ }
241
294
 
242
295
  // src/index.ts
243
- import { basename, join as join2 } from "node:path";
296
+ import { basename, join as join3 } from "node:path";
244
297
 
245
298
  // src/template.ts
246
299
  import { parse } from "jsonc-parser";
@@ -690,7 +743,7 @@ function isPropertyMap(value) {
690
743
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
691
744
  }
692
745
 
693
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/entity.js
746
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/entity.js
694
747
  var entityKind = /* @__PURE__ */ Symbol.for("drizzle:entityKind");
695
748
  function is(value, type) {
696
749
  if (!value || typeof value !== "object") {
@@ -716,7 +769,7 @@ function is(value, type) {
716
769
  return false;
717
770
  }
718
771
 
719
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/column.js
772
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/column.js
720
773
  var Column = class {
721
774
  constructor(table, config) {
722
775
  this.table = table;
@@ -767,7 +820,7 @@ var Column = class {
767
820
  }
768
821
  };
769
822
 
770
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/column-builder.js
823
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/column-builder.js
771
824
  var ColumnBuilder = class {
772
825
  static [entityKind] = "ColumnBuilder";
773
826
  config;
@@ -870,10 +923,10 @@ var ColumnBuilder = class {
870
923
  }
871
924
  };
872
925
 
873
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/table.utils.js
926
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/table.utils.js
874
927
  var TableName = /* @__PURE__ */ Symbol.for("drizzle:Name");
875
928
 
876
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/pg-core/foreign-keys.js
929
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/pg-core/foreign-keys.js
877
930
  var ForeignKeyBuilder = class {
878
931
  static [entityKind] = "PgForeignKeyBuilder";
879
932
  /** @internal */
@@ -930,12 +983,12 @@ var ForeignKey = class {
930
983
  }
931
984
  };
932
985
 
933
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/tracing-utils.js
986
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/tracing-utils.js
934
987
  function iife(fn, ...args) {
935
988
  return fn(...args);
936
989
  }
937
990
 
938
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/pg-core/unique-constraint.js
991
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/pg-core/unique-constraint.js
939
992
  function uniqueKeyName(table, columns) {
940
993
  return `${table[TableName]}_${columns.join("_")}_unique`;
941
994
  }
@@ -985,7 +1038,7 @@ var UniqueConstraint = class {
985
1038
  }
986
1039
  };
987
1040
 
988
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/pg-core/utils/array.js
1041
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/pg-core/utils/array.js
989
1042
  function parsePgArrayValue(arrayString, startFrom, inQuotes) {
990
1043
  for (let i = startFrom; i < arrayString.length; i++) {
991
1044
  const char = arrayString[i];
@@ -1061,7 +1114,7 @@ function makePgArray(array) {
1061
1114
  }).join(",")}}`;
1062
1115
  }
1063
1116
 
1064
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/pg-core/columns/common.js
1117
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/pg-core/columns/common.js
1065
1118
  var PgColumnBuilder = class extends ColumnBuilder {
1066
1119
  foreignKeyConfigs = [];
1067
1120
  static [entityKind] = "PgColumnBuilder";
@@ -1245,7 +1298,7 @@ var PgArray = class _PgArray extends PgColumn {
1245
1298
  }
1246
1299
  };
1247
1300
 
1248
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/pg-core/columns/enum.js
1301
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/pg-core/columns/enum.js
1249
1302
  var PgEnumObjectColumnBuilder = class extends PgColumnBuilder {
1250
1303
  static [entityKind] = "PgEnumObjectColumnBuilder";
1251
1304
  constructor(name, enumInstance) {
@@ -1303,7 +1356,7 @@ var PgEnumColumn = class extends PgColumn {
1303
1356
  }
1304
1357
  };
1305
1358
 
1306
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/subquery.js
1359
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/subquery.js
1307
1360
  var Subquery = class {
1308
1361
  static [entityKind] = "Subquery";
1309
1362
  constructor(sql2, fields, alias, isWith = false, usedTables = []) {
@@ -1324,10 +1377,10 @@ var WithSubquery = class extends Subquery {
1324
1377
  static [entityKind] = "WithSubquery";
1325
1378
  };
1326
1379
 
1327
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/version.js
1328
- var version = "0.45.2";
1380
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/version.js
1381
+ var version = "0.45.4";
1329
1382
 
1330
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/tracing.js
1383
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/tracing.js
1331
1384
  var otel;
1332
1385
  var rawTracer;
1333
1386
  var tracer = {
@@ -1362,10 +1415,10 @@ var tracer = {
1362
1415
  }
1363
1416
  };
1364
1417
 
1365
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/view-common.js
1418
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/view-common.js
1366
1419
  var ViewBaseConfig = /* @__PURE__ */ Symbol.for("drizzle:ViewBaseConfig");
1367
1420
 
1368
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/table.js
1421
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/table.js
1369
1422
  var Schema = /* @__PURE__ */ Symbol.for("drizzle:Schema");
1370
1423
  var Columns = /* @__PURE__ */ Symbol.for("drizzle:Columns");
1371
1424
  var ExtraConfigColumns = /* @__PURE__ */ Symbol.for("drizzle:ExtraConfigColumns");
@@ -1421,7 +1474,7 @@ var Table = class {
1421
1474
  }
1422
1475
  };
1423
1476
 
1424
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sql/sql.js
1477
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sql/sql.js
1425
1478
  var FakePrimitiveParam = class {
1426
1479
  static [entityKind] = "FakePrimitiveParam";
1427
1480
  };
@@ -1711,7 +1764,7 @@ function sql(strings, ...params) {
1711
1764
  return new SQL([new StringChunk(str)]);
1712
1765
  }
1713
1766
  sql2.raw = raw;
1714
- function join3(chunks, separator) {
1767
+ function join4(chunks, separator) {
1715
1768
  const result = [];
1716
1769
  for (const [i, chunk] of chunks.entries()) {
1717
1770
  if (i > 0 && separator !== void 0) {
@@ -1721,7 +1774,7 @@ function sql(strings, ...params) {
1721
1774
  }
1722
1775
  return new SQL(result);
1723
1776
  }
1724
- sql2.join = join3;
1777
+ sql2.join = join4;
1725
1778
  function identifier(value) {
1726
1779
  return new Name(value);
1727
1780
  }
@@ -1795,7 +1848,7 @@ Subquery.prototype.getSQL = function() {
1795
1848
  return new SQL([this]);
1796
1849
  };
1797
1850
 
1798
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/utils.js
1851
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/utils.js
1799
1852
  function getColumnNameAndConfig(a, b) {
1800
1853
  return {
1801
1854
  name: typeof a === "string" && a.length > 0 ? a : "",
@@ -1804,7 +1857,7 @@ function getColumnNameAndConfig(a, b) {
1804
1857
  }
1805
1858
  var textDecoder = typeof TextDecoder === "undefined" ? null : new TextDecoder();
1806
1859
 
1807
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/foreign-keys.js
1860
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/foreign-keys.js
1808
1861
  var ForeignKeyBuilder2 = class {
1809
1862
  static [entityKind] = "SQLiteForeignKeyBuilder";
1810
1863
  /** @internal */
@@ -1861,7 +1914,7 @@ var ForeignKey2 = class {
1861
1914
  }
1862
1915
  };
1863
1916
 
1864
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/unique-constraint.js
1917
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/unique-constraint.js
1865
1918
  function uniqueKeyName2(table, columns) {
1866
1919
  return `${table[TableName]}_${columns.join("_")}_unique`;
1867
1920
  }
@@ -1903,7 +1956,7 @@ var UniqueConstraint2 = class {
1903
1956
  }
1904
1957
  };
1905
1958
 
1906
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/common.js
1959
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/common.js
1907
1960
  var SQLiteColumnBuilder = class extends ColumnBuilder {
1908
1961
  static [entityKind] = "SQLiteColumnBuilder";
1909
1962
  foreignKeyConfigs = [];
@@ -1954,7 +2007,7 @@ var SQLiteColumn = class extends Column {
1954
2007
  static [entityKind] = "SQLiteColumn";
1955
2008
  };
1956
2009
 
1957
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/blob.js
2010
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/blob.js
1958
2011
  var SQLiteBigIntBuilder = class extends SQLiteColumnBuilder {
1959
2012
  static [entityKind] = "SQLiteBigIntBuilder";
1960
2013
  constructor(name) {
@@ -2043,7 +2096,7 @@ function blob(a, b) {
2043
2096
  return new SQLiteBlobBufferBuilder(name);
2044
2097
  }
2045
2098
 
2046
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/custom.js
2099
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/custom.js
2047
2100
  var SQLiteCustomColumnBuilder = class extends SQLiteColumnBuilder {
2048
2101
  static [entityKind] = "SQLiteCustomColumnBuilder";
2049
2102
  constructor(name, fieldConfig, customTypeParams) {
@@ -2091,7 +2144,7 @@ function customType(customTypeParams) {
2091
2144
  };
2092
2145
  }
2093
2146
 
2094
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/integer.js
2147
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/integer.js
2095
2148
  var SQLiteBaseIntegerBuilder = class extends SQLiteColumnBuilder {
2096
2149
  static [entityKind] = "SQLiteBaseIntegerBuilder";
2097
2150
  constructor(name, dataType, columnType) {
@@ -2200,7 +2253,7 @@ function integer(a, b) {
2200
2253
  return new SQLiteIntegerBuilder(name);
2201
2254
  }
2202
2255
 
2203
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/numeric.js
2256
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/numeric.js
2204
2257
  var SQLiteNumericBuilder = class extends SQLiteColumnBuilder {
2205
2258
  static [entityKind] = "SQLiteNumericBuilder";
2206
2259
  constructor(name) {
@@ -2275,7 +2328,7 @@ function numeric(a, b) {
2275
2328
  return mode === "number" ? new SQLiteNumericNumberBuilder(name) : mode === "bigint" ? new SQLiteNumericBigIntBuilder(name) : new SQLiteNumericBuilder(name);
2276
2329
  }
2277
2330
 
2278
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/real.js
2331
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/real.js
2279
2332
  var SQLiteRealBuilder = class extends SQLiteColumnBuilder {
2280
2333
  static [entityKind] = "SQLiteRealBuilder";
2281
2334
  constructor(name) {
@@ -2296,7 +2349,7 @@ function real(name) {
2296
2349
  return new SQLiteRealBuilder(name ?? "");
2297
2350
  }
2298
2351
 
2299
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/text.js
2352
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/text.js
2300
2353
  var SQLiteTextBuilder = class extends SQLiteColumnBuilder {
2301
2354
  static [entityKind] = "SQLiteTextBuilder";
2302
2355
  constructor(name, config) {
@@ -2356,7 +2409,7 @@ function text2(a, b = {}) {
2356
2409
  return new SQLiteTextBuilder(name, config);
2357
2410
  }
2358
2411
 
2359
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/all.js
2412
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/columns/all.js
2360
2413
  function getSQLiteColumnBuilders() {
2361
2414
  return {
2362
2415
  blob,
@@ -2368,7 +2421,7 @@ function getSQLiteColumnBuilders() {
2368
2421
  };
2369
2422
  }
2370
2423
 
2371
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/table.js
2424
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/table.js
2372
2425
  var InlineForeignKeys = /* @__PURE__ */ Symbol.for("drizzle:SQLiteInlineForeignKeys");
2373
2426
  var SQLiteTable = class extends Table {
2374
2427
  static [entityKind] = "SQLiteTable";
@@ -2407,7 +2460,7 @@ var sqliteTable = (name, columns, extraConfig) => {
2407
2460
  return sqliteTableBase(name, columns, extraConfig);
2408
2461
  };
2409
2462
 
2410
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20261014.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/primary-keys.js
2463
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.4_@cloudflare+workers-types@5.20261015.1_kysely@0.29.6/node_modules/drizzle-orm/sqlite-core/primary-keys.js
2411
2464
  function primaryKey(...config) {
2412
2465
  if (config[0].columns) {
2413
2466
  return new PrimaryKeyBuilder(config[0].columns, config[0].name);
@@ -2555,7 +2608,7 @@ async function validateNotionChangelogDataSource({
2555
2608
  const properties = recordAt(dataSource, "properties");
2556
2609
  const propertiesById = new Map(
2557
2610
  Object.values(properties).flatMap((property) => {
2558
- if (!isRecord(property) || typeof property.id !== "string") return [];
2611
+ if (!isRecord2(property) || typeof property.id !== "string") return [];
2559
2612
  return [[property.id, property]];
2560
2613
  })
2561
2614
  );
@@ -2610,7 +2663,7 @@ async function readObject(response, subject) {
2610
2663
  `Notion rejected the ${subject} request (${response.status}).`
2611
2664
  );
2612
2665
  }
2613
- if (!isRecord(body))
2666
+ if (!isRecord2(body))
2614
2667
  throw new Error(`Notion returned an invalid ${subject} response.`);
2615
2668
  return body;
2616
2669
  }
@@ -2618,7 +2671,7 @@ function recordAt(value, key) {
2618
2671
  return record(value[key]);
2619
2672
  }
2620
2673
  function record(value) {
2621
- if (!isRecord(value))
2674
+ if (!isRecord2(value))
2622
2675
  throw new Error("Notion returned an invalid Changelog response.");
2623
2676
  return value;
2624
2677
  }
@@ -2627,13 +2680,141 @@ function requiredString(value) {
2627
2680
  throw new Error("Notion returned an invalid Changelog response.");
2628
2681
  return value;
2629
2682
  }
2630
- function isRecord(value) {
2683
+ function isRecord2(value) {
2631
2684
  return typeof value === "object" && value !== null && !Array.isArray(value);
2632
2685
  }
2633
2686
 
2687
+ // src/github.ts
2688
+ import { createHash, randomBytes } from "node:crypto";
2689
+ import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2690
+ import { join as join2 } from "node:path";
2691
+ async function provisionGitHubInstallation(options) {
2692
+ const { connected, github } = options;
2693
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(options.workerName) || !/^[0-9a-f-]{36}$/i.test(options.database.id) || !options.database.name.trim())
2694
+ throw new Error(
2695
+ "GitHub setup requires a distinct Worker and an explicitly provisioned FEEDBAX_DB D1 database."
2696
+ );
2697
+ if (connected && (connected.databaseId !== options.database.id || connected.handoffSecret.length < 32 || connected.processing !== "standard" && !connected.queueName))
2698
+ throw new Error(
2699
+ "Connected setup requires the same D1 database, a handoff secret, and an existing processing queue for small processing."
2700
+ );
2701
+ if (connected?.imageBucket && !/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/.test(connected.imageBucket))
2702
+ throw new Error("Use a dedicated private R2 bucket for images.");
2703
+ const config = defineFeedbax({
2704
+ provider: "github",
2705
+ github,
2706
+ product: { ...options.product, logo: "/feedbax-mark.svg" },
2707
+ theme: { preset: "lyra-neutral" },
2708
+ features: options.features,
2709
+ ...connected ? {
2710
+ connected: {
2711
+ processing: connected.processing ?? "small",
2712
+ issuer: connected.issuer,
2713
+ loginUrl: connected.loginUrl,
2714
+ ...connected.imageBucket ? { images: true } : {},
2715
+ ...connected.email ? {
2716
+ email: {
2717
+ provider: connected.email.provider,
2718
+ from: connected.email.from,
2719
+ ...connected.email.endpoint ? { endpoint: connected.email.endpoint } : {}
2720
+ }
2721
+ } : {}
2722
+ }
2723
+ } : {},
2724
+ submissions: {
2725
+ moderation: "required",
2726
+ maxTitleLength: 160,
2727
+ maxDescriptionLength: 5e3
2728
+ },
2729
+ turnstile: {
2730
+ enabled: Boolean(options.turnstile?.siteKey),
2731
+ ...options.turnstile?.siteKey ? { siteKey: options.turnstile.siteKey } : {}
2732
+ }
2733
+ });
2734
+ if (!await options.preview(
2735
+ "Create GitHub-backed portal source and private pairing secrets. GitHub requires D1 independently of sign-in. If the repository is public, issue titles/bodies are public immediately even while portal Posts await moderation. This does not create issues, pair the App, or deploy."
2736
+ ))
2737
+ throw new Error("GitHub setup was not approved.");
2738
+ await copyPackagedTemplate(options.destination);
2739
+ const path = join2(options.destination, "wrangler.jsonc");
2740
+ const base = JSON.parse(await readFile2(path, "utf8"));
2741
+ const wrangler = {
2742
+ ...base,
2743
+ name: options.workerName,
2744
+ compatibility_flags: [
2745
+ .../* @__PURE__ */ new Set([
2746
+ ...base.compatibility_flags ?? [],
2747
+ "global_fetch_strictly_public"
2748
+ ])
2749
+ ],
2750
+ d1_databases: [
2751
+ {
2752
+ binding: "FEEDBAX_DB",
2753
+ database_id: options.database.id,
2754
+ database_name: options.database.name,
2755
+ migrations_dir: "migrations"
2756
+ }
2757
+ ],
2758
+ triggers: { crons: ["*/5 * * * *"] },
2759
+ ...connected?.queueName ? {
2760
+ queues: {
2761
+ producers: [
2762
+ { binding: "FEEDBAX_UPDATES", queue: connected.queueName }
2763
+ ],
2764
+ consumers: [
2765
+ {
2766
+ queue: connected.queueName,
2767
+ max_batch_size: 1,
2768
+ max_concurrency: 1,
2769
+ max_retries: 3,
2770
+ retry_delay: 60
2771
+ }
2772
+ ]
2773
+ }
2774
+ } : {},
2775
+ ...connected?.imageBucket ? {
2776
+ r2_buckets: [
2777
+ { binding: "FEEDBAX_IMAGES", bucket_name: connected.imageBucket }
2778
+ ]
2779
+ } : {},
2780
+ ...connected?.email?.provider === "cloudflare" ? {
2781
+ send_email: [
2782
+ {
2783
+ name: "FEEDBAX_EMAIL",
2784
+ allowed_sender_addresses: [connected.email.from]
2785
+ }
2786
+ ]
2787
+ } : {}
2788
+ };
2789
+ const apiKey = randomBytes(32).toString("base64url");
2790
+ const pairingSecret = randomBytes(32).toString("base64url");
2791
+ const secrets = {
2792
+ FEEDBAX_API_KEY_HASH: createHash("sha256").update(apiKey).digest("base64url"),
2793
+ PARTICIPATION_SIGNING_SECRET: randomBytes(32).toString("base64url"),
2794
+ GITHUB_PAIRING_SECRET: pairingSecret,
2795
+ ...connected ? { IDENTITY_HANDOFF_SECRET: connected.handoffSecret } : {},
2796
+ ...connected?.email?.secret ? { EMAIL_PROVIDER_SECRET: connected.email.secret } : {},
2797
+ ...options.turnstile?.secretKey ? { TURNSTILE_SECRET_KEY: options.turnstile.secretKey } : {}
2798
+ };
2799
+ await writeFile2(path, JSON.stringify(wrangler, null, 2) + "\n");
2800
+ await writeFile2(
2801
+ join2(options.destination, "src/feedbax.ts"),
2802
+ `import { defineFeedbax } from "@feedbax/config";
2803
+
2804
+ export default defineFeedbax(${JSON.stringify(config, null, 2)});
2805
+ `
2806
+ );
2807
+ await writeFile2(
2808
+ join2(options.destination, ".dev.vars"),
2809
+ Object.entries(secrets).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join("\n") + "\n",
2810
+ { mode: 384 }
2811
+ );
2812
+ return { apiKey, pairingSecret, configuration: config };
2813
+ }
2814
+
2634
2815
  // src/index.ts
2635
2816
  function defaultProcessingQueueName(workerName) {
2636
- return workerName.length <= 55 ? `${workerName}-updates` : `${workerName.slice(0, 46)}-${createHash("sha256").update(workerName).digest("hex").slice(0, 8)}-updates`;
2817
+ return workerName.length <= 55 ? `${workerName}-updates` : `${workerName.slice(0, 46)}-${createHash2("sha256").update(workerName).digest("hex").slice(0, 8)}-updates`;
2637
2818
  }
2638
2819
  async function provisionGuidedInstallation({
2639
2820
  destination,
@@ -2741,9 +2922,9 @@ async function provisionGuidedInstallation({
2741
2922
  preview
2742
2923
  });
2743
2924
  await copyPackagedTemplate(destination);
2744
- const wranglerPath = join2(destination, "wrangler.jsonc");
2745
- const wrangler = JSON.parse(await readFile2(wranglerPath, "utf8"));
2746
- await writeFile2(
2925
+ const wranglerPath = join3(destination, "wrangler.jsonc");
2926
+ const wrangler = JSON.parse(await readFile3(wranglerPath, "utf8"));
2927
+ await writeFile3(
2747
2928
  wranglerPath,
2748
2929
  JSON.stringify(
2749
2930
  {
@@ -2798,14 +2979,14 @@ async function provisionGuidedInstallation({
2798
2979
  2
2799
2980
  ) + "\n"
2800
2981
  );
2801
- const apiKey = randomBytes(32).toString("base64url");
2802
- const signingSecret = randomBytes(32).toString("base64url");
2982
+ const apiKey = randomBytes2(32).toString("base64url");
2983
+ const signingSecret = randomBytes2(32).toString("base64url");
2803
2984
  const values = {
2804
2985
  NOTION_TOKEN: notionToken,
2805
2986
  NOTION_FEEDBACK_DATABASE_ID: feedback.databaseId,
2806
2987
  NOTION_FEEDBACK_DATA_SOURCE_ID: feedback.dataSourceId,
2807
2988
  ...prefixProperties("NOTION_FEEDBACK", feedback.propertyIds),
2808
- FEEDBAX_API_KEY_HASH: createHash("sha256").update(apiKey).digest("base64url"),
2989
+ FEEDBAX_API_KEY_HASH: createHash2("sha256").update(apiKey).digest("base64url"),
2809
2990
  PARTICIPATION_SIGNING_SECRET: signingSecret,
2810
2991
  ...connected ? { IDENTITY_HANDOFF_SECRET: connected.handoffSecret } : {},
2811
2992
  ...connected?.email?.secret ? { EMAIL_PROVIDER_SECRET: connected.email.secret } : {},
@@ -2820,8 +3001,8 @@ async function provisionGuidedInstallation({
2820
3001
  ...prefixProperties("NOTION_CHANGELOG", changelog.propertyIds)
2821
3002
  } : {}
2822
3003
  };
2823
- await writeFile2(
2824
- join2(destination, ".dev.vars"),
3004
+ await writeFile3(
3005
+ join3(destination, ".dev.vars"),
2825
3006
  Object.entries(values).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join("\n") + "\n",
2826
3007
  { mode: 384 }
2827
3008
  );
@@ -2866,16 +3047,16 @@ async function provisionGuidedInstallation({
2866
3047
  ...turnstile.siteKey ? { siteKey: turnstile.siteKey } : {}
2867
3048
  }
2868
3049
  });
2869
- await writeFile2(
2870
- join2(destination, "src/feedbax.ts"),
3050
+ await writeFile3(
3051
+ join3(destination, "src/feedbax.ts"),
2871
3052
  `import { defineFeedbax } from "@feedbax/config";
2872
3053
 
2873
3054
  export default defineFeedbax(${JSON.stringify(configuration, null, 2)});
2874
3055
  `
2875
3056
  );
2876
3057
  const probePage = commentProbePageId ?? (feedbackSelection.kind === "create" ? feedbackSelection.parentPageId : feedback.databaseId);
2877
- await writeFile2(
2878
- join2(destination, ".dev.vars"),
3058
+ await writeFile3(
3059
+ join3(destination, ".dev.vars"),
2879
3060
  `NOTION_COMMENT_CAPABILITY_PROBE_PAGE_ID=${JSON.stringify(probePage)}
2880
3061
  `,
2881
3062
  { flag: "a", mode: 384 }
@@ -3019,12 +3200,12 @@ try {
3019
3200
  const args = process.argv.slice(2);
3020
3201
  if (args.includes("--help") || args.includes("-h")) {
3021
3202
  console.log(
3022
- "Usage: create-feedbax [directory] [--template-only]\nGuided setup prompts for a destination, product, Notion connection, features, and deployment.\n--template-only copies source without external requests or installing dependencies."
3203
+ "Usage: create-feedbax [directory] [--template-only]\nGuided setup prompts for a destination, product, Notion or GitHub connection, features, and deployment.\n--template-only copies source without external requests or installing dependencies."
3023
3204
  );
3024
3205
  } else if (args.includes("--version")) {
3025
3206
  console.log(
3026
3207
  JSON.parse(
3027
- await readFile3(new URL("../package.json", import.meta.url), "utf8")
3208
+ await readFile4(new URL("../package.json", import.meta.url), "utf8")
3028
3209
  ).version
3029
3210
  );
3030
3211
  } else {
@@ -3053,163 +3234,181 @@ try {
3053
3234
  "Cloudflare Worker name (choose a distinct name for this product)",
3054
3235
  basename2(destination).toLowerCase().replace(/[^a-z0-9-]/g, "-")
3055
3236
  );
3056
- const connected = await confirm2(
3057
- "Enable Connected Profile using your application's sign-in, dedicated D1, and a processing queue? Small processing supports Workers Free with Resend/custom email.",
3058
- false
3059
- ) ? {
3060
- issuer: await text4("Application identity issuer (HTTPS URL)"),
3061
- loginUrl: await text4(
3062
- "Application signed-handoff login URL (HTTPS)"
3063
- ),
3064
- databaseId: await text4("Dedicated Cloudflare D1 database ID"),
3065
- databaseName: await text4("Dedicated Cloudflare D1 database name"),
3066
- queueName: await text4(
3067
- "Dedicated Cloudflare Queue name (create it with wrangler queues create first)",
3068
- defaultProcessingQueueName(workerName)
3069
- ),
3070
- handoffSecret: await secret(
3071
- "Shared server-only identity handoff secret (at least 32 characters)"
3072
- )
3073
- } : void 0;
3074
- let email;
3075
- let imageBucket;
3076
- if (connected) {
3077
- const provider = await text4(
3078
- "Email sender: cloudflare, resend, custom, or none",
3079
- "none"
3080
- );
3081
- if (provider !== "none") {
3082
- if (provider !== "cloudflare" && provider !== "resend" && provider !== "custom")
3083
- throw new Error(
3084
- "Email sender must be cloudflare, resend, custom, or none."
3237
+ const provider = checked(
3238
+ await prompt.select({
3239
+ message: "Content Provider",
3240
+ options: [
3241
+ { value: "notion", label: "Notion" },
3242
+ { value: "github", label: "GitHub" }
3243
+ ]
3244
+ })
3245
+ );
3246
+ if (provider === "github") {
3247
+ await githubSetup(destination, product, workerName);
3248
+ } else {
3249
+ const connected = await confirm2(
3250
+ "Enable Connected Profile using your application's sign-in, dedicated D1, and a processing queue? Small processing supports Workers Free with Resend/custom email.",
3251
+ false
3252
+ ) ? {
3253
+ issuer: await text4("Application identity issuer (HTTPS URL)"),
3254
+ loginUrl: await text4(
3255
+ "Application signed-handoff login URL (HTTPS)"
3256
+ ),
3257
+ databaseId: await text4("Dedicated Cloudflare D1 database ID"),
3258
+ databaseName: await text4("Dedicated Cloudflare D1 database name"),
3259
+ queueName: await text4(
3260
+ "Dedicated Cloudflare Queue name (create it with wrangler queues create first)",
3261
+ defaultProcessingQueueName(workerName)
3262
+ ),
3263
+ handoffSecret: await secret(
3264
+ "Shared server-only identity handoff secret (at least 32 characters)"
3265
+ )
3266
+ } : void 0;
3267
+ let email;
3268
+ let imageBucket;
3269
+ if (connected) {
3270
+ const provider2 = await text4(
3271
+ "Email sender: cloudflare, resend, custom, or none",
3272
+ "none"
3273
+ );
3274
+ if (provider2 !== "none") {
3275
+ if (provider2 !== "cloudflare" && provider2 !== "resend" && provider2 !== "custom")
3276
+ throw new Error(
3277
+ "Email sender must be cloudflare, resend, custom, or none."
3278
+ );
3279
+ email = {
3280
+ provider: provider2,
3281
+ from: await text4("Verified sender email address"),
3282
+ ...provider2 === "custom" ? { endpoint: await text4("Custom HTTPS delivery endpoint") } : {},
3283
+ ...provider2 !== "cloudflare" ? { secret: await secret("Server-only email provider secret") } : {}
3284
+ };
3285
+ }
3286
+ if (await confirm2(
3287
+ "Enable Participant image attachments using an existing private R2 bucket?",
3288
+ false
3289
+ ))
3290
+ imageBucket = await text4(
3291
+ "Dedicated private R2 bucket name (create it in Cloudflare first)"
3085
3292
  );
3086
- email = {
3087
- provider,
3088
- from: await text4("Verified sender email address"),
3089
- ...provider === "custom" ? { endpoint: await text4("Custom HTTPS delivery endpoint") } : {},
3090
- ...provider !== "cloudflare" ? { secret: await secret("Server-only email provider secret") } : {}
3091
- };
3092
3293
  }
3093
- if (await confirm2(
3094
- "Enable Participant image attachments using an existing private R2 bucket?",
3294
+ const themePreset = await text4(
3295
+ "shadcn preset code, or lyra-neutral for the included preset",
3296
+ "lyra-neutral"
3297
+ );
3298
+ const notionToken = process.env.NOTION_TOKEN?.trim() || await secret("Notion internal integration token");
3299
+ const existing = await confirm2(
3300
+ "Use an existing compatible Feedback Data Source?",
3095
3301
  false
3096
- ))
3097
- imageBucket = await text4(
3098
- "Dedicated private R2 bucket name (create it in Cloudflare first)"
3099
- );
3100
- }
3101
- const themePreset = await text4(
3102
- "shadcn preset code, or lyra-neutral for the included preset",
3103
- "lyra-neutral"
3104
- );
3105
- const notionToken = process.env.NOTION_TOKEN?.trim() || await secret("Notion internal integration token");
3106
- const existing = await confirm2(
3107
- "Use an existing compatible Feedback Data Source?",
3108
- false
3109
- );
3110
- const feedbackSelection = existing ? { kind: "existing", configuration: await existingFeedback() } : {
3111
- kind: "create",
3112
- parentPageId: await text4(
3113
- "Notion parent page ID (share it with the integration)"
3114
- )
3115
- };
3116
- const features = await collectFeatureSelection({
3117
- confirm: ({ message, initialValue }) => confirm2(message, initialValue)
3118
- });
3119
- const commentProbePageId = existing && features.comments ? await text4("Shared Notion page ID for checking Comment permissions") : void 0;
3120
- const changelogSelection = features.changelog && await confirm2(
3121
- "Use an existing compatible Changelog Data Source?",
3122
- false
3123
- ) ? {
3124
- kind: "existing",
3125
- dataSourceId: await text4("Changelog Data Source ID"),
3126
- propertyIds: await propertyIds([
3127
- "title",
3128
- "slug",
3129
- "date",
3130
- "summary",
3131
- "body",
3132
- "labels",
3133
- "image",
3134
- "published",
3135
- "createdAt",
3136
- "updatedAt"
3137
- ])
3138
- } : void 0;
3139
- const turnstile = await confirm2(
3140
- "Enable Cloudflare Turnstile? Recommended for production.",
3141
- true
3142
- ) ? {
3143
- siteKey: await text4("Turnstile site key"),
3144
- secretKey: process.env.TURNSTILE_SECRET_KEY || await secret("Turnstile secret key")
3145
- } : {};
3146
- const result = await provisionGuidedInstallation({
3147
- destination,
3148
- product,
3149
- themePreset,
3150
- workerName,
3151
- commentProbePageId,
3152
- connected: connected ? {
3153
- ...connected,
3154
- ...email ? { email } : {},
3155
- ...imageBucket ? { imageBucket } : {}
3156
- } : void 0,
3157
- features,
3158
- feedbackSelection,
3159
- changelogSelection,
3160
- notionToken,
3161
- turnstile,
3162
- preview: (message) => confirm2(message, false)
3163
- });
3164
- console.log(
3165
- `Created ${destination}.
3302
+ );
3303
+ const feedbackSelection = existing ? {
3304
+ kind: "existing",
3305
+ configuration: await existingFeedback()
3306
+ } : {
3307
+ kind: "create",
3308
+ parentPageId: await text4(
3309
+ "Notion parent page ID (share it with the integration)"
3310
+ )
3311
+ };
3312
+ const features = await collectFeatureSelection({
3313
+ confirm: ({ message, initialValue }) => confirm2(message, initialValue)
3314
+ });
3315
+ const commentProbePageId = existing && features.comments ? await text4(
3316
+ "Shared Notion page ID for checking Comment permissions"
3317
+ ) : void 0;
3318
+ const changelogSelection = features.changelog && await confirm2(
3319
+ "Use an existing compatible Changelog Data Source?",
3320
+ false
3321
+ ) ? {
3322
+ kind: "existing",
3323
+ dataSourceId: await text4("Changelog Data Source ID"),
3324
+ propertyIds: await propertyIds([
3325
+ "title",
3326
+ "slug",
3327
+ "date",
3328
+ "summary",
3329
+ "body",
3330
+ "labels",
3331
+ "image",
3332
+ "published",
3333
+ "createdAt",
3334
+ "updatedAt"
3335
+ ])
3336
+ } : void 0;
3337
+ const turnstile = await confirm2(
3338
+ "Enable Cloudflare Turnstile? Recommended for production.",
3339
+ true
3340
+ ) ? {
3341
+ siteKey: await text4("Turnstile site key"),
3342
+ secretKey: process.env.TURNSTILE_SECRET_KEY || await secret("Turnstile secret key")
3343
+ } : {};
3344
+ const result = await provisionGuidedInstallation({
3345
+ destination,
3346
+ product,
3347
+ themePreset,
3348
+ workerName,
3349
+ commentProbePageId,
3350
+ connected: connected ? {
3351
+ ...connected,
3352
+ ...email ? { email } : {},
3353
+ ...imageBucket ? { imageBucket } : {}
3354
+ } : void 0,
3355
+ features,
3356
+ feedbackSelection,
3357
+ changelogSelection,
3358
+ notionToken,
3359
+ turnstile,
3360
+ preview: (message) => confirm2(message, false)
3361
+ });
3362
+ console.log(
3363
+ `Created ${destination}.
3166
3364
  Trusted API key \u2014 copy it now; plaintext is not stored:
3167
3365
  ${result.apiKey}`
3168
- );
3169
- await runIn(destination, "pnpm", ["install"]);
3170
- if (themePreset !== "lyra-neutral")
3171
- await runIn(destination, "pnpm", [
3172
- "dlx",
3173
- "shadcn@latest",
3174
- "init",
3175
- "--preset",
3176
- themePreset,
3177
- "--yes",
3178
- "--force"
3179
- ]);
3180
- await runIn(destination, "pnpm", [
3181
- "exec",
3182
- "prettier",
3183
- "--write",
3184
- "src/feedbax.ts"
3185
- ]);
3186
- if (connected) {
3187
- if (!await confirm2(
3188
- `Apply the included Connected schema migration to D1 ${connected.databaseName} (${connected.databaseId}) in your authenticated Cloudflare account?`,
3189
- false
3190
- ))
3191
- throw new Error(
3192
- "Connected source is preserved. Apply the D1 migration described in CONNECTED.md before running doctor or deploying."
3193
- );
3366
+ );
3367
+ await runIn(destination, "pnpm", ["install"]);
3368
+ if (themePreset !== "lyra-neutral")
3369
+ await runIn(destination, "pnpm", [
3370
+ "dlx",
3371
+ "shadcn@latest",
3372
+ "init",
3373
+ "--preset",
3374
+ themePreset,
3375
+ "--yes",
3376
+ "--force"
3377
+ ]);
3194
3378
  await runIn(destination, "pnpm", [
3195
3379
  "exec",
3196
- "wrangler",
3197
- "d1",
3198
- "migrations",
3199
- "apply",
3200
- "FEEDBAX_DB",
3201
- "--remote"
3380
+ "prettier",
3381
+ "--write",
3382
+ "src/feedbax.ts"
3202
3383
  ]);
3384
+ if (connected) {
3385
+ if (!await confirm2(
3386
+ `Apply the included Connected schema migration to D1 ${connected.databaseName} (${connected.databaseId}) in your authenticated Cloudflare account?`,
3387
+ false
3388
+ ))
3389
+ throw new Error(
3390
+ "Connected source is preserved. Apply the D1 migration described in CONNECTED.md before running doctor or deploying."
3391
+ );
3392
+ await runIn(destination, "pnpm", [
3393
+ "exec",
3394
+ "wrangler",
3395
+ "d1",
3396
+ "migrations",
3397
+ "apply",
3398
+ "FEEDBAX_DB",
3399
+ "--remote"
3400
+ ]);
3401
+ }
3402
+ await runIn(destination, "pnpm", ["exec", "feedbax", "doctor"]);
3403
+ if (await confirm2(
3404
+ "Authenticate with Cloudflare and deploy this portal?",
3405
+ false
3406
+ )) {
3407
+ await runIn(destination, "pnpm", ["exec", "wrangler", "login"]);
3408
+ await runIn(destination, "pnpm", ["exec", "feedbax", "deploy"]);
3409
+ }
3410
+ prompt.outro(`Your portal source is ready in ${destination}.`);
3203
3411
  }
3204
- await runIn(destination, "pnpm", ["exec", "feedbax", "doctor"]);
3205
- if (await confirm2(
3206
- "Authenticate with Cloudflare and deploy this portal?",
3207
- false
3208
- )) {
3209
- await runIn(destination, "pnpm", ["exec", "wrangler", "login"]);
3210
- await runIn(destination, "pnpm", ["exec", "feedbax", "deploy"]);
3211
- }
3212
- prompt.outro(`Your portal source is ready in ${destination}.`);
3213
3412
  }
3214
3413
  }
3215
3414
  } catch (error) {
@@ -3287,3 +3486,97 @@ function runIn(cwd, command, args) {
3287
3486
  );
3288
3487
  });
3289
3488
  }
3489
+ async function githubSetup(destination, product, workerName) {
3490
+ const serviceUrl = await text4(
3491
+ "GitHub App service HTTPS origin (official or your self-hosted service)"
3492
+ );
3493
+ const infoResponse = await fetch(`${serviceUrl}/v1/info`, {
3494
+ redirect: "manual"
3495
+ });
3496
+ if (!infoResponse.ok)
3497
+ throw new Error(
3498
+ "The App service is unavailable. Configure it before portal setup."
3499
+ );
3500
+ const info = await infoResponse.json();
3501
+ if (!Number.isSafeInteger(info.botUserId))
3502
+ throw new Error("The service did not return a valid App identity.");
3503
+ const database = {
3504
+ id: await text4(
3505
+ "Dedicated Cloudflare D1 database ID (required independently of sign-in)"
3506
+ ),
3507
+ name: await text4("Dedicated D1 database name")
3508
+ };
3509
+ const result = await provisionGitHubInstallation({
3510
+ destination,
3511
+ product,
3512
+ workerName,
3513
+ database,
3514
+ github: {
3515
+ serviceUrl,
3516
+ installationId: await text4("Stable Feedbax Installation ID", workerName),
3517
+ repository: {
3518
+ id: Number(
3519
+ await text4(
3520
+ "Feedback Repository numeric ID (gh api repos/OWNER/NAME --jq .id)"
3521
+ )
3522
+ ),
3523
+ owner: await text4("Repository owner"),
3524
+ name: await text4("Repository name")
3525
+ },
3526
+ botUserId: info.botUserId
3527
+ },
3528
+ features: {
3529
+ voting: await confirm2("Enable best-effort browser voting?", true),
3530
+ comments: await confirm2("Enable portal Comments?", true),
3531
+ changelog: false
3532
+ },
3533
+ turnstile: await confirm2("Enable Cloudflare Turnstile?", true) ? {
3534
+ siteKey: await text4("Turnstile site key"),
3535
+ secretKey: await secret("Turnstile secret key")
3536
+ } : {},
3537
+ preview: (message) => confirm2(message, false)
3538
+ });
3539
+ console.log(
3540
+ `Created ${destination}. Trusted API key \u2014 copy it now; plaintext is not stored:
3541
+ ${result.apiKey}`
3542
+ );
3543
+ await runIn(destination, "pnpm", ["install"]);
3544
+ await runIn(destination, "pnpm", [
3545
+ "exec",
3546
+ "prettier",
3547
+ "--write",
3548
+ "src/feedbax.ts"
3549
+ ]);
3550
+ if (await confirm2(
3551
+ `Apply included additive migrations to D1 ${database.name}?`,
3552
+ false
3553
+ ))
3554
+ await runIn(destination, "pnpm", [
3555
+ "exec",
3556
+ "wrangler",
3557
+ "d1",
3558
+ "migrations",
3559
+ "apply",
3560
+ "FEEDBAX_DB",
3561
+ "--remote"
3562
+ ]);
3563
+ if (await confirm2("Deploy the unpaired portal for GitHub authorization?", false))
3564
+ await runIn(destination, "pnpm", [
3565
+ "exec",
3566
+ "feedbax",
3567
+ "deploy",
3568
+ "--pairing-bootstrap"
3569
+ ]);
3570
+ const installationId = await text4(
3571
+ "GitHub App installation ID from the App installation page"
3572
+ );
3573
+ await runIn(destination, "pnpm", [
3574
+ "exec",
3575
+ "feedbax",
3576
+ "pair-github",
3577
+ installationId
3578
+ ]);
3579
+ prompt.outro(
3580
+ "Finish the GitHub authorization, then run feedbax doctor. Connected identity can be configured independently using CONNECTED.md."
3581
+ );
3582
+ }