@intelligo-dev/cli 1.0.0-beta.1 → 1.0.0-beta.14

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 (223) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +102 -0
  3. package/dist/bin.js +122 -37
  4. package/dist/bin.js.map +1 -1
  5. package/dist/commands/add.d.ts +26 -1
  6. package/dist/commands/add.d.ts.map +1 -1
  7. package/dist/commands/add.js +70 -4
  8. package/dist/commands/add.js.map +1 -1
  9. package/dist/commands/create-flow.d.ts +37 -0
  10. package/dist/commands/create-flow.d.ts.map +1 -0
  11. package/dist/commands/create-flow.js +212 -0
  12. package/dist/commands/create-flow.js.map +1 -0
  13. package/dist/commands/create.d.ts +27 -6
  14. package/dist/commands/create.d.ts.map +1 -1
  15. package/dist/commands/create.js +53 -13
  16. package/dist/commands/create.js.map +1 -1
  17. package/dist/commands/doctor.d.ts +14 -3
  18. package/dist/commands/doctor.d.ts.map +1 -1
  19. package/dist/commands/doctor.js +295 -37
  20. package/dist/commands/doctor.js.map +1 -1
  21. package/dist/commands/migrate-check.d.ts +73 -1
  22. package/dist/commands/migrate-check.d.ts.map +1 -1
  23. package/dist/commands/migrate-check.js +130 -5
  24. package/dist/commands/migrate-check.js.map +1 -1
  25. package/dist/commands/migrate.d.ts +97 -0
  26. package/dist/commands/migrate.d.ts.map +1 -0
  27. package/dist/commands/migrate.js +141 -0
  28. package/dist/commands/migrate.js.map +1 -0
  29. package/dist/commands/sync-messages.d.ts +37 -0
  30. package/dist/commands/sync-messages.d.ts.map +1 -0
  31. package/dist/commands/sync-messages.js +88 -0
  32. package/dist/commands/sync-messages.js.map +1 -0
  33. package/dist/commands/sync-scaffold.d.ts +36 -0
  34. package/dist/commands/sync-scaffold.d.ts.map +1 -0
  35. package/dist/commands/sync-scaffold.js +53 -0
  36. package/dist/commands/sync-scaffold.js.map +1 -0
  37. package/dist/commands/sync.d.ts +103 -0
  38. package/dist/commands/sync.d.ts.map +1 -0
  39. package/dist/commands/sync.js +361 -0
  40. package/dist/commands/sync.js.map +1 -0
  41. package/dist/commands/upgrade-check.d.ts +5 -5
  42. package/dist/commands/upgrade-check.d.ts.map +1 -1
  43. package/dist/commands/upgrade-check.js +22 -6
  44. package/dist/commands/upgrade-check.js.map +1 -1
  45. package/dist/env-files.d.ts +12 -0
  46. package/dist/env-files.d.ts.map +1 -0
  47. package/dist/env-files.js +29 -0
  48. package/dist/env-files.js.map +1 -0
  49. package/dist/index.d.ts +9 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +9 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/manifest.d.ts +36 -15
  54. package/dist/manifest.d.ts.map +1 -1
  55. package/dist/manifest.js +32 -16
  56. package/dist/manifest.js.map +1 -1
  57. package/dist/migrations.d.ts +3 -6
  58. package/dist/migrations.d.ts.map +1 -1
  59. package/dist/migrations.js +3 -6
  60. package/dist/migrations.js.map +1 -1
  61. package/dist/model-catalogue.d.ts +14 -0
  62. package/dist/model-catalogue.d.ts.map +1 -0
  63. package/dist/model-catalogue.js +34 -0
  64. package/dist/model-catalogue.js.map +1 -0
  65. package/dist/module-exports.d.ts +63 -0
  66. package/dist/module-exports.d.ts.map +1 -0
  67. package/dist/module-exports.js +231 -0
  68. package/dist/module-exports.js.map +1 -0
  69. package/dist/registry-bundle.d.ts +43 -0
  70. package/dist/registry-bundle.d.ts.map +1 -0
  71. package/dist/registry-bundle.js +74 -0
  72. package/dist/registry-bundle.js.map +1 -0
  73. package/dist/registry-items.d.ts +84 -0
  74. package/dist/registry-items.d.ts.map +1 -0
  75. package/dist/registry-items.js +187 -0
  76. package/dist/registry-items.js.map +1 -0
  77. package/package.json +36 -10
  78. package/src/bin.ts +316 -0
  79. package/src/commands/add.ts +255 -0
  80. package/src/commands/create-flow.ts +284 -0
  81. package/src/commands/create.ts +149 -0
  82. package/src/commands/doctor.ts +487 -0
  83. package/src/commands/migrate-check.ts +298 -0
  84. package/src/commands/migrate.ts +228 -0
  85. package/src/commands/sync-messages.ts +114 -0
  86. package/src/commands/sync-scaffold.ts +83 -0
  87. package/src/commands/sync.ts +534 -0
  88. package/src/commands/upgrade-check.ts +160 -0
  89. package/src/env-files.ts +31 -0
  90. package/src/index.ts +39 -0
  91. package/src/manifest.ts +147 -0
  92. package/src/migrations-dir.ts +25 -0
  93. package/src/migrations.ts +133 -0
  94. package/src/model-catalogue.ts +35 -0
  95. package/src/module-exports.ts +266 -0
  96. package/src/registry-bundle.ts +103 -0
  97. package/src/registry-items.ts +237 -0
  98. package/templates/admin-page/admin-page.tsx.tpl +2 -3
  99. package/templates/app-scaffold/assistant-route.ts.tpl +2 -2
  100. package/templates/app-scaffold/auth-route.ts.tpl +10 -0
  101. package/templates/app-scaffold/components.json.tpl +6 -2
  102. package/templates/app-scaffold/db-schema.ts.tpl +29 -0
  103. package/templates/app-scaffold/drizzle-journal.json.tpl +5 -0
  104. package/templates/app-scaffold/drizzle.config.ts.tpl +45 -0
  105. package/templates/app-scaffold/env.example.tpl +65 -1
  106. package/templates/app-scaffold/globals.css.tpl +172 -281
  107. package/templates/app-scaffold/i18n-request.ts.tpl +25 -0
  108. package/templates/app-scaffold/instrumentation.ts.tpl +21 -0
  109. package/templates/app-scaffold/intelligo.ts.tpl +95 -41
  110. package/templates/app-scaffold/layout.tsx.tpl +18 -1
  111. package/templates/app-scaffold/lib-utils.ts.tpl +1 -6
  112. package/templates/app-scaffold/next.config.mjs.tpl +2 -0
  113. package/templates/app-scaffold/package.json.tpl +31 -17
  114. package/templates/app-scaffold/page.tsx.tpl +16 -0
  115. package/templates/app-scaffold/plans.ts.tpl +32 -13
  116. package/templates/app-scaffold/{middleware.ts.tpl → proxy.ts.tpl} +6 -0
  117. package/templates/app-scaffold/sonner.tsx.tpl +42 -20
  118. package/templates/app-scaffold/stripe-webhook-route.ts.tpl +24 -0
  119. package/templates/app-scaffold/theme-provider.tsx.tpl +6 -4
  120. package/templates/app-scaffold/tsconfig.json.tpl +20 -4
  121. package/templates/app-scaffold/use-mobile.ts.tpl +19 -0
  122. package/templates/app-scaffold/workspace-bootstrap.ts.tpl +30 -0
  123. package/templates/maintenance/maintenance-route.ts.tpl +119 -0
  124. package/templates/manifest.json +75 -17
  125. package/templates/registry/ai-agent-activity.json +23 -0
  126. package/templates/registry/ai-agent-progress.json +21 -0
  127. package/templates/registry/ai-approval-card.json +27 -0
  128. package/templates/registry/ai-artifact.json +22 -0
  129. package/templates/registry/ai-branch.json +21 -0
  130. package/templates/registry/ai-citations.json +24 -0
  131. package/templates/registry/ai-code-block.json +23 -0
  132. package/templates/registry/ai-composer-menu.json +19 -0
  133. package/templates/registry/ai-file-diff.json +25 -0
  134. package/templates/registry/ai-image-generation.json +23 -0
  135. package/templates/registry/ai-markdown.json +23 -0
  136. package/templates/registry/ai-message-bubble.json +23 -0
  137. package/templates/registry/ai-message-scroller.json +22 -0
  138. package/templates/registry/ai-message.json +21 -0
  139. package/templates/registry/ai-motion.json +19 -0
  140. package/templates/registry/ai-prompt-input.json +28 -0
  141. package/templates/registry/ai-reasoning-text.json +22 -0
  142. package/templates/registry/ai-reasoning.json +22 -0
  143. package/templates/registry/ai-shimmer-text.json +19 -0
  144. package/templates/registry/ai-sidebar.json +25 -0
  145. package/templates/registry/ai-speech-input.json +21 -0
  146. package/templates/registry/ai-streaming-response.json +24 -0
  147. package/templates/registry/ai-suggestion.json +19 -0
  148. package/templates/registry/ai-todo-list.json +22 -0
  149. package/templates/registry/ai-tool-approval.json +26 -0
  150. package/templates/registry/ai-tool-result.json +25 -0
  151. package/templates/registry/alert-dialog.json +23 -0
  152. package/templates/registry/animated-list.json +21 -0
  153. package/templates/registry/app-shell.json +93 -0
  154. package/templates/registry/artifacts.json +82 -0
  155. package/templates/registry/attachment.json +22 -0
  156. package/templates/registry/auth-email-verification.json +39 -0
  157. package/templates/registry/auth-login.json +72 -0
  158. package/templates/registry/auth-password-reset.json +53 -0
  159. package/templates/registry/auth-signup.json +41 -0
  160. package/templates/registry/billing-settings.json +60 -0
  161. package/templates/registry/button.json +22 -0
  162. package/templates/registry/chat-eve.json +19 -0
  163. package/templates/registry/chat-panel.json +30 -0
  164. package/templates/registry/chat-share.json +42 -0
  165. package/templates/registry/chat-widget.json +34 -0
  166. package/templates/registry/chat.json +326 -0
  167. package/templates/registry/checkbox.json +22 -0
  168. package/templates/registry/checkout.json +40 -0
  169. package/templates/registry/collapsible.json +19 -0
  170. package/templates/registry/command.json +23 -0
  171. package/templates/registry/copy-button.json +21 -0
  172. package/templates/registry/dashboard.json +69 -0
  173. package/templates/registry/dialog.json +24 -0
  174. package/templates/registry/document-viewer.json +22 -0
  175. package/templates/registry/dropdown-menu.json +23 -0
  176. package/templates/registry/expandable-tabs.json +22 -0
  177. package/templates/registry/feature-gating.json +73 -0
  178. package/templates/registry/file-upload.json +24 -0
  179. package/templates/registry/hold-action-button.json +22 -0
  180. package/templates/registry/input-group.json +23 -0
  181. package/templates/registry/input.json +19 -0
  182. package/templates/registry/intelligo.json +187 -0
  183. package/templates/registry/invitation-accept.json +64 -0
  184. package/templates/registry/language-switcher.json +29 -0
  185. package/templates/registry/morphing-modal.json +24 -0
  186. package/templates/registry/notification-stack.json +24 -0
  187. package/templates/registry/notifications.json +87 -0
  188. package/templates/registry/onboarding.json +83 -0
  189. package/templates/registry/otp-input.json +22 -0
  190. package/templates/registry/page-header.json +15 -0
  191. package/templates/registry/payment-poll.json +52 -0
  192. package/templates/registry/popover-morph.json +22 -0
  193. package/templates/registry/popover.json +22 -0
  194. package/templates/registry/pricing.json +111 -0
  195. package/templates/registry/privacy-settings.json +65 -0
  196. package/templates/registry/profile-settings.json +68 -0
  197. package/templates/registry/progress.json +19 -0
  198. package/templates/registry/radio-group.json +22 -0
  199. package/templates/registry/registry.json +2945 -0
  200. package/templates/registry/route-error.json +65 -0
  201. package/templates/registry/select-morph.json +23 -0
  202. package/templates/registry/select.json +23 -0
  203. package/templates/registry/settings-shell.json +47 -0
  204. package/templates/registry/sheet.json +24 -0
  205. package/templates/registry/sidebar.json +28 -0
  206. package/templates/registry/smoke.json +34 -0
  207. package/templates/registry/spinner.json +16 -0
  208. package/templates/registry/stat-card.json +18 -0
  209. package/templates/registry/status-badge.json +18 -0
  210. package/templates/registry/switch.json +20 -0
  211. package/templates/registry/tabs.json +23 -0
  212. package/templates/registry/team-settings.json +97 -0
  213. package/templates/registry/textarea.json +16 -0
  214. package/templates/registry/tooltip.json +22 -0
  215. package/templates/registry/trial-banner.json +43 -0
  216. package/templates/registry/usage.json +84 -0
  217. package/templates/registry/workspace-settings.json +71 -0
  218. package/templates/registry-items.json +116 -0
  219. package/templates/registry-requires.json +180 -0
  220. package/templates/usage-page/usage-page.tsx.tpl +23 -8
  221. package/templates/vitest/server-only.ts.tpl +7 -0
  222. package/templates/vitest/vitest.config.ts.tpl +37 -0
  223. package/templates/billing-page/billing-page.tsx.tpl +0 -72
