@obh/forge 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 (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/dist/capabilities/analytics.d.ts +3 -0
  4. package/dist/capabilities/analytics.d.ts.map +1 -0
  5. package/dist/capabilities/analytics.js +91 -0
  6. package/dist/capabilities/analytics.js.map +1 -0
  7. package/dist/capabilities/api-keys.d.ts +3 -0
  8. package/dist/capabilities/api-keys.d.ts.map +1 -0
  9. package/dist/capabilities/api-keys.js +90 -0
  10. package/dist/capabilities/api-keys.js.map +1 -0
  11. package/dist/capabilities/audit.d.ts +3 -0
  12. package/dist/capabilities/audit.d.ts.map +1 -0
  13. package/dist/capabilities/audit.js +74 -0
  14. package/dist/capabilities/audit.js.map +1 -0
  15. package/dist/capabilities/entitlements.d.ts +3 -0
  16. package/dist/capabilities/entitlements.d.ts.map +1 -0
  17. package/dist/capabilities/entitlements.js +94 -0
  18. package/dist/capabilities/entitlements.js.map +1 -0
  19. package/dist/capabilities/events.d.ts +3 -0
  20. package/dist/capabilities/events.d.ts.map +1 -0
  21. package/dist/capabilities/events.js +107 -0
  22. package/dist/capabilities/events.js.map +1 -0
  23. package/dist/capabilities/files.d.ts +3 -0
  24. package/dist/capabilities/files.d.ts.map +1 -0
  25. package/dist/capabilities/files.js +100 -0
  26. package/dist/capabilities/files.js.map +1 -0
  27. package/dist/capabilities/helpers.d.ts +27 -0
  28. package/dist/capabilities/helpers.d.ts.map +1 -0
  29. package/dist/capabilities/helpers.js +63 -0
  30. package/dist/capabilities/helpers.js.map +1 -0
  31. package/dist/capabilities/import-export.d.ts +3 -0
  32. package/dist/capabilities/import-export.d.ts.map +1 -0
  33. package/dist/capabilities/import-export.js +267 -0
  34. package/dist/capabilities/import-export.js.map +1 -0
  35. package/dist/capabilities/index.d.ts +16 -0
  36. package/dist/capabilities/index.d.ts.map +1 -0
  37. package/dist/capabilities/index.js +71 -0
  38. package/dist/capabilities/index.js.map +1 -0
  39. package/dist/capabilities/jobs.d.ts +3 -0
  40. package/dist/capabilities/jobs.d.ts.map +1 -0
  41. package/dist/capabilities/jobs.js +117 -0
  42. package/dist/capabilities/jobs.js.map +1 -0
  43. package/dist/capabilities/notifications.d.ts +3 -0
  44. package/dist/capabilities/notifications.d.ts.map +1 -0
  45. package/dist/capabilities/notifications.js +113 -0
  46. package/dist/capabilities/notifications.js.map +1 -0
  47. package/dist/capabilities/search.d.ts +3 -0
  48. package/dist/capabilities/search.d.ts.map +1 -0
  49. package/dist/capabilities/search.js +121 -0
  50. package/dist/capabilities/search.js.map +1 -0
  51. package/dist/capabilities/settings.d.ts +3 -0
  52. package/dist/capabilities/settings.d.ts.map +1 -0
  53. package/dist/capabilities/settings.js +75 -0
  54. package/dist/capabilities/settings.js.map +1 -0
  55. package/dist/capabilities/types.d.ts +16 -0
  56. package/dist/capabilities/types.d.ts.map +1 -0
  57. package/dist/capabilities/types.js +3 -0
  58. package/dist/capabilities/types.js.map +1 -0
  59. package/dist/capabilities/webhooks.d.ts +3 -0
  60. package/dist/capabilities/webhooks.d.ts.map +1 -0
  61. package/dist/capabilities/webhooks.js +117 -0
  62. package/dist/capabilities/webhooks.js.map +1 -0
  63. package/dist/cli.d.ts +3 -0
  64. package/dist/cli.d.ts.map +1 -0
  65. package/dist/cli.js +60 -0
  66. package/dist/cli.js.map +1 -0
  67. package/dist/commands/add.d.ts +3 -0
  68. package/dist/commands/add.d.ts.map +1 -0
  69. package/dist/commands/add.js +88 -0
  70. package/dist/commands/add.js.map +1 -0
  71. package/dist/commands/doctor.d.ts +3 -0
  72. package/dist/commands/doctor.d.ts.map +1 -0
  73. package/dist/commands/doctor.js +94 -0
  74. package/dist/commands/doctor.js.map +1 -0
  75. package/dist/commands/generate.d.ts +3 -0
  76. package/dist/commands/generate.d.ts.map +1 -0
  77. package/dist/commands/generate.js +41 -0
  78. package/dist/commands/generate.js.map +1 -0
  79. package/dist/commands/inspect.d.ts +3 -0
  80. package/dist/commands/inspect.d.ts.map +1 -0
  81. package/dist/commands/inspect.js +64 -0
  82. package/dist/commands/inspect.js.map +1 -0
  83. package/dist/commands/new.d.ts +18 -0
  84. package/dist/commands/new.d.ts.map +1 -0
  85. package/dist/commands/new.js +149 -0
  86. package/dist/commands/new.js.map +1 -0
  87. package/dist/commands/shared.d.ts +14 -0
  88. package/dist/commands/shared.d.ts.map +1 -0
  89. package/dist/commands/shared.js +36 -0
  90. package/dist/commands/shared.js.map +1 -0
  91. package/dist/commands/skill.d.ts +3 -0
  92. package/dist/commands/skill.d.ts.map +1 -0
  93. package/dist/commands/skill.js +59 -0
  94. package/dist/commands/skill.js.map +1 -0
  95. package/dist/commands/stack.d.ts +13 -0
  96. package/dist/commands/stack.d.ts.map +1 -0
  97. package/dist/commands/stack.js +192 -0
  98. package/dist/commands/stack.js.map +1 -0
  99. package/dist/generators/api.d.ts +16 -0
  100. package/dist/generators/api.d.ts.map +1 -0
  101. package/dist/generators/api.js +375 -0
  102. package/dist/generators/api.js.map +1 -0
  103. package/dist/generators/lwd.d.ts +12 -0
  104. package/dist/generators/lwd.d.ts.map +1 -0
  105. package/dist/generators/lwd.js +143 -0
  106. package/dist/generators/lwd.js.map +1 -0
  107. package/dist/generators/mobile.d.ts +16 -0
  108. package/dist/generators/mobile.d.ts.map +1 -0
  109. package/dist/generators/mobile.js +180 -0
  110. package/dist/generators/mobile.js.map +1 -0
  111. package/dist/generators/platform-package.d.ts +18 -0
  112. package/dist/generators/platform-package.d.ts.map +1 -0
  113. package/dist/generators/platform-package.js +568 -0
  114. package/dist/generators/platform-package.js.map +1 -0
  115. package/dist/generators/root.d.ts +15 -0
  116. package/dist/generators/root.d.ts.map +1 -0
  117. package/dist/generators/root.js +249 -0
  118. package/dist/generators/root.js.map +1 -0
  119. package/dist/generators/sdk.d.ts +14 -0
  120. package/dist/generators/sdk.d.ts.map +1 -0
  121. package/dist/generators/sdk.js +137 -0
  122. package/dist/generators/sdk.js.map +1 -0
  123. package/dist/generators/web.d.ts +15 -0
  124. package/dist/generators/web.d.ts.map +1 -0
  125. package/dist/generators/web.js +203 -0
  126. package/dist/generators/web.js.map +1 -0
  127. package/dist/generators/worker.d.ts +13 -0
  128. package/dist/generators/worker.d.ts.map +1 -0
  129. package/dist/generators/worker.js +148 -0
  130. package/dist/generators/worker.js.map +1 -0
  131. package/dist/index.d.ts +10 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +44 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/project/context.d.ts +24 -0
  136. package/dist/project/context.d.ts.map +1 -0
  137. package/dist/project/context.js +63 -0
  138. package/dist/project/context.js.map +1 -0
  139. package/dist/project/manifest.d.ts +26 -0
  140. package/dist/project/manifest.d.ts.map +1 -0
  141. package/dist/project/manifest.js +73 -0
  142. package/dist/project/manifest.js.map +1 -0
  143. package/dist/project/paths.d.ts +25 -0
  144. package/dist/project/paths.d.ts.map +1 -0
  145. package/dist/project/paths.js +51 -0
  146. package/dist/project/paths.js.map +1 -0
  147. package/dist/project/plan.d.ts +42 -0
  148. package/dist/project/plan.d.ts.map +1 -0
  149. package/dist/project/plan.js +251 -0
  150. package/dist/project/plan.js.map +1 -0
  151. package/dist/prompts/index.d.ts +24 -0
  152. package/dist/prompts/index.d.ts.map +1 -0
  153. package/dist/prompts/index.js +111 -0
  154. package/dist/prompts/index.js.map +1 -0
  155. package/dist/recipes/index.d.ts +25 -0
  156. package/dist/recipes/index.d.ts.map +1 -0
  157. package/dist/recipes/index.js +77 -0
  158. package/dist/recipes/index.js.map +1 -0
  159. package/dist/stack/infer.d.ts +4 -0
  160. package/dist/stack/infer.d.ts.map +1 -0
  161. package/dist/stack/infer.js +165 -0
  162. package/dist/stack/infer.js.map +1 -0
  163. package/dist/stack/lwd.d.ts +24 -0
  164. package/dist/stack/lwd.d.ts.map +1 -0
  165. package/dist/stack/lwd.js +62 -0
  166. package/dist/stack/lwd.js.map +1 -0
  167. package/dist/stack/lwdtoml.d.ts +38 -0
  168. package/dist/stack/lwdtoml.d.ts.map +1 -0
  169. package/dist/stack/lwdtoml.js +57 -0
  170. package/dist/stack/lwdtoml.js.map +1 -0
  171. package/dist/stack/manifest.d.ts +15 -0
  172. package/dist/stack/manifest.d.ts.map +1 -0
  173. package/dist/stack/manifest.js +86 -0
  174. package/dist/stack/manifest.js.map +1 -0
  175. package/dist/stack/types.d.ts +48 -0
  176. package/dist/stack/types.d.ts.map +1 -0
  177. package/dist/stack/types.js +5 -0
  178. package/dist/stack/types.js.map +1 -0
  179. package/dist/stack/wire.d.ts +40 -0
  180. package/dist/stack/wire.d.ts.map +1 -0
  181. package/dist/stack/wire.js +192 -0
  182. package/dist/stack/wire.js.map +1 -0
  183. package/dist/types.d.ts +114 -0
  184. package/dist/types.d.ts.map +1 -0
  185. package/dist/types.js +5 -0
  186. package/dist/types.js.map +1 -0
  187. package/dist/utils/fs.d.ts +13 -0
  188. package/dist/utils/fs.d.ts.map +1 -0
  189. package/dist/utils/fs.js +64 -0
  190. package/dist/utils/fs.js.map +1 -0
  191. package/dist/utils/git.d.ts +17 -0
  192. package/dist/utils/git.d.ts.map +1 -0
  193. package/dist/utils/git.js +49 -0
  194. package/dist/utils/git.js.map +1 -0
  195. package/dist/utils/logger.d.ts +16 -0
  196. package/dist/utils/logger.d.ts.map +1 -0
  197. package/dist/utils/logger.js +21 -0
  198. package/dist/utils/logger.js.map +1 -0
  199. package/dist/utils/shell.d.ts +21 -0
  200. package/dist/utils/shell.d.ts.map +1 -0
  201. package/dist/utils/shell.js +40 -0
  202. package/dist/utils/shell.js.map +1 -0
  203. package/dist/version.d.ts +2 -0
  204. package/dist/version.d.ts.map +1 -0
  205. package/dist/version.js +6 -0
  206. package/dist/version.js.map +1 -0
  207. package/package.json +40 -0
  208. package/skills/obh-add-events/SKILL.md +26 -0
  209. package/skills/obh-generate-audit-rules/SKILL.md +26 -0
  210. package/skills/obh-inspect-project/SKILL.md +26 -0
  211. package/skills/obh-lwd-manifest/SKILL.md +26 -0
  212. package/skills/obh-retrofit-files/SKILL.md +24 -0
  213. package/skills/obh-retrofit-jobs/SKILL.md +26 -0
  214. package/skills/obh-sdk-extraction/SKILL.md +24 -0
  215. package/skills/obh-settings-migration/SKILL.md +24 -0
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CAPABILITIES = void 0;
4
+ exports.getCapability = getCapability;
5
+ exports.isImplemented = isImplemented;
6
+ exports.resolveOrder = resolveOrder;
7
+ exports.missingPrerequisites = missingPrerequisites;
8
+ const events_1 = require("./events");
9
+ const jobs_1 = require("./jobs");
10
+ const files_1 = require("./files");
11
+ const audit_1 = require("./audit");
12
+ const settings_1 = require("./settings");
13
+ const api_keys_1 = require("./api-keys");
14
+ const webhooks_1 = require("./webhooks");
15
+ const import_export_1 = require("./import-export");
16
+ const entitlements_1 = require("./entitlements");
17
+ const search_1 = require("./search");
18
+ const analytics_1 = require("./analytics");
19
+ const notifications_1 = require("./notifications");
20
+ /** Every OBH platform primitive Forge can install. */
21
+ exports.CAPABILITIES = {
22
+ events: events_1.events,
23
+ jobs: jobs_1.jobs,
24
+ files: files_1.files,
25
+ audit: audit_1.audit,
26
+ settings: settings_1.settings,
27
+ "api-keys": api_keys_1.apiKeys,
28
+ webhooks: webhooks_1.webhooks,
29
+ "import-export": import_export_1.importExport,
30
+ entitlements: entitlements_1.entitlements,
31
+ search: search_1.search,
32
+ analytics: analytics_1.analytics,
33
+ notifications: notifications_1.notifications,
34
+ };
35
+ function getCapability(name) {
36
+ return exports.CAPABILITIES[name];
37
+ }
38
+ function isImplemented(name) {
39
+ return exports.CAPABILITIES[name] !== undefined;
40
+ }
41
+ /**
42
+ * Expand a requested set of capabilities to include their prerequisites, then
43
+ * return them in dependency order (requires-before-dependents). Unknown names
44
+ * are dropped by the caller after reporting.
45
+ */
46
+ function resolveOrder(requested) {
47
+ const ordered = [];
48
+ const seen = new Set();
49
+ const visit = (name) => {
50
+ if (seen.has(name))
51
+ return;
52
+ seen.add(name);
53
+ const cap = exports.CAPABILITIES[name];
54
+ if (!cap)
55
+ return;
56
+ for (const req of cap.requires ?? [])
57
+ visit(req);
58
+ ordered.push(name);
59
+ };
60
+ for (const name of requested)
61
+ visit(name);
62
+ return ordered;
63
+ }
64
+ /** Prerequisites of a capability that are not already installed/requested. */
65
+ function missingPrerequisites(name, installed) {
66
+ const cap = exports.CAPABILITIES[name];
67
+ if (!cap)
68
+ return [];
69
+ return (cap.requires ?? []).filter((r) => !installed(r));
70
+ }
71
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/capabilities/index.ts"],"names":[],"mappings":";;;AAiCA,sCAEC;AAED,sCAEC;AAOD,oCAeC;AAGD,oDAOC;AArED,qCAAiC;AACjC,iCAA6B;AAC7B,mCAA+B;AAC/B,mCAA+B;AAC/B,yCAAqC;AACrC,yCAAoC;AACpC,yCAAqC;AACrC,mDAA8C;AAC9C,iDAA6C;AAC7C,qCAAiC;AACjC,2CAAuC;AACvC,mDAA+C;AAI/C,sDAAsD;AACzC,QAAA,YAAY,GAAgD;IACvE,MAAM,EAAN,eAAM;IACN,IAAI,EAAJ,WAAI;IACJ,KAAK,EAAL,aAAK;IACL,KAAK,EAAL,aAAK;IACL,QAAQ,EAAR,mBAAQ;IACR,UAAU,EAAE,kBAAO;IACnB,QAAQ,EAAR,mBAAQ;IACR,eAAe,EAAE,4BAAY;IAC7B,YAAY,EAAZ,2BAAY;IACZ,MAAM,EAAN,eAAM;IACN,SAAS,EAAT,qBAAS;IACT,aAAa,EAAb,6BAAa;CACd,CAAA;AAED,SAAgB,aAAa,CAAC,IAAoB;IAChD,OAAO,oBAAY,CAAC,IAAI,CAAC,CAAA;AAC3B,CAAC;AAED,SAAgB,aAAa,CAAC,IAAoB;IAChD,OAAO,oBAAY,CAAC,IAAI,CAAC,KAAK,SAAS,CAAA;AACzC,CAAC;AAED;;;;GAIG;AACH,SAAgB,YAAY,CAAC,SAA2B;IACtD,MAAM,OAAO,GAAqB,EAAE,CAAA;IACpC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAA;IAEtC,MAAM,KAAK,GAAG,CAAC,IAAoB,EAAQ,EAAE;QAC3C,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAM;QAC1B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACd,MAAM,GAAG,GAAG,oBAAY,CAAC,IAAI,CAAC,CAAA;QAC9B,IAAI,CAAC,GAAG;YAAE,OAAM;QAChB,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,IAAI,EAAE;YAAE,KAAK,CAAC,GAAG,CAAC,CAAA;QAChD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACpB,CAAC,CAAA;IAED,KAAK,MAAM,IAAI,IAAI,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,CAAA;IACzC,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,8EAA8E;AAC9E,SAAgB,oBAAoB,CAClC,IAAoB,EACpB,SAAyC;IAEzC,MAAM,GAAG,GAAG,oBAAY,CAAC,IAAI,CAAC,CAAA;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAA;IACnB,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;AAC1D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Capability } from "./types";
2
+ export declare const jobs: Capability;
3
+ //# sourceMappingURL=jobs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../src/capabilities/jobs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,IAAI,EAAE,UAyBlB,CAAA"}
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.jobs = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ exports.jobs = {
6
+ name: "jobs",
7
+ describe: "Postgres job queue + worker — background/scheduled commands, idempotent handlers",
8
+ apply(ctx, plan) {
9
+ (0, helpers_1.addPlatformPackage)(plan, ctx, "jobs", { api: true, worker: true });
10
+ // defineJob validates payloads with a zod schema on both the enqueue and run sides.
11
+ if (ctx.hasApp("api"))
12
+ plan.addDependency("apps/api", "zod", "^3.23.8");
13
+ if (ctx.hasWorker())
14
+ plan.addDependency("apps/worker", "zod", "^3.23.8");
15
+ plan.create("scripts/migrations.d/jobs.ts", (0, helpers_1.migrationModule)("jobs"), "jobs migrations wiring");
16
+ const notes = (0, helpers_1.hasNotesExample)(ctx);
17
+ if (ctx.hasApp("api")) {
18
+ plan.create("apps/api/src/platform/jobs.ts", apiClient(notes), "jobs client + registry");
19
+ if (notes) {
20
+ // Enqueue a job whenever a note is created — inside the write transaction.
21
+ plan.create("apps/api/src/bus.d/jobs-index.ts", ENQUEUE, "enqueue a job on note.created");
22
+ }
23
+ }
24
+ if (ctx.hasWorker()) {
25
+ plan.create("apps/worker/src/consumers.d/jobs.ts", worker(notes), "jobs worker (claims + runs jobs)");
26
+ }
27
+ plan.patchManifest({ platform: { jobs: true } });
28
+ plan.nextStep("Run `pnpm migrate`. Enqueue with jobs.enqueue(tx, { name, workspaceId, payload }); the worker runs handlers.");
29
+ },
30
+ };
31
+ function jobExample(notes) {
32
+ return notes
33
+ ? { name: "send_welcome_email", export: "sendWelcomeEmail", schema: "z.object({ noteId: z.string() })", field: "note_id: payload.noteId" }
34
+ : { name: "example_job", export: "exampleJob", schema: "z.object({ id: z.string() })", field: "id: payload.id" };
35
+ }
36
+ function apiClient(notes) {
37
+ const d = jobExample(notes);
38
+ return `import { createJobClient, createJobRegistry, defineJob, type JobDb, type JobDefinition } from "@obh/jobs"
39
+ import { z } from "zod"
40
+
41
+ // Every platform row is scoped to a workspace. Single-tenant apps can leave this.
42
+ export const WORKSPACE = process.env.WORKSPACE_ID ?? "default"
43
+
44
+ // Jobs are commands (snake_case imperatives), not events. The handler runs in the
45
+ // worker (apps/worker/src/consumers.d/jobs.ts); here the schema validates payloads
46
+ // at enqueue time. Keep the name + schema in sync with the worker's copy.
47
+ export const ${d.export} = defineJob({
48
+ name: "${d.name}",
49
+ version: 1,
50
+ schema: ${d.schema},
51
+ handler: async () => {
52
+ throw new Error("${d.name} runs in the worker, not the API")
53
+ },
54
+ })
55
+
56
+ export const registry = createJobRegistry([${d.export} as JobDefinition])
57
+
58
+ export const jobs = createJobClient({ source: "app", registry })
59
+
60
+ /** Adapt a raw pg transaction to the structural JobDb @obh/jobs expects. */
61
+ export function asJobDb(tx: { query: (sql: string, params?: unknown[]) => Promise<{ rows: unknown[] }> }): JobDb {
62
+ return { query: (sql: string, params?: unknown[]) => tx.query(sql, params) as never }
63
+ }
64
+ `;
65
+ }
66
+ const ENQUEUE = `import { onEmit } from "../bus"
67
+ import { asJobDb, jobs, sendWelcomeEmail, WORKSPACE } from "../platform/jobs"
68
+
69
+ // Enqueue a job on the SAME transaction as the note write, so the job is durable
70
+ // exactly when the note is (and never queued if the write rolls back). The
71
+ // idempotency key makes a re-emit a no-op rather than a duplicate job.
72
+ onEmit(async (tx, name, payload) => {
73
+ if (name !== "note.created") return
74
+ const noteId = String((payload as { id?: string }).id ?? "")
75
+ await jobs.enqueue(asJobDb(tx), {
76
+ name: sendWelcomeEmail.name,
77
+ workspaceId: WORKSPACE,
78
+ payload: { noteId },
79
+ idempotencyKey: \`welcome:\${noteId}\`,
80
+ })
81
+ })
82
+ `;
83
+ function worker(notes) {
84
+ const d = jobExample(notes);
85
+ return `// Runs queued jobs: each tick claims a batch and executes the matching handler.
86
+ // Handlers live here so the worker never imports across app boundaries; keep the
87
+ // name + schema in sync with apps/api/src/platform/jobs.ts.
88
+ import { createJobRegistry, createWorker, defineJob, pgAdapter, type JobDefinition } from "@obh/jobs"
89
+ import { z } from "zod"
90
+ import type { WorkerContext } from "../context"
91
+
92
+ const ${d.export} = defineJob({
93
+ name: "${d.name}",
94
+ version: 1,
95
+ schema: ${d.schema},
96
+ handler: async (ctx, payload) => {
97
+ ctx.log.info("running ${d.name}", { ${d.field} })
98
+ // Real work goes here (call your mailer, render a PDF, …). Handlers must be
99
+ // idempotent: at-least-once execution means a job can run more than once.
100
+ console.log(JSON.stringify({ msg: "${d.name} done", ${d.field}, job_id: ctx.jobId }))
101
+ },
102
+ })
103
+
104
+ const registry = createJobRegistry([${d.export} as JobDefinition])
105
+
106
+ let worker: ReturnType<typeof createWorker>
107
+
108
+ export function init(ctx: WorkerContext): void {
109
+ worker = createWorker({ db: pgAdapter(ctx.pool), registry, instanceId: "worker" })
110
+ }
111
+
112
+ export async function tick(): Promise<void> {
113
+ await worker.tick()
114
+ }
115
+ `;
116
+ }
117
+ //# sourceMappingURL=jobs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobs.js","sourceRoot":"","sources":["../../src/capabilities/jobs.ts"],"names":[],"mappings":";;;AACA,uCAAgF;AAEnE,QAAA,IAAI,GAAe;IAC9B,IAAI,EAAE,MAAM;IACZ,QAAQ,EAAE,kFAAkF;IAC5F,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QAClE,oFAAoF;QACpF,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;QACvE,IAAI,GAAG,CAAC,SAAS,EAAE;YAAE,IAAI,CAAC,aAAa,CAAC,aAAa,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;QACxE,IAAI,CAAC,MAAM,CAAC,8BAA8B,EAAE,IAAA,yBAAe,EAAC,MAAM,CAAC,EAAE,wBAAwB,CAAC,CAAA;QAE9F,MAAM,KAAK,GAAG,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAA;QAClC,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,+BAA+B,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,wBAAwB,CAAC,CAAA;YACxF,IAAI,KAAK,EAAE,CAAC;gBACV,2EAA2E;gBAC3E,IAAI,CAAC,MAAM,CAAC,kCAAkC,EAAE,OAAO,EAAE,+BAA+B,CAAC,CAAA;YAC3F,CAAC;QACH,CAAC;QACD,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,qCAAqC,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,kCAAkC,CAAC,CAAA;QACvG,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QAChD,IAAI,CAAC,QAAQ,CAAC,8GAA8G,CAAC,CAAA;IAC/H,CAAC;CACF,CAAA;AASD,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,KAAK;QACV,CAAC,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,kCAAkC,EAAE,KAAK,EAAE,yBAAyB,EAAE;QAC1I,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,8BAA8B,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAA;AACpH,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;IAC3B,OAAO;;;;;;;;;eASM,CAAC,CAAC,MAAM;WACZ,CAAC,CAAC,IAAI;;YAEL,CAAC,CAAC,MAAM;;uBAEG,CAAC,CAAC,IAAI;;;;6CAIgB,CAAC,CAAC,MAAM;;;;;;;;CAQpD,CAAA;AACD,CAAC;AAED,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;CAgBf,CAAA;AAED,SAAS,MAAM,CAAC,KAAc;IAC5B,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;IAC3B,OAAO;;;;;;;QAOD,CAAC,CAAC,MAAM;WACL,CAAC,CAAC,IAAI;;YAEL,CAAC,CAAC,MAAM;;4BAEQ,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,KAAK;;;yCAGR,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,KAAK;;;;sCAI3B,CAAC,CAAC,MAAM;;;;;;;;;;;CAW7C,CAAA;AACD,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Capability } from "./types";
2
+ export declare const notifications: Capability;
3
+ //# sourceMappingURL=notifications.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../../src/capabilities/notifications.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,aAAa,EAAE,UA+B3B,CAAA"}
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.notifications = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ exports.notifications = {
6
+ name: "notifications",
7
+ requires: ["events"],
8
+ describe: "Email notification engine — templates + rules reacting to events, delivered over SMTP",
9
+ apply(ctx, plan) {
10
+ (0, helpers_1.addPlatformPackage)(plan, ctx, "notifications", { api: true, worker: true });
11
+ plan.create("scripts/migrations.d/notifications.ts", (0, helpers_1.migrationModule)("notifications"), "notifications migrations wiring");
12
+ const notes = (0, helpers_1.hasNotesExample)(ctx);
13
+ if (ctx.hasApp("api")) {
14
+ plan.create("apps/api/src/platform/notifications.ts", CLIENT, "direct-send notification client");
15
+ }
16
+ if (ctx.hasWorker()) {
17
+ plan.create("apps/worker/src/notification-defs.ts", defsFile(notes), "worker-side templates + rules");
18
+ plan.create("apps/worker/src/dispatch.d/notifications.ts", `export const consumer = { name: "notifications", events: ["*"] }\n`, "register notifications as an event consumer");
19
+ plan.create("apps/worker/src/consumers.d/notifications.ts", WORKER, "notification intake + send tick");
20
+ }
21
+ plan.addEnvVar({
22
+ name: "SMTP_URL",
23
+ example: "smtp://user:pass@localhost:1025",
24
+ comment: "smtp://user:pass@host:port (smtps:// for implicit TLS)",
25
+ });
26
+ plan.patchManifest({ platform: { notifications: true } });
27
+ plan.nextStep("Run `pnpm migrate` and set SMTP_URL. Rules turn events into intents the worker renders and sends.");
28
+ },
29
+ };
30
+ // The event-driven path (rules) is preferred; this client is the escape hatch
31
+ // for notifications that aren't domain events (invites, password resets).
32
+ const CLIENT = `import { createNotificationClient, pgAdapter } from "@obh/notifications"
33
+ import { pool } from "../db"
34
+
35
+ export const notifications = createNotificationClient({ db: pgAdapter(pool) })
36
+ `;
37
+ function defsFile(notes) {
38
+ const templates = notes
39
+ ? ` defineTemplate({
40
+ key: "note_created_email",
41
+ version: 1,
42
+ channel: "email",
43
+ name: "Note created",
44
+ subject: "New note: {{title}}",
45
+ bodyText: "A note titled {{title}} was just created.",
46
+ }),`
47
+ : ` // defineTemplate({ key: "thing_created_email", channel: "email", name: "Thing created", subject: "New thing", bodyText: "…" }),`;
48
+ const rules = notes
49
+ ? ` defineNotificationRule({
50
+ id: "rule_note_created",
51
+ eventName: "note.created",
52
+ templateKey: "note_created_email",
53
+ recipientStrategy: "static_email",
54
+ recipientConfig: { email: process.env.NOTIFY_TO ?? "ops@example.com" },
55
+ variableMapping: { title: "$.payload.title" },
56
+ }),`
57
+ : ` // defineNotificationRule({ id: "rule_thing_created", eventName: "thing.created", templateKey: "thing_created_email", recipientStrategy: "static_email", recipientConfig: { email: "ops@example.com" } }),`;
58
+ return `import {
59
+ defineNotificationRule,
60
+ defineTemplate,
61
+ type NotificationRule,
62
+ type NotificationTemplate,
63
+ } from "@obh/notifications"
64
+
65
+ // Worker-side template + rule definitions, seeded into the DB on startup.
66
+ export const templates: NotificationTemplate[] = [
67
+ ${templates}
68
+ ]
69
+
70
+ export const rules: NotificationRule[] = [
71
+ ${rules}
72
+ ]
73
+ `;
74
+ }
75
+ const WORKER = `// Seeds templates/rules, then each tick: claim the "notifications" event
76
+ // deliveries into intents, then render and send pending intents over SMTP
77
+ // (retried, dead-lettered). intake and send both run in the default tick().
78
+ import { createNotificationWorker, pgAdapter, smtpProvider, syncRules, syncTemplates } from "@obh/notifications"
79
+ import { rules, templates } from "../notification-defs"
80
+ import type { WorkerContext } from "../context"
81
+
82
+ let worker: ReturnType<typeof createNotificationWorker>
83
+
84
+ function providerFromEnv() {
85
+ const url = new URL(process.env.SMTP_URL ?? "smtp://localhost:1025")
86
+ return smtpProvider({
87
+ host: url.hostname,
88
+ port: Number(url.port) || 25,
89
+ secure: url.protocol === "smtps:",
90
+ user: url.username ? decodeURIComponent(url.username) : undefined,
91
+ pass: url.password ? decodeURIComponent(url.password) : undefined,
92
+ from: process.env.SMTP_FROM ?? "no-reply@example.com",
93
+ })
94
+ }
95
+
96
+ export async function init(ctx: WorkerContext): Promise<void> {
97
+ const db = pgAdapter(ctx.pool)
98
+ // Keep the DB in sync with the code-authored templates/rules (idempotent).
99
+ await syncTemplates(db, templates)
100
+ await syncRules(db, rules)
101
+ worker = createNotificationWorker({
102
+ db,
103
+ provider: providerFromEnv(),
104
+ instanceId: "worker",
105
+ defaultFrom: process.env.SMTP_FROM ?? "no-reply@example.com",
106
+ })
107
+ }
108
+
109
+ export async function tick(): Promise<void> {
110
+ await worker.tick()
111
+ }
112
+ `;
113
+ //# sourceMappingURL=notifications.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../src/capabilities/notifications.ts"],"names":[],"mappings":";;;AACA,uCAAgF;AAEnE,QAAA,aAAa,GAAe;IACvC,IAAI,EAAE,eAAe;IACrB,QAAQ,EAAE,CAAC,QAAQ,CAAC;IACpB,QAAQ,EAAE,uFAAuF;IACjG,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,eAAe,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QAC3E,IAAI,CAAC,MAAM,CAAC,uCAAuC,EAAE,IAAA,yBAAe,EAAC,eAAe,CAAC,EAAE,iCAAiC,CAAC,CAAA;QAEzH,MAAM,KAAK,GAAG,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAA;QAClC,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,wCAAwC,EAAE,MAAM,EAAE,iCAAiC,CAAC,CAAA;QAClG,CAAC;QACD,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,sCAAsC,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,+BAA+B,CAAC,CAAA;YACrG,IAAI,CAAC,MAAM,CACT,6CAA6C,EAC7C,oEAAoE,EACpE,6CAA6C,CAC9C,CAAA;YACD,IAAI,CAAC,MAAM,CAAC,8CAA8C,EAAE,MAAM,EAAE,iCAAiC,CAAC,CAAA;QACxG,CAAC;QAED,IAAI,CAAC,SAAS,CAAC;YACb,IAAI,EAAE,UAAU;YAChB,OAAO,EAAE,iCAAiC;YAC1C,OAAO,EAAE,wDAAwD;SAClE,CAAC,CAAA;QAEF,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QACzD,IAAI,CAAC,QAAQ,CAAC,mGAAmG,CAAC,CAAA;IACpH,CAAC;CACF,CAAA;AAED,8EAA8E;AAC9E,0EAA0E;AAC1E,MAAM,MAAM,GAAG;;;;CAId,CAAA;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,MAAM,SAAS,GAAG,KAAK;QACrB,CAAC,CAAC;;;;;;;MAOA;QACF,CAAC,CAAC,oIAAoI,CAAA;IACxI,MAAM,KAAK,GAAG,KAAK;QACjB,CAAC,CAAC;;;;;;;MAOA;QACF,CAAC,CAAC,8MAA8M,CAAA;IAClN,OAAO;;;;;;;;;EASP,SAAS;;;;EAIT,KAAK;;CAEN,CAAA;AACD,CAAC;AAED,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCd,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { Capability } from "./types";
2
+ export declare const search: Capability;
3
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/capabilities/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,MAAM,EAAE,UA8BpB,CAAA"}
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.search = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ exports.search = {
6
+ name: "search",
7
+ requires: ["events"],
8
+ describe: "Workspace search + indexing — Postgres full-text/trigram, kept fresh from events",
9
+ apply(ctx, plan) {
10
+ (0, helpers_1.addPlatformPackage)(plan, ctx, "search", { api: true, worker: true });
11
+ plan.create("scripts/migrations.d/search.ts", (0, helpers_1.migrationModule)("search"), "search migrations wiring");
12
+ const notes = (0, helpers_1.hasNotesExample)(ctx);
13
+ if (ctx.hasApp("api")) {
14
+ plan.create("apps/api/src/platform/search.ts", CLIENT, "search client + query provider");
15
+ plan.create("apps/api/src/routes/search.ts", (0, helpers_1.apiFramework)(ctx) === "hono" ? ROUTE_HONO : ROUTE_EXPRESS, "search query route");
16
+ }
17
+ if (ctx.hasWorker()) {
18
+ plan.create("apps/worker/src/search-entities.ts", entitiesFile(notes), "worker-side search entity definitions");
19
+ plan.create("apps/worker/src/dispatch.d/search.ts", `export const consumer = { name: "search", events: ["*"] }\n`, "register search as an event consumer");
20
+ plan.create("apps/worker/src/consumers.d/search.ts", WORKER, "search indexer tick");
21
+ }
22
+ plan.patchManifest({ platform: { search: true } });
23
+ plan.nextStep("Run `pnpm migrate` (the @obh/search migration enables pg_trgm). The worker keeps the index fresh from events; query at GET /api/search?q=.");
24
+ },
25
+ };
26
+ // The client only needs the query provider; entity mappers live worker-side.
27
+ const CLIENT = `import { createPostgresSearchProvider, createSearchClient, pgAdapter } from "@obh/search"
28
+ import { pool } from "../db"
29
+
30
+ // Postgres full-text + trigram provider. pg_trgm is enabled by the migration.
31
+ const provider = createPostgresSearchProvider({ db: pgAdapter(pool) })
32
+
33
+ export const search = createSearchClient({ provider })
34
+ `;
35
+ function entitiesFile(notes) {
36
+ const defs = notes
37
+ ? ` defineSearchEntity({
38
+ type: "note",
39
+ events: ["note.created", "note.updated"],
40
+ deleteEvents: ["note.deleted"],
41
+ buildDocument: (_ctx, event) => {
42
+ const p = event.payload as { id: string; title?: string; body?: string }
43
+ return {
44
+ workspaceId: event.workspaceId,
45
+ entityType: "note",
46
+ entityId: p.id,
47
+ title: p.title ?? "",
48
+ content: p.body ?? "",
49
+ }
50
+ },
51
+ buildDeleteRef: (_ctx, event) => {
52
+ const p = event.payload as { id: string }
53
+ return { workspaceId: event.workspaceId, entityType: "note", entityId: p.id }
54
+ },
55
+ }),`
56
+ : ` // defineSearchEntity({
57
+ // type: "thing",
58
+ // events: ["thing.created", "thing.updated"],
59
+ // deleteEvents: ["thing.deleted"],
60
+ // buildDocument: (_ctx, event) => ({ workspaceId: event.workspaceId, entityType: "thing", entityId: String((event.payload as { id: string }).id), title: "…", content: "…" }),
61
+ // buildDeleteRef: (_ctx, event) => ({ workspaceId: event.workspaceId, entityType: "thing", entityId: String((event.payload as { id: string }).id) }),
62
+ // }),`;
63
+ return `import { defineSearchEntity, type SearchEntityDefinition } from "@obh/search"
64
+
65
+ // Worker-side entity definitions: how domain events become search documents.
66
+ // Kept here so the worker never imports across app boundaries.
67
+ export const entities: SearchEntityDefinition[] = [
68
+ ${defs}
69
+ ]
70
+ `;
71
+ }
72
+ const ROUTE_HONO = `import type { Hono } from "hono"
73
+ import { search } from "../platform/search"
74
+ import { WORKSPACE } from "../platform/events"
75
+
76
+ export function register(app: Hono): void {
77
+ app.get("/api/search", async (c) => {
78
+ const q = c.req.query("q") ?? ""
79
+ return c.json(await search.query({ workspaceId: WORKSPACE, query: q }))
80
+ })
81
+ }
82
+ `;
83
+ const ROUTE_EXPRESS = `import type { Express, Request, Response } from "express"
84
+ import { search } from "../platform/search"
85
+ import { WORKSPACE } from "../platform/events"
86
+
87
+ export function register(app: Express): void {
88
+ app.get("/api/search", async (req: Request, res: Response) => {
89
+ const q = (req.query.q as string) ?? ""
90
+ res.json(await search.query({ workspaceId: WORKSPACE, query: q }))
91
+ })
92
+ }
93
+ `;
94
+ const WORKER = `// Claims the "search" event deliveries and keeps the index in sync. The search
95
+ // worker exposes an events consumer (index on create/update, unindex on delete);
96
+ // createConsumerRunner drives it against this consumer's deliveries.
97
+ import { createConsumerRunner, pgAdapter } from "@obh/events"
98
+ import { createPostgresSearchProvider, createSearchClient, createSearchWorker, pgAdapter as searchPgAdapter } from "@obh/search"
99
+ import { entities } from "../search-entities"
100
+ import type { WorkerContext } from "../context"
101
+
102
+ let runner: ReturnType<typeof createConsumerRunner>
103
+
104
+ export function init(ctx: WorkerContext): void {
105
+ const provider = createPostgresSearchProvider({ db: searchPgAdapter(ctx.pool) })
106
+ const client = createSearchClient({ provider })
107
+ // deps are passed to mappers as ctx.db; the note mappers read straight from
108
+ // the event payload, so nothing product-specific is needed here.
109
+ const worker = createSearchWorker<unknown>({ client, entities, deps: undefined })
110
+ runner = createConsumerRunner({
111
+ db: pgAdapter(ctx.pool),
112
+ consumers: [worker.consumer],
113
+ instanceId: "worker",
114
+ })
115
+ }
116
+
117
+ export async function tick(): Promise<void> {
118
+ await runner.tick()
119
+ }
120
+ `;
121
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/capabilities/search.ts"],"names":[],"mappings":";;;AACA,uCAA8F;AAEjF,QAAA,MAAM,GAAe;IAChC,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,CAAC,QAAQ,CAAC;IACpB,QAAQ,EAAE,kFAAkF;IAC5F,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QACpE,IAAI,CAAC,MAAM,CAAC,gCAAgC,EAAE,IAAA,yBAAe,EAAC,QAAQ,CAAC,EAAE,0BAA0B,CAAC,CAAA;QAEpG,MAAM,KAAK,GAAG,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAA;QAClC,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,iCAAiC,EAAE,MAAM,EAAE,gCAAgC,CAAC,CAAA;YACxF,IAAI,CAAC,MAAM,CACT,+BAA+B,EAC/B,IAAA,sBAAY,EAAC,GAAG,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,EACzD,oBAAoB,CACrB,CAAA;QACH,CAAC;QACD,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,oCAAoC,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,uCAAuC,CAAC,CAAA;YAC/G,IAAI,CAAC,MAAM,CACT,sCAAsC,EACtC,6DAA6D,EAC7D,sCAAsC,CACvC,CAAA;YACD,IAAI,CAAC,MAAM,CAAC,uCAAuC,EAAE,MAAM,EAAE,qBAAqB,CAAC,CAAA;QACrF,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QAClD,IAAI,CAAC,QAAQ,CAAC,4IAA4I,CAAC,CAAA;IAC7J,CAAC;CACF,CAAA;AAED,6EAA6E;AAC7E,MAAM,MAAM,GAAG;;;;;;;CAOd,CAAA;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,MAAM,IAAI,GAAG,KAAK;QAChB,CAAC,CAAC;;;;;;;;;;;;;;;;;;MAkBA;QACF,CAAC,CAAC;;;;;;SAMG,CAAA;IACP,OAAO;;;;;EAKP,IAAI;;CAEL,CAAA;AACD,CAAC;AAED,MAAM,UAAU,GAAG;;;;;;;;;;CAUlB,CAAA;AAED,MAAM,aAAa,GAAG;;;;;;;;;;CAUrB,CAAA;AAED,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Bd,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { Capability } from "./types";
2
+ export declare const settings: Capability;
3
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/capabilities/settings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,QAAQ,EAAE,UAiBtB,CAAA"}
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.settings = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ exports.settings = {
6
+ name: "settings",
7
+ describe: "Typed, scoped configuration — defaults in code, overrides per workspace in Postgres",
8
+ apply(ctx, plan) {
9
+ (0, helpers_1.addPlatformPackage)(plan, ctx, "settings", { api: true });
10
+ // defineSetting validates values with a zod schema.
11
+ if (ctx.hasApp("api"))
12
+ plan.addDependency("apps/api", "zod", "^3.23.8");
13
+ plan.create("scripts/migrations.d/settings.ts", (0, helpers_1.migrationModule)("settings"), "settings migrations wiring");
14
+ if (ctx.hasApp("api")) {
15
+ plan.create("apps/api/src/platform/settings.ts", registryFile((0, helpers_1.hasNotesExample)(ctx)), "settings registry + client");
16
+ plan.create("apps/api/src/routes/settings.ts", (0, helpers_1.apiFramework)(ctx) === "hono" ? ROUTE_HONO : ROUTE_EXPRESS, "read a resolved setting");
17
+ }
18
+ plan.patchManifest({ platform: { settings: true } });
19
+ plan.nextStep("Run `pnpm migrate`. Defaults live in code; overrides resolve per scope from platform.settings.");
20
+ },
21
+ };
22
+ function registryFile(notes) {
23
+ const def = notes
24
+ ? `export const notesRetentionDays = defineSetting({
25
+ key: "notes.retention_days",
26
+ schema: z.number().int().positive(),
27
+ defaultValue: 30,
28
+ scopes: ["platform", "workspace"],
29
+ })`
30
+ : `export const exampleFlag = defineSetting({
31
+ key: "app.example_flag",
32
+ schema: z.boolean(),
33
+ defaultValue: false,
34
+ scopes: ["platform", "workspace"],
35
+ })`;
36
+ const exportName = notes ? "notesRetentionDays" : "exampleFlag";
37
+ return `import { createSettingsClient, createSettingsRegistry, defineSetting, pgAdapter } from "@obh/settings"
38
+ import { z } from "zod"
39
+ import { pool } from "../db"
40
+
41
+ // Every platform row is scoped to a workspace. Single-tenant apps can leave this.
42
+ export const WORKSPACE = process.env.WORKSPACE_ID ?? "default"
43
+
44
+ // A typed setting: schema + default in code, overridable per scope (most specific
45
+ // wins: user > group > workspace > platform > default).
46
+ ${def}
47
+
48
+ export const registry = createSettingsRegistry([${exportName}])
49
+
50
+ // The client is stateless; each call takes a db/tx handle and a resolution context.
51
+ export const settings = createSettingsClient({ registry })
52
+ export const settingsDb = pgAdapter(pool)
53
+ `;
54
+ }
55
+ const ROUTE_HONO = `import type { Hono } from "hono"
56
+ import { settings, settingsDb, WORKSPACE } from "../platform/settings"
57
+
58
+ export function register(app: Hono): void {
59
+ // Resolve a setting's effective value for the current workspace, falling back
60
+ // through platform default to the definition default.
61
+ app.get("/settings/:key", async (c) => {
62
+ return c.json(await settings.resolve(settingsDb, c.req.param("key"), { workspaceId: WORKSPACE }))
63
+ })
64
+ }
65
+ `;
66
+ const ROUTE_EXPRESS = `import type { Express, Request, Response } from "express"
67
+ import { settings, settingsDb, WORKSPACE } from "../platform/settings"
68
+
69
+ export function register(app: Express): void {
70
+ app.get("/settings/:key", async (req: Request, res: Response) => {
71
+ res.json(await settings.resolve(settingsDb, req.params.key, { workspaceId: WORKSPACE }))
72
+ })
73
+ }
74
+ `;
75
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/capabilities/settings.ts"],"names":[],"mappings":";;;AACA,uCAA8F;AAEjF,QAAA,QAAQ,GAAe;IAClC,IAAI,EAAE,UAAU;IAChB,QAAQ,EAAE,qFAAqF;IAC/F,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;QACxD,oDAAoD;QACpD,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;QACvE,IAAI,CAAC,MAAM,CAAC,kCAAkC,EAAE,IAAA,yBAAe,EAAC,UAAU,CAAC,EAAE,4BAA4B,CAAC,CAAA;QAE1G,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,mCAAmC,EAAE,YAAY,CAAC,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAC,EAAE,4BAA4B,CAAC,CAAA;YAClH,IAAI,CAAC,MAAM,CAAC,iCAAiC,EAAE,IAAA,sBAAY,EAAC,GAAG,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,EAAE,yBAAyB,CAAC,CAAA;QACtI,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QACpD,IAAI,CAAC,QAAQ,CAAC,gGAAgG,CAAC,CAAA;IACjH,CAAC;CACF,CAAA;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,MAAM,GAAG,GAAG,KAAK;QACf,CAAC,CAAC;;;;;GAKH;QACC,CAAC,CAAC;;;;;GAKH,CAAA;IACD,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,aAAa,CAAA;IAC/D,OAAO;;;;;;;;;EASP,GAAG;;kDAE6C,UAAU;;;;;CAK3D,CAAA;AACD,CAAC;AAED,MAAM,UAAU,GAAG;;;;;;;;;;CAUlB,CAAA;AAED,MAAM,aAAa,GAAG;;;;;;;;CAQrB,CAAA"}
@@ -0,0 +1,16 @@
1
+ import type { CapabilityName } from "../types";
2
+ import type { ProjectContext } from "../project/context";
3
+ import type { Plan } from "../project/plan";
4
+ /**
5
+ * A capability installs one OBH platform primitive into a project. It only ever
6
+ * builds up a Plan (which only ever creates files) — never patches shared code.
7
+ */
8
+ export interface Capability {
9
+ name: CapabilityName;
10
+ /** Other capabilities that must be present first. */
11
+ requires?: CapabilityName[];
12
+ /** One-line description for prompts and docs. */
13
+ describe: string;
14
+ apply(ctx: ProjectContext, plan: Plan): void;
15
+ }
16
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/capabilities/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAE3C;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAA;IACpB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAA;IAC3B,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;CAC7C"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/capabilities/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ import type { Capability } from "./types";
2
+ export declare const webhooks: Capability;
3
+ //# sourceMappingURL=webhooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../src/capabilities/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,QAAQ,EAAE,UAmCtB,CAAA"}