@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.
- package/NOTICE +6 -0
- package/README.md +63 -0
- package/dist/bin.js +85 -37
- package/dist/bin.js.map +1 -1
- package/dist/commands/add.d.ts +22 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +61 -4
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/create-flow.d.ts +34 -0
- package/dist/commands/create-flow.d.ts.map +1 -0
- package/dist/commands/create-flow.js +180 -0
- package/dist/commands/create-flow.js.map +1 -0
- package/dist/commands/create.d.ts +23 -6
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +45 -13
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.d.ts +14 -3
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +293 -37
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/migrate-check.d.ts +54 -1
- package/dist/commands/migrate-check.d.ts.map +1 -1
- package/dist/commands/migrate-check.js +101 -5
- package/dist/commands/migrate-check.js.map +1 -1
- package/dist/commands/migrate.d.ts +97 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +147 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/upgrade-check.d.ts +5 -5
- package/dist/commands/upgrade-check.d.ts.map +1 -1
- package/dist/commands/upgrade-check.js +19 -6
- package/dist/commands/upgrade-check.js.map +1 -1
- package/dist/env-files.d.ts +12 -0
- package/dist/env-files.d.ts.map +1 -0
- package/dist/env-files.js +29 -0
- package/dist/env-files.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +9 -15
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +5 -16
- package/dist/manifest.js.map +1 -1
- package/dist/migrations.d.ts +3 -6
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +3 -6
- package/dist/migrations.js.map +1 -1
- package/dist/model-catalogue.d.ts +14 -0
- package/dist/model-catalogue.d.ts.map +1 -0
- package/dist/model-catalogue.js +34 -0
- package/dist/model-catalogue.js.map +1 -0
- package/dist/registry-items.d.ts +56 -0
- package/dist/registry-items.d.ts.map +1 -0
- package/dist/registry-items.js +103 -0
- package/dist/registry-items.js.map +1 -0
- package/package.json +36 -10
- package/src/bin.ts +264 -0
- package/src/commands/add.ts +244 -0
- package/src/commands/create-flow.ts +228 -0
- package/src/commands/create.ts +139 -0
- package/src/commands/doctor.ts +487 -0
- package/src/commands/migrate-check.ts +257 -0
- package/src/commands/migrate.ts +234 -0
- package/src/commands/upgrade-check.ts +157 -0
- package/src/env-files.ts +31 -0
- package/src/index.ts +39 -0
- package/src/manifest.ts +93 -0
- package/src/migrations-dir.ts +25 -0
- package/src/migrations.ts +133 -0
- package/src/model-catalogue.ts +35 -0
- package/src/registry-items.ts +132 -0
- package/templates/admin-page/admin-page.tsx.tpl +2 -3
- package/templates/app-scaffold/assistant-route.ts.tpl +2 -2
- package/templates/app-scaffold/auth-route.ts.tpl +10 -0
- package/templates/app-scaffold/components.json.tpl +6 -2
- package/templates/app-scaffold/db-schema.ts.tpl +29 -0
- package/templates/app-scaffold/drizzle-journal.json.tpl +5 -0
- package/templates/app-scaffold/drizzle.config.ts.tpl +45 -0
- package/templates/app-scaffold/env.example.tpl +65 -1
- package/templates/app-scaffold/globals.css.tpl +172 -281
- package/templates/app-scaffold/i18n-request.ts.tpl +25 -0
- package/templates/app-scaffold/instrumentation.ts.tpl +21 -0
- package/templates/app-scaffold/intelligo.ts.tpl +104 -9
- package/templates/app-scaffold/layout.tsx.tpl +18 -1
- package/templates/app-scaffold/lib-utils.ts.tpl +1 -6
- package/templates/app-scaffold/next.config.mjs.tpl +2 -0
- package/templates/app-scaffold/package.json.tpl +31 -17
- package/templates/app-scaffold/page.tsx.tpl +16 -0
- package/templates/app-scaffold/plans.ts.tpl +32 -13
- package/templates/app-scaffold/{middleware.ts.tpl → proxy.ts.tpl} +6 -0
- package/templates/app-scaffold/sonner.tsx.tpl +42 -20
- package/templates/app-scaffold/stripe-webhook-route.ts.tpl +24 -0
- package/templates/app-scaffold/theme-provider.tsx.tpl +6 -4
- package/templates/app-scaffold/tsconfig.json.tpl +20 -4
- package/templates/app-scaffold/use-mobile.ts.tpl +19 -0
- package/templates/app-scaffold/workspace-bootstrap.ts.tpl +30 -0
- package/templates/maintenance/maintenance-route.ts.tpl +119 -0
- package/templates/manifest.json +57 -18
- package/templates/registry-items.json +116 -0
- package/templates/registry-requires.json +156 -0
- package/templates/usage-page/usage-page.tsx.tpl +23 -8
- 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";
|
package/src/manifest.ts
ADDED
|
@@ -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
|
|
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
|
|
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
|
-
//
|
|
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": "
|
|
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,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=
|