@@ -0,0 +1,160 @@
1
+ /**
2
+ * `intelligo upgrade --check`
3
+ *
4
+ * Reports what a template upgrade would do, and does nothing. Upgrades
5
+ * never overwrite consumer source, so the interesting output is not
6
+ * "these templates changed" but "these changed AND you have edited
7
+ * them" — the set where the consumer has to make a decision.
8
+ */
9
+
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import path from "node:path";
12
+
13
+ import { readCatalogue, substitute } from "./add.js";
14
+ import { hashContents, readManifest, type Manifest } from "../manifest.js";
15
+
16
+ export type UpgradeItem = {
17
+ feature: string;
18
+ path: string;
19
+ /**
20
+ * - `current` — generated file matches the current template
21
+ * - `outdated` — template changed; the local copy is untouched, so
22
+ * re-generating is safe
23
+ * - `conflict` — template changed AND the consumer edited the file
24
+ * - `customized` — consumer edited it; template unchanged
25
+ * - `deleted` — consumer removed it
26
+ * - `new` — the template has a file this app was never given
27
+ */
28
+ state: "current" | "outdated" | "conflict" | "customized" | "deleted" | "new";
29
+ };
30
+
31
+ export type UpgradeReport = {
32
+ installedVersion: Record<string, string>;
33
+ templateVersion: Record<string, string>;
34
+ items: UpgradeItem[];
35
+ };
36
+
37
+ export type UpgradeCheckOptions = {
38
+ appRoot: string;
39
+ templatesDir: string;
40
+ };
41
+
42
+ export function upgradeCheck(options: UpgradeCheckOptions): UpgradeReport {
43
+ const manifest: Manifest | null = readManifest(options.appRoot);
44
+ if (!manifest) {
45
+ return { installedVersion: {}, templateVersion: {}, items: [] };
46
+ }
47
+
48
+ const catalogue = readCatalogue(options.templatesDir);
49
+ const report: UpgradeReport = {
50
+ installedVersion: {},
51
+ templateVersion: {},
52
+ items: [],
53
+ };
54
+
55
+ for (const [feature, entry] of Object.entries(manifest.features)) {
56
+ const spec = catalogue[feature];
57
+ report.installedVersion[feature] = entry.templateVersion;
58
+ if (spec) report.templateVersion[feature] = spec.templateVersion;
59
+
60
+ const targetToTemplate = new Map(
61
+ (spec?.files ?? []).map((f) => [f.target, f.template])
62
+ );
63
+
64
+ for (const target of newFiles(spec, [
65
+ ...entry.files,
66
+ ...(entry.handedOver ?? []).map((handed) => ({ path: handed })),
67
+ ])) {
68
+ report.items.push({ feature, path: target, state: "new" });
69
+ }
70
+
71
+ for (const file of entry.files) {
72
+ const abs = path.join(options.appRoot, file.path);
73
+ if (!existsSync(abs)) {
74
+ report.items.push({ feature, path: file.path, state: "deleted" });
75
+ continue;
76
+ }
77
+
78
+ const localHash = hashContents(readFileSync(abs, "utf8"));
79
+ const customized = localHash !== file.hash;
80
+
81
+ // Compare against the template as it would be written for THIS
82
+ // app: the recorded hash is of substituted content, so the raw
83
+ // template never matches a file carrying a placeholder.
84
+ const templateRel = targetToTemplate.get(file.path);
85
+ let templateChanged = false;
86
+ if (templateRel) {
87
+ const templateHash = hashContents(
88
+ substitute(
89
+ readFileSync(path.join(options.templatesDir, templateRel), "utf8"),
90
+ entry.variables
91
+ )
92
+ );
93
+ templateChanged = templateHash !== file.hash;
94
+ }
95
+
96
+ report.items.push({
97
+ feature,
98
+ path: file.path,
99
+ state: templateChanged
100
+ ? customized
101
+ ? "conflict"
102
+ : "outdated"
103
+ : customized
104
+ ? "customized"
105
+ : "current",
106
+ });
107
+ }
108
+ }
109
+
110
+ return report;
111
+ }
112
+
113
+ /** The feature's files the manifest has no record of: added to the template since. */
114
+ function newFiles(
115
+ spec: { files: { target: string }[] } | undefined,
116
+ recorded: { path: string }[]
117
+ ): string[] {
118
+ const known = new Set(recorded.map((file) => file.path));
119
+ return (spec?.files ?? [])
120
+ .map((file) => file.target)
121
+ .filter((target) => !known.has(target));
122
+ }
123
+
124
+ export function formatUpgradeReport(r: UpgradeReport): string {
125
+ if (r.items.length === 0) {
126
+ return "No generated files recorded — nothing to upgrade.";
127
+ }
128
+
129
+ const label: Record<UpgradeItem["state"], string> = {
130
+ current: "✓ up to date",
131
+ outdated: "↑ template changed — safe to re-run `intelligo add`",
132
+ conflict: "! template changed AND you edited it — review the diff",
133
+ customized: "= yours (template unchanged)",
134
+ deleted: "✗ removed by you",
135
+ new: "+ new in the template — `intelligo add` writes it",
136
+ };
137
+
138
+ const lines: string[] = [];
139
+ for (const [feature, installed] of Object.entries(r.installedVersion)) {
140
+ const latest = r.templateVersion[feature];
141
+ lines.push(
142
+ latest && latest !== installed
143
+ ? `${feature}: ${installed} → ${latest}`
144
+ : `${feature}: ${installed}`
145
+ );
146
+ for (const item of r.items.filter((i) => i.feature === feature)) {
147
+ lines.push(` ${label[item.state]} ${item.path}`);
148
+ }
149
+ }
150
+ return lines.join("\n");
151
+ }
152
+
153
+ /**
154
+ * Non-zero only on conflicts. An outdated-but-untouched file is not a
155
+ * problem to fail a build over; a file that changed on both sides is a
156
+ * decision someone has to make.
157
+ */
158
+ export function upgradeCheckExitCode(r: UpgradeReport): number {
159
+ return r.items.some((i) => i.state === "conflict") ? 1 : 0;
160
+ }
@@ -0,0 +1,31 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { parseEnv } from "node:util";
4
+
5
+ /**
6
+ * The env files a Next.js app reads, in the order it reads them: an
7
+ * earlier file wins, and a variable already set in the shell wins over
8
+ * every file.
9
+ *
10
+ * `migrate` and `doctor` run outside Next, so without this a fresh app
11
+ * whose `DATABASE_URL` lives in `.env.local` — exactly what the
12
+ * quickstart says to do — would fail with "DATABASE_URL is required".
13
+ */
14
+ export const ENV_FILES = [".env.local", ".env"] as const;
15
+
16
+ export function loadAppEnv(
17
+ root: string,
18
+ env: NodeJS.ProcessEnv = process.env
19
+ ): string[] {
20
+ const loaded: string[] = [];
21
+ for (const name of ENV_FILES) {
22
+ const file = path.join(root, name);
23
+ if (!existsSync(file)) continue;
24
+ const values = parseEnv(readFileSync(file, "utf8"));
25
+ for (const [key, value] of Object.entries(values)) {
26
+ if (env[key] === undefined) env[key] = value;
27
+ }
28
+ loaded.push(name);
29
+ }
30
+ return loaded;
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @intelligo-dev/cli — operational commands for an Intelligo application.
3
+ *
4
+ * Exported as functions as well as a binary so CI can call the checks
5
+ * directly (see the migration-chain gate) without shelling out.
6
+ */
7
+
8
+ export {
9
+ runChecks,
10
+ formatResults,
11
+ exitCodeFor,
12
+ type CheckResult,
13
+ type DoctorOptions,
14
+ } from "./commands/doctor.js";
15
+
16
+ export {
17
+ migrateCheck,
18
+ hashMigration,
19
+ formatMigrateCheck,
20
+ migrateCheckExitCode,
21
+ type MigrateCheckResult,
22
+ } from "./commands/migrate-check.js";
23
+
24
+ export {
25
+ readMigrationChain,
26
+ inspectMigrationChain,
27
+ type MigrationChain,
28
+ type ChainProblem,
29
+ } from "./migrations.js";
30
+
31
+ /**
32
+ * Where the framework's chain lives. Exported because a consumer's own
33
+ * operational scripts — baselining a push-provisioned database, a
34
+ * one-off audit — otherwise hardcode
35
+ * `node_modules/@intelligo-dev/core/src/db/migrations`, which is right
36
+ * in an application and wrong in the framework repository, and which
37
+ * silently stops being either if the package layout ever changes.
38
+ */
39
+ export { resolveMigrationsDir, MIGRATION_LOCATIONS } from "./migrations-dir.js";
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Generated-source manifest.
3
+ *
4
+ * Intelligo generates starting source into the consumer's repository
5
+ * and then stops owning it. That promise needs a mechanism,
6
+ * because "never overwrite a customized file" requires knowing which
7
+ * files were generated, from which template version, and whether the
8
+ * consumer has since edited them.
9
+ *
10
+ * The manifest records a content hash per generated file at the moment
11
+ * it was written. A file whose hash still matches was not touched and
12
+ * can be re-generated safely; one whose hash differs is the consumer's
13
+ * code now, and an upgrade may only show them a diff.
14
+ *
15
+ * Deliberately not a lockfile of package versions: it tracks template
16
+ * provenance, not dependencies.
17
+ */
18
+
19
+ import { createHash } from "node:crypto";
20
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
21
+ import path from "node:path";
22
+
23
+ export const MANIFEST_FILE = "intelligo.manifest.json";
24
+
25
+ export type GeneratedFile = {
26
+ /** Path relative to the consumer application root. */
27
+ path: string;
28
+ /** sha256 of the contents Intelligo wrote. */
29
+ hash: string;
30
+ };
31
+
32
+ export type FeatureEntry = {
33
+ templateVersion: string;
34
+ files: GeneratedFile[];
35
+ /**
36
+ * Placeholder substitutions the files were generated with
37
+ * (`__APP_NAME__` → "acme"). Recorded so an upgrade check can hash the
38
+ * template *as it would be written for this app*; the raw template
39
+ * would make every substituted file look permanently outdated.
40
+ */
41
+ variables?: Record<string, string>;
42
+ /**
43
+ * Files this feature wrote that a registry install has since replaced
44
+ * (the scaffold's globals.css once the design-system base lands):
45
+ * `intelligo sync` owns them now, so `upgrade --check` neither
46
+ * compares them nor offers them as new, and `add` does not write them
47
+ * back.
48
+ */
49
+ handedOver?: string[];
50
+ };
51
+
52
+ /**
53
+ * Registry items `intelligo sync` installed: which items the app keeps
54
+ * in sync, the framework version they came from, and — per installed
55
+ * file that is not a seam or a message file — the hash of the file as
56
+ * written (after shadcn's normalisation, see `asInstalled`). A file
57
+ * whose hash still matches was not edited since, so a newer registry
58
+ * may replace it; one that differs was edited by hand.
59
+ */
60
+ export type RegistryEntry = {
61
+ version: string;
62
+ items: string[];
63
+ files: Record<string, string>;
64
+ };
65
+
66
+ export type Manifest = {
67
+ schemaVersion: 1;
68
+ frameworkVersion: string;
69
+ features: Record<string, FeatureEntry>;
70
+ registry?: RegistryEntry;
71
+ };
72
+
73
+ export function hashContents(contents: string): string {
74
+ return createHash("sha256").update(contents).digest("hex");
75
+ }
76
+
77
+ export function emptyManifest(frameworkVersion: string): Manifest {
78
+ return { schemaVersion: 1, frameworkVersion, features: {} };
79
+ }
80
+
81
+ export function readManifest(appRoot: string): Manifest | null {
82
+ const file = path.join(appRoot, MANIFEST_FILE);
83
+ if (!existsSync(file)) return null;
84
+ return JSON.parse(readFileSync(file, "utf8")) as Manifest;
85
+ }
86
+
87
+ export function writeManifest(appRoot: string, manifest: Manifest): void {
88
+ writeFileSync(
89
+ path.join(appRoot, MANIFEST_FILE),
90
+ `${JSON.stringify(manifest, null, 2)}\n`
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Record a feature's generated files, replacing any previous entry for
96
+ * the same feature — re-generating is how a consumer accepts a new
97
+ * template version, and the recorded hashes must then describe what is
98
+ * actually on disk.
99
+ */
100
+ export function recordFeature(
101
+ manifest: Manifest,
102
+ feature: string,
103
+ templateVersion: string,
104
+ files: GeneratedFile[],
105
+ variables?: Record<string, string>
106
+ ): Manifest {
107
+ const handedOver = manifest.features[feature]?.handedOver;
108
+ return {
109
+ ...manifest,
110
+ features: {
111
+ ...manifest.features,
112
+ [feature]: {
113
+ templateVersion,
114
+ files,
115
+ ...(variables ? { variables } : {}),
116
+ ...(handedOver?.length ? { handedOver } : {}),
117
+ },
118
+ },
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Move `paths` out of a feature's generated files into its `handedOver`
124
+ * list. A no-op for a feature the manifest does not record.
125
+ */
126
+ export function handOver(
127
+ manifest: Manifest,
128
+ feature: string,
129
+ paths: readonly string[]
130
+ ): Manifest {
131
+ const entry = manifest.features[feature];
132
+ if (!entry || paths.length === 0) return manifest;
133
+ const moved = new Set(paths);
134
+ return {
135
+ ...manifest,
136
+ features: {
137
+ ...manifest.features,
138
+ [feature]: {
139
+ ...entry,
140
+ files: entry.files.filter((f) => !moved.has(f.path)),
141
+ handedOver: [
142
+ ...new Set([...(entry.handedOver ?? []), ...paths]),
143
+ ].sort(),
144
+ },
145
+ },
146
+ };
147
+ }
@@ -0,0 +1,25 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Where the framework's migration chain lives, relative to the
6
+ * directory the CLI is run from.
7
+ *
8
+ * Two layouts are real: the framework repository itself, where core is
9
+ * a workspace package, and a consumer application, where core arrives
10
+ * from the registry and ships its migrations inside the package (they
11
+ * are in `files`, next to `dist`). The first match wins, so a checkout
12
+ * that has both — the framework repo — reads its own source.
13
+ */
14
+ export const MIGRATION_LOCATIONS = [
15
+ "packages/core/src/db/migrations",
16
+ "node_modules/@intelligo-dev/core/src/db/migrations",
17
+ ] as const;
18
+
19
+ export function resolveMigrationsDir(root: string): string | null {
20
+ for (const relative of MIGRATION_LOCATIONS) {
21
+ const dir = path.join(root, relative);
22
+ if (existsSync(dir)) return dir;
23
+ }
24
+ return null;
25
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Migration-chain inspection.
3
+ *
4
+ * Drizzle keeps two sources of truth: the `.sql` files on disk and
5
+ * `meta/_journal.json`, which is what `drizzle-kit migrate` actually
6
+ * reads. Nothing keeps them in step: a file missing from the journal is
7
+ * silently skipped by `migrate`, while `drizzle-kit push` from
8
+ * `schema.ts` hides the gap.
9
+ *
10
+ * The drift is invisible until someone provisions a database from the
11
+ * migrations, at which point it is a production incident rather than a
12
+ * CI failure. These helpers make it a check.
13
+ */
14
+
15
+ import { readFileSync, readdirSync } from "node:fs";
16
+ import path from "node:path";
17
+
18
+ export type JournalEntry = {
19
+ idx: number;
20
+ version: string;
21
+ when: number;
22
+ tag: string;
23
+ breakpoints: boolean;
24
+ };
25
+
26
+ export type MigrationChain = {
27
+ dir: string;
28
+ /** `.sql` files present on disk, sorted by their numeric prefix. */
29
+ files: string[];
30
+ /** Tags drizzle-kit will actually apply, in order. */
31
+ journalTags: string[];
32
+ /** On disk but absent from the journal — `migrate` skips these. */
33
+ unregistered: string[];
34
+ /** In the journal but missing from disk — `migrate` fails on these. */
35
+ missing: string[];
36
+ };
37
+
38
+ function tagOf(file: string): string {
39
+ return file.replace(/\.sql$/, "");
40
+ }
41
+
42
+ export function readMigrationChain(dir: string): MigrationChain {
43
+ const files = readdirSync(dir)
44
+ .filter((f) => f.endsWith(".sql"))
45
+ .sort();
46
+
47
+ let journalTags: string[] = [];
48
+ try {
49
+ const journal = JSON.parse(
50
+ readFileSync(path.join(dir, "meta", "_journal.json"), "utf8")
51
+ ) as { entries?: JournalEntry[] };
52
+ journalTags = (journal.entries ?? []).map((e) => e.tag);
53
+ } catch {
54
+ // No journal at all is itself the maximal drift: every file is
55
+ // unregistered, which the caller reports.
56
+ journalTags = [];
57
+ }
58
+
59
+ const registered = new Set(journalTags);
60
+ const onDisk = new Set(files.map(tagOf));
61
+
62
+ return {
63
+ dir,
64
+ files,
65
+ journalTags,
66
+ unregistered: files.map(tagOf).filter((t) => !registered.has(t)),
67
+ missing: journalTags.filter((t) => !onDisk.has(t)),
68
+ };
69
+ }
70
+
71
+ export type ChainProblem = { level: "error" | "warn"; message: string };
72
+
73
+ export function inspectMigrationChain(chain: MigrationChain): ChainProblem[] {
74
+ const problems: ChainProblem[] = [];
75
+
76
+ if (chain.files.length === 0) {
77
+ problems.push({ level: "error", message: `No .sql files in ${chain.dir}` });
78
+ return problems;
79
+ }
80
+
81
+ if (chain.missing.length > 0) {
82
+ problems.push({
83
+ level: "error",
84
+ message:
85
+ `Journal references ${chain.missing.length} migration(s) that are not on disk ` +
86
+ `(drizzle-kit migrate will fail): ${chain.missing.join(", ")}`,
87
+ });
88
+ }
89
+
90
+ if (chain.unregistered.length > 0) {
91
+ problems.push({
92
+ level: "error",
93
+ message:
94
+ `${chain.unregistered.length} of ${chain.files.length} migration files are not in ` +
95
+ `meta/_journal.json, so drizzle-kit migrate skips them: ` +
96
+ `${chain.unregistered.slice(0, 5).join(", ")}` +
97
+ (chain.unregistered.length > 5 ? ", …" : ""),
98
+ });
99
+ }
100
+
101
+ // Numbering problems. Two files sharing a prefix is worse than a
102
+ // gap: `migrate` orders by tag, so which one runs first depends on
103
+ // the rest of the filename rather than on intent.
104
+ const numbers = chain.files
105
+ .map((f) => Number.parseInt(f.slice(0, 4), 10))
106
+ .filter((n) => Number.isFinite(n));
107
+
108
+ const seen = new Map<number, number>();
109
+ for (const n of numbers) seen.set(n, (seen.get(n) ?? 0) + 1);
110
+ const duplicates = [...seen.entries()]
111
+ .filter(([, count]) => count > 1)
112
+ .map(([n]) => String(n).padStart(4, "0"));
113
+ if (duplicates.length > 0) {
114
+ problems.push({
115
+ level: "warn",
116
+ message: `Duplicate migration prefixes (apply order is filename-dependent): ${duplicates.join(", ")}`,
117
+ });
118
+ }
119
+
120
+ const unique = [...new Set(numbers)].sort((a, b) => a - b);
121
+ for (let i = 1; i < unique.length; i++) {
122
+ const prev = unique[i - 1]!;
123
+ const cur = unique[i]!;
124
+ if (cur > prev + 1) {
125
+ problems.push({
126
+ level: "warn",
127
+ message: `Gap in migration numbering: ${String(prev).padStart(4, "0")} → ${String(cur).padStart(4, "0")}`,
128
+ });
129
+ }
130
+ }
131
+
132
+ return problems;
133
+ }
@@ -0,0 +1,35 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Where the shipped model catalogue (`DEFAULT_MODELS`) can be read from,
6
+ * relative to the directory the CLI is run from: the framework
7
+ * repository's own source, then the installed package's source, then
8
+ * its build. The CLI reads the file rather than importing the package,
9
+ * so `doctor` works in an app whose dependencies do not even load.
10
+ */
11
+ export const MODEL_CATALOGUE_LOCATIONS = [
12
+ "packages/executions/src/pricing.ts",
13
+ "node_modules/@intelligo-dev/executions/src/pricing.ts",
14
+ "node_modules/@intelligo-dev/executions/dist/pricing.js",
15
+ ] as const;
16
+
17
+ /**
18
+ * The ids `DEFAULT_MODELS` prices, or null when no catalogue file is
19
+ * found or the declaration is not in it.
20
+ */
21
+ export function readCatalogueModelIds(root: string): Set<string> | null {
22
+ for (const relative of MODEL_CATALOGUE_LOCATIONS) {
23
+ const file = path.join(root, relative);
24
+ if (!existsSync(file)) continue;
25
+ const text = readFileSync(file, "utf8");
26
+ const start = text.indexOf("export const DEFAULT_MODELS");
27
+ if (start === -1) continue;
28
+ return new Set(
29
+ [
30
+ ...text.slice(start).matchAll(/\bid:\s*"([a-z0-9-]+\/[a-z0-9._-]+)"/g),
31
+ ].map((m) => m[1]!)
32
+ );
33
+ }
34
+ return null;
35
+ }