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

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 (103) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +63 -0
  3. package/dist/bin.js +85 -37
  4. package/dist/bin.js.map +1 -1
  5. package/dist/commands/add.d.ts +22 -1
  6. package/dist/commands/add.d.ts.map +1 -1
  7. package/dist/commands/add.js +61 -4
  8. package/dist/commands/add.js.map +1 -1
  9. package/dist/commands/create-flow.d.ts +34 -0
  10. package/dist/commands/create-flow.d.ts.map +1 -0
  11. package/dist/commands/create-flow.js +180 -0
  12. package/dist/commands/create-flow.js.map +1 -0
  13. package/dist/commands/create.d.ts +23 -6
  14. package/dist/commands/create.d.ts.map +1 -1
  15. package/dist/commands/create.js +45 -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 +293 -37
  20. package/dist/commands/doctor.js.map +1 -1
  21. package/dist/commands/migrate-check.d.ts +54 -1
  22. package/dist/commands/migrate-check.d.ts.map +1 -1
  23. package/dist/commands/migrate-check.js +101 -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 +147 -0
  28. package/dist/commands/migrate.js.map +1 -0
  29. package/dist/commands/upgrade-check.d.ts +5 -5
  30. package/dist/commands/upgrade-check.d.ts.map +1 -1
  31. package/dist/commands/upgrade-check.js +19 -6
  32. package/dist/commands/upgrade-check.js.map +1 -1
  33. package/dist/env-files.d.ts +12 -0
  34. package/dist/env-files.d.ts.map +1 -0
  35. package/dist/env-files.js +29 -0
  36. package/dist/env-files.js.map +1 -0
  37. package/dist/index.d.ts +9 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +9 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/manifest.d.ts +9 -15
  42. package/dist/manifest.d.ts.map +1 -1
  43. package/dist/manifest.js +5 -16
  44. package/dist/manifest.js.map +1 -1
  45. package/dist/migrations.d.ts +3 -6
  46. package/dist/migrations.d.ts.map +1 -1
  47. package/dist/migrations.js +3 -6
  48. package/dist/migrations.js.map +1 -1
  49. package/dist/model-catalogue.d.ts +14 -0
  50. package/dist/model-catalogue.d.ts.map +1 -0
  51. package/dist/model-catalogue.js +34 -0
  52. package/dist/model-catalogue.js.map +1 -0
  53. package/dist/registry-items.d.ts +56 -0
  54. package/dist/registry-items.d.ts.map +1 -0
  55. package/dist/registry-items.js +103 -0
  56. package/dist/registry-items.js.map +1 -0
  57. package/package.json +36 -10
  58. package/src/bin.ts +264 -0
  59. package/src/commands/add.ts +244 -0
  60. package/src/commands/create-flow.ts +228 -0
  61. package/src/commands/create.ts +139 -0
  62. package/src/commands/doctor.ts +487 -0
  63. package/src/commands/migrate-check.ts +257 -0
  64. package/src/commands/migrate.ts +234 -0
  65. package/src/commands/upgrade-check.ts +157 -0
  66. package/src/env-files.ts +31 -0
  67. package/src/index.ts +39 -0
  68. package/src/manifest.ts +93 -0
  69. package/src/migrations-dir.ts +25 -0
  70. package/src/migrations.ts +133 -0
  71. package/src/model-catalogue.ts +35 -0
  72. package/src/registry-items.ts +132 -0
  73. package/templates/admin-page/admin-page.tsx.tpl +2 -3
  74. package/templates/app-scaffold/assistant-route.ts.tpl +2 -2
  75. package/templates/app-scaffold/auth-route.ts.tpl +10 -0
  76. package/templates/app-scaffold/components.json.tpl +6 -2
  77. package/templates/app-scaffold/db-schema.ts.tpl +29 -0
  78. package/templates/app-scaffold/drizzle-journal.json.tpl +5 -0
  79. package/templates/app-scaffold/drizzle.config.ts.tpl +45 -0
  80. package/templates/app-scaffold/env.example.tpl +65 -1
  81. package/templates/app-scaffold/globals.css.tpl +172 -281
  82. package/templates/app-scaffold/i18n-request.ts.tpl +25 -0
  83. package/templates/app-scaffold/instrumentation.ts.tpl +21 -0
  84. package/templates/app-scaffold/intelligo.ts.tpl +104 -9
  85. package/templates/app-scaffold/layout.tsx.tpl +18 -1
  86. package/templates/app-scaffold/lib-utils.ts.tpl +1 -6
  87. package/templates/app-scaffold/next.config.mjs.tpl +2 -0
  88. package/templates/app-scaffold/package.json.tpl +31 -17
  89. package/templates/app-scaffold/page.tsx.tpl +16 -0
  90. package/templates/app-scaffold/plans.ts.tpl +32 -13
  91. package/templates/app-scaffold/{middleware.ts.tpl → proxy.ts.tpl} +6 -0
  92. package/templates/app-scaffold/sonner.tsx.tpl +42 -20
  93. package/templates/app-scaffold/stripe-webhook-route.ts.tpl +24 -0
  94. package/templates/app-scaffold/theme-provider.tsx.tpl +6 -4
  95. package/templates/app-scaffold/tsconfig.json.tpl +20 -4
  96. package/templates/app-scaffold/use-mobile.ts.tpl +19 -0
  97. package/templates/app-scaffold/workspace-bootstrap.ts.tpl +30 -0
  98. package/templates/maintenance/maintenance-route.ts.tpl +119 -0
  99. package/templates/manifest.json +57 -18
  100. package/templates/registry-items.json +116 -0
  101. package/templates/registry-requires.json +156 -0
  102. package/templates/usage-page/usage-page.tsx.tpl +23 -8
  103. package/templates/billing-page/billing-page.tsx.tpl +0 -72
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,93 @@
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
+
44
+ export type Manifest = {
45
+ schemaVersion: 1;
46
+ frameworkVersion: string;
47
+ features: Record<string, FeatureEntry>;
48
+ };
49
+
50
+ export function hashContents(contents: string): string {
51
+ return createHash("sha256").update(contents).digest("hex");
52
+ }
53
+
54
+ export function emptyManifest(frameworkVersion: string): Manifest {
55
+ return { schemaVersion: 1, frameworkVersion, features: {} };
56
+ }
57
+
58
+ export function readManifest(appRoot: string): Manifest | null {
59
+ const file = path.join(appRoot, MANIFEST_FILE);
60
+ if (!existsSync(file)) return null;
61
+ return JSON.parse(readFileSync(file, "utf8")) as Manifest;
62
+ }
63
+
64
+ export function writeManifest(appRoot: string, manifest: Manifest): void {
65
+ writeFileSync(
66
+ path.join(appRoot, MANIFEST_FILE),
67
+ `${JSON.stringify(manifest, null, 2)}\n`
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Record a feature's generated files, replacing any previous entry for
73
+ * the same feature — re-generating is how a consumer accepts a new
74
+ * template version, and the recorded hashes must then describe what is
75
+ * actually on disk.
76
+ */
77
+ export function recordFeature(
78
+ manifest: Manifest,
79
+ feature: string,
80
+ templateVersion: string,
81
+ files: GeneratedFile[],
82
+ variables?: Record<string, string>
83
+ ): Manifest {
84
+ return {
85
+ ...manifest,
86
+ features: {
87
+ ...manifest.features,
88
+ [feature]: variables
89
+ ? { templateVersion, files, variables }
90
+ : { templateVersion, files },
91
+ },
92
+ };
93
+ }
@@ -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
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The registry's pages as `intelligo create` offers them: what each one
3
+ * is, what else it needs installed first, and the commands that install
4
+ * a selection into a fresh app.
5
+ *
6
+ * Installing is the shadcn CLI's job, not this package's — the items
7
+ * are shadcn-schema JSON served at intelligo.dev/r, and the scaffold's
8
+ * components.json already names that registry as `@intelligo`. What the
9
+ * CLI adds is the order: an item that imports a sibling's files
10
+ * (requires.json `items`) has to land after it.
11
+ */
12
+
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import path from "node:path";
15
+
16
+ import type { RegistryRequires } from "./commands/doctor.js";
17
+
18
+ export type RegistryItem = { title: string; description: string };
19
+ export type RegistryCatalogue = {
20
+ items: Record<string, RegistryItem>;
21
+ requires: RegistryRequires;
22
+ };
23
+
24
+ /** Both files are build-time copies from packages/registry (see scripts/). */
25
+ export function readRegistryCatalogue(templatesDir: string): RegistryCatalogue {
26
+ const read = (file: string) =>
27
+ JSON.parse(readFileSync(path.join(templatesDir, file), "utf8"));
28
+ return {
29
+ items: (
30
+ read("registry-items.json") as { items: Record<string, RegistryItem> }
31
+ ).items,
32
+ requires: read("registry-requires.json") as RegistryRequires,
33
+ };
34
+ }
35
+
36
+ /** The description's opening clause — short enough for one picker line. */
37
+ export function shortDescription(description: string, max = 64): string {
38
+ const head = description.split(/[:.]\s|\s—\s/)[0]!.trim();
39
+ return head.length <= max ? head : `${head.slice(0, max - 1).trimEnd()}…`;
40
+ }
41
+
42
+ /**
43
+ * The selection plus everything it depends on, dependencies first.
44
+ * Unknown names throw rather than being dropped: a typo in `--items`
45
+ * should not quietly install less than was asked for.
46
+ */
47
+ export function withDependencies(
48
+ selected: readonly string[],
49
+ requires: RegistryRequires
50
+ ): string[] {
51
+ const order: string[] = [];
52
+ const seen = new Set<string>();
53
+ const visit = (name: string) => {
54
+ if (seen.has(name)) return;
55
+ const entry = requires.items[name];
56
+ if (!entry) {
57
+ throw new Error(
58
+ `Unknown registry item "${name}". Available: ${Object.keys(requires.items).join(", ")}`
59
+ );
60
+ }
61
+ seen.add(name);
62
+ for (const dependency of entry.items ?? []) visit(dependency);
63
+ order.push(name);
64
+ };
65
+ for (const name of selected) visit(name);
66
+ return order;
67
+ }
68
+
69
+ export type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
70
+
71
+ /**
72
+ * Whichever manager ran the CLI (`pnpm dlx`, `npx`, `bunx`… all set
73
+ * npm_config_user_agent), else pnpm — the one the scaffold documents.
74
+ */
75
+ export function detectPackageManager(
76
+ userAgent: string | undefined = process.env.npm_config_user_agent
77
+ ): PackageManager {
78
+ const name = userAgent?.split("/")[0];
79
+ return name === "npm" || name === "yarn" || name === "bun" ? name : "pnpm";
80
+ }
81
+
82
+ export type Command = { command: string; args: string[] };
83
+
84
+ export function formatCommand(c: Command): string {
85
+ return [c.command, ...c.args].join(" ");
86
+ }
87
+
88
+ const EXEC: Record<PackageManager, string[]> = {
89
+ pnpm: ["pnpm", "exec"],
90
+ npm: ["npx"],
91
+ yarn: ["yarn"],
92
+ bun: ["bunx"],
93
+ };
94
+
95
+ /** Whether the app has its own shadcn binary, i.e. dependencies are installed. */
96
+ export function hasLocalShadcn(appRoot: string): boolean {
97
+ return existsSync(path.join(appRoot, "node_modules", ".bin", "shadcn"));
98
+ }
99
+
100
+ /**
101
+ * The commands that put `items` into the app at `appRoot`: install the
102
+ * dependencies when shadcn is not there yet (the scaffold declares it),
103
+ * then the design-system base, then the items in dependency order.
104
+ * `--overwrite` because the base replaces the scaffold's globals.css,
105
+ * and items that share a file agree on it — the same flags the
106
+ * reference app is regenerated with.
107
+ */
108
+ export function installPlan(
109
+ items: readonly string[],
110
+ options: { appRoot: string; packageManager: PackageManager }
111
+ ): Command[] {
112
+ if (items.length === 0) return [];
113
+ const [command, ...exec] = EXEC[options.packageManager];
114
+ const add = (names: string[]): Command => ({
115
+ command: command!,
116
+ args: [
117
+ ...exec,
118
+ "shadcn",
119
+ "add",
120
+ ...names.map((n) => `@intelligo/${n}`),
121
+ "--yes",
122
+ "--overwrite",
123
+ ],
124
+ });
125
+ return [
126
+ ...(hasLocalShadcn(options.appRoot)
127
+ ? []
128
+ : [{ command: options.packageManager, args: ["install"] }]),
129
+ add(["intelligo"]),
130
+ add([...items]),
131
+ ];
132
+ }
@@ -1,11 +1,10 @@
1
1
  /**
2
- * The Intelligo operational console, mounted in your app.
2
+ * The operational console, mounted in your app.
3
3
  *
4
4
  * The screen itself ships from @intelligo-dev/admin so every deployment
5
5
  * shows the same operational truth — a fork could quietly stop showing
6
6
  * you unsettled executions. What is generated here is only the mount:
7
- * the route, the authorization call, and your own page chrome
8
- * (ADR-0002).
7
+ * the route, the authorization call, and your own page chrome.
9
8
  *
10
9
  * Access is gated on PLATFORM_ADMIN_EMAILS, not on a workspace role.
11
10
  * Workspace `owner` is per-tenant and every signup has one.
@@ -5,7 +5,7 @@
5
5
  * it runs inside an execution — entitlement decided, worst-case cost
6
6
  * held, usage settled, audit emitted. Swap `callModel` for a Mastra
7
7
  * agent, an AI SDK call, or anything else and nothing around it
8
- * changes (ADR-0003).
8
+ * changes.
9
9
  */
10
10
 
11
11
  import { requireWorkspace } from "@intelligo-dev/auth";
@@ -15,7 +15,7 @@ import { CAPABILITIES, composeIntelligo, executions } from "@/lib/intelligo";
15
15
  export const maxDuration = 60;
16
16
 
17
17
  async function callModel(prompt: string) {
18
- // Replace this with your framework's call.
18
+ // Your AI framework's call goes here.
19
19
  const inputTokens = Math.max(1, Math.ceil(prompt.length / 4));
20
20
  return {
21
21
  text: `Echo: ${prompt}`,
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Better-Auth's HTTP mount.
3
+ *
4
+ * Every call the auth client makes is a request to `/api/auth/*` on
5
+ * this app's own origin; without this file every sign-in POST answers
6
+ * 404. The handlers come from `@intelligo-dev/next`, so the app never
7
+ * depends on `better-auth` directly.
8
+ */
9
+
10
+ export { GET, POST } from "@intelligo-dev/next/auth";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://ui.shadcn.com/schema.json",
3
- "style": "new-york",
3
+ "style": "base-nova",
4
4
  "rsc": true,
5
5
  "tsx": true,
6
6
  "tailwind": {
@@ -19,5 +19,9 @@
19
19
  "lib": "@/lib",
20
20
  "hooks": "@/hooks"
21
21
  },
22
- "registries": {}
22
+ "registries": {
23
+ "@intelligo": "https://intelligo.dev/r/{name}.json"
24
+ },
25
+ "menuColor": "default",
26
+ "menuAccent": "subtle"
23
27
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Tables this application owns.
3
+ *
4
+ * Everything the framework needs — users, workspaces, members, plans,
5
+ * subscriptions, credits, executions, audit events, notifications,
6
+ * conversations, documents — is `@intelligo-dev/core`'s and arrives
7
+ * with its own migrations. Define your product's tables here, scope
8
+ * them to a workspace, and reference framework tables by import:
9
+ *
10
+ * import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
11
+ * import { organization, users } from "@intelligo-dev/core/db/schema";
12
+ *
13
+ * export const reports = pgTable("reports", {
14
+ * id: text("id").primaryKey(),
15
+ * workspaceId: text("workspace_id")
16
+ * .notNull()
17
+ * .references(() => organization.id, { onDelete: "cascade" }),
18
+ * createdBy: text("created_by").references(() => users.id),
19
+ * createdAt: timestamp("created_at").defaultNow().notNull(),
20
+ * });
21
+ *
22
+ * Then `pnpm db:generate` writes the migration into ./drizzle and
23
+ * `pnpm db:migrate` applies it after the framework's chain.
24
+ *
25
+ * Intelligo never reads these tables (every table has one
26
+ * owner), so their shape is entirely yours.
27
+ */
28
+
29
+ export {};
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": []
5
+ }
@@ -0,0 +1,45 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+ import { config } from "dotenv";
3
+
4
+ // drizzle-kit does not load env files; Next.js does. Same precedence.
5
+ config({ path: ".env.local" });
6
+ config({ path: ".env" });
7
+
8
+ /**
9
+ * Your application's migration chain — and only yours.
10
+ *
11
+ * The framework's tables (users, workspaces, plans, credits, executions,
12
+ * …) arrive inside `@intelligo-dev/core` together with their migration
13
+ * history, and `intelligo migrate` applies that chain. This config
14
+ * covers the tables you own: list their schema files here, generate
15
+ * with `pnpm db:generate`, apply with `pnpm db:migrate`.
16
+ * `drizzle/meta/_journal.json` starts empty so `db:migrate` succeeds
17
+ * before you own a table — drizzle-kit exits 1, with no message, when
18
+ * the journal is missing.
19
+ *
20
+ * Two chains, one database, so they record themselves in different
21
+ * tables: the framework in drizzle's default `__drizzle_migrations`,
22
+ * yours in `__app_migrations`. Do not merge them — drizzle-kit applies
23
+ * by timestamp, and a framework migration published after you
24
+ * generated one of yours would be skipped silently.
25
+ *
26
+ * Your schema may reference framework tables (`references(() =>
27
+ * users.id)`); drizzle-kit emits the foreign key by name and does not
28
+ * try to create the framework's table.
29
+ *
30
+ * Do not point `drizzle-kit push` at this config: push diffs the whole
31
+ * database against the schema it sees, and this config deliberately
32
+ * sees only yours.
33
+ */
34
+ export default defineConfig({
35
+ dialect: "postgresql",
36
+ schema: ["./lib/db/schema.ts"],
37
+ out: "./drizzle",
38
+ migrations: { table: "__app_migrations", schema: "drizzle" },
39
+ // The framework's columns are snake_case in Postgres and camelCase in
40
+ // TypeScript. Keep the same convention so joins read naturally.
41
+ casing: "snake_case",
42
+ dbCredentials: {
43
+ url: process.env.DATABASE_URL!,
44
+ },
45
+ });
@@ -1,6 +1,18 @@
1
+ # ============================================
1
2
  # Required — the app will not boot without these.
3
+ # ============================================
4
+
5
+ # Postgres with the pgvector extension (Neon, Supabase, RDS, or local).
2
6
  DATABASE_URL=
7
+ # Driver is chosen from the URL (Neon hosts get the WebSocket driver,
8
+ # everything else node-postgres). Force it with pg | neon-serverless.
9
+ # INTELLIGO_DB_DRIVER=
10
+
11
+ # At least 32 characters. Generate with: openssl rand -base64 32
3
12
  BETTER_AUTH_SECRET=
13
+
14
+ # Where users reach the app. Auth callbacks, email links and redirects
15
+ # are built from it, so production needs the public https URL.
4
16
  NEXT_PUBLIC_APP_URL=http://localhost:3000
5
17
 
6
18
  # Which product's plans the billing engine bills against. Your
@@ -8,6 +20,58 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
8
20
  # variable is here so CLI tooling can check it without booting the app.
9
21
  INTELLIGO_BILLING_PRODUCT=__APP_SLUG__
10
22
 
23
+ # ============================================
24
+ # Optional — everything works locally without these.
25
+ # ============================================
26
+
27
+ # Sign in with Google or GitHub. A provider's button appears once both
28
+ # of its values are set; the callback URL to register with the provider
29
+ # is NEXT_PUBLIC_APP_URL + /api/auth/callback/google (or /github).
30
+ # GOOGLE_CLIENT_ID=
31
+ # GOOGLE_CLIENT_SECRET=
32
+ # GITHUB_CLIENT_ID=
33
+ # GITHUB_CLIENT_SECRET=
34
+
35
+ # Email. Without a provider, emails (verification, invitations, reset
36
+ # links) are printed to the server console, and sign-ups are not held
37
+ # for email verification.
38
+ # RESEND_API_KEY=
39
+ # The sender of every email, on a domain verified with Resend. Required
40
+ # with RESEND_API_KEY: there is no built-in sender, so sends fail without
41
+ # it. Loops ignores it and takes the sender from each template.
42
+ # EMAIL_FROM=Acme <noreply@example.com>
43
+ # The product's name in every email's heading and footer; defaults to
44
+ # the display name in EMAIL_FROM. SUPPORT_EMAIL is the address the
45
+ # footer offers for replies; without it the footer offers none.
46
+ # APP_NAME=Acme
47
+ # SUPPORT_EMAIL=support@example.com
48
+ # Loops sends by template id instead of HTML: map each template with
49
+ # LOOPS_TRANSACTIONAL_ID_<KEY>, e.g. LOOPS_TRANSACTIONAL_ID_VERIFY_EMAIL.
50
+ # LOOPS_API_KEY=
51
+ # With both keys set Resend wins; force one with resend | loops | console.
52
+ # EMAIL_PROVIDER=
53
+
54
+ # Stripe. Billing pages say "not configured" until these are set; the
55
+ # webhook at /api/webhooks/stripe verifies with the secret.
56
+ # STRIPE_SECRET_KEY=
57
+ # STRIPE_WEBHOOK_SECRET=
58
+
59
+ # Which registered payment provider serves payments outside Stripe
60
+ # (registerPaymentProvider in the composition root). Defaults to the
61
+ # in-memory mock, which production refuses.
62
+ # PAYMENT_MODE=
63
+
64
+ # AI providers. Chat runs on a built-in stub model until lib/chat-model.ts
65
+ # names a real one. Set only the key of the provider that model belongs
66
+ # to — the provider's SDK reads it, and the framework checks none of them.
67
+ # OPENAI_API_KEY=
68
+ # ANTHROPIC_API_KEY=
69
+ # GOOGLE_GENERATIVE_AI_API_KEY=
70
+
71
+ # Bearer token your scheduler sends to /api/cron/maintenance
72
+ # (`intelligo add maintenance`). At least 32 characters.
73
+ # CRON_SECRET=
74
+
11
75
  # Comma-separated emails allowed to reach the Intelligo admin console.
12
76
  # Closed by default: with no value, nobody is a platform admin.
13
- PLATFORM_ADMIN_EMAILS=
77
+ # PLATFORM_ADMIN_EMAILS=