@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.
- package/NOTICE +6 -0
- package/README.md +102 -0
- package/dist/bin.js +122 -37
- package/dist/bin.js.map +1 -1
- package/dist/commands/add.d.ts +26 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +70 -4
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/create-flow.d.ts +37 -0
- package/dist/commands/create-flow.d.ts.map +1 -0
- package/dist/commands/create-flow.js +212 -0
- package/dist/commands/create-flow.js.map +1 -0
- package/dist/commands/create.d.ts +27 -6
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +53 -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 +295 -37
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/migrate-check.d.ts +73 -1
- package/dist/commands/migrate-check.d.ts.map +1 -1
- package/dist/commands/migrate-check.js +130 -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 +141 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/sync-messages.d.ts +37 -0
- package/dist/commands/sync-messages.d.ts.map +1 -0
- package/dist/commands/sync-messages.js +88 -0
- package/dist/commands/sync-messages.js.map +1 -0
- package/dist/commands/sync-scaffold.d.ts +36 -0
- package/dist/commands/sync-scaffold.d.ts.map +1 -0
- package/dist/commands/sync-scaffold.js +53 -0
- package/dist/commands/sync-scaffold.js.map +1 -0
- package/dist/commands/sync.d.ts +103 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +361 -0
- package/dist/commands/sync.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 +22 -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 +36 -15
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +32 -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/module-exports.d.ts +63 -0
- package/dist/module-exports.d.ts.map +1 -0
- package/dist/module-exports.js +231 -0
- package/dist/module-exports.js.map +1 -0
- package/dist/registry-bundle.d.ts +43 -0
- package/dist/registry-bundle.d.ts.map +1 -0
- package/dist/registry-bundle.js +74 -0
- package/dist/registry-bundle.js.map +1 -0
- package/dist/registry-items.d.ts +84 -0
- package/dist/registry-items.d.ts.map +1 -0
- package/dist/registry-items.js +187 -0
- package/dist/registry-items.js.map +1 -0
- package/package.json +36 -10
- package/src/bin.ts +316 -0
- package/src/commands/add.ts +255 -0
- package/src/commands/create-flow.ts +284 -0
- package/src/commands/create.ts +149 -0
- package/src/commands/doctor.ts +487 -0
- package/src/commands/migrate-check.ts +298 -0
- package/src/commands/migrate.ts +228 -0
- package/src/commands/sync-messages.ts +114 -0
- package/src/commands/sync-scaffold.ts +83 -0
- package/src/commands/sync.ts +534 -0
- package/src/commands/upgrade-check.ts +160 -0
- package/src/env-files.ts +31 -0
- package/src/index.ts +39 -0
- package/src/manifest.ts +147 -0
- package/src/migrations-dir.ts +25 -0
- package/src/migrations.ts +133 -0
- package/src/model-catalogue.ts +35 -0
- package/src/module-exports.ts +266 -0
- package/src/registry-bundle.ts +103 -0
- package/src/registry-items.ts +237 -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 +95 -41
- 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 +75 -17
- package/templates/registry/ai-agent-activity.json +23 -0
- package/templates/registry/ai-agent-progress.json +21 -0
- package/templates/registry/ai-approval-card.json +27 -0
- package/templates/registry/ai-artifact.json +22 -0
- package/templates/registry/ai-branch.json +21 -0
- package/templates/registry/ai-citations.json +24 -0
- package/templates/registry/ai-code-block.json +23 -0
- package/templates/registry/ai-composer-menu.json +19 -0
- package/templates/registry/ai-file-diff.json +25 -0
- package/templates/registry/ai-image-generation.json +23 -0
- package/templates/registry/ai-markdown.json +23 -0
- package/templates/registry/ai-message-bubble.json +23 -0
- package/templates/registry/ai-message-scroller.json +22 -0
- package/templates/registry/ai-message.json +21 -0
- package/templates/registry/ai-motion.json +19 -0
- package/templates/registry/ai-prompt-input.json +28 -0
- package/templates/registry/ai-reasoning-text.json +22 -0
- package/templates/registry/ai-reasoning.json +22 -0
- package/templates/registry/ai-shimmer-text.json +19 -0
- package/templates/registry/ai-sidebar.json +25 -0
- package/templates/registry/ai-speech-input.json +21 -0
- package/templates/registry/ai-streaming-response.json +24 -0
- package/templates/registry/ai-suggestion.json +19 -0
- package/templates/registry/ai-todo-list.json +22 -0
- package/templates/registry/ai-tool-approval.json +26 -0
- package/templates/registry/ai-tool-result.json +25 -0
- package/templates/registry/alert-dialog.json +23 -0
- package/templates/registry/animated-list.json +21 -0
- package/templates/registry/app-shell.json +93 -0
- package/templates/registry/artifacts.json +82 -0
- package/templates/registry/attachment.json +22 -0
- package/templates/registry/auth-email-verification.json +39 -0
- package/templates/registry/auth-login.json +72 -0
- package/templates/registry/auth-password-reset.json +53 -0
- package/templates/registry/auth-signup.json +41 -0
- package/templates/registry/billing-settings.json +60 -0
- package/templates/registry/button.json +22 -0
- package/templates/registry/chat-eve.json +19 -0
- package/templates/registry/chat-panel.json +30 -0
- package/templates/registry/chat-share.json +42 -0
- package/templates/registry/chat-widget.json +34 -0
- package/templates/registry/chat.json +326 -0
- package/templates/registry/checkbox.json +22 -0
- package/templates/registry/checkout.json +40 -0
- package/templates/registry/collapsible.json +19 -0
- package/templates/registry/command.json +23 -0
- package/templates/registry/copy-button.json +21 -0
- package/templates/registry/dashboard.json +69 -0
- package/templates/registry/dialog.json +24 -0
- package/templates/registry/document-viewer.json +22 -0
- package/templates/registry/dropdown-menu.json +23 -0
- package/templates/registry/expandable-tabs.json +22 -0
- package/templates/registry/feature-gating.json +73 -0
- package/templates/registry/file-upload.json +24 -0
- package/templates/registry/hold-action-button.json +22 -0
- package/templates/registry/input-group.json +23 -0
- package/templates/registry/input.json +19 -0
- package/templates/registry/intelligo.json +187 -0
- package/templates/registry/invitation-accept.json +64 -0
- package/templates/registry/language-switcher.json +29 -0
- package/templates/registry/morphing-modal.json +24 -0
- package/templates/registry/notification-stack.json +24 -0
- package/templates/registry/notifications.json +87 -0
- package/templates/registry/onboarding.json +83 -0
- package/templates/registry/otp-input.json +22 -0
- package/templates/registry/page-header.json +15 -0
- package/templates/registry/payment-poll.json +52 -0
- package/templates/registry/popover-morph.json +22 -0
- package/templates/registry/popover.json +22 -0
- package/templates/registry/pricing.json +111 -0
- package/templates/registry/privacy-settings.json +65 -0
- package/templates/registry/profile-settings.json +68 -0
- package/templates/registry/progress.json +19 -0
- package/templates/registry/radio-group.json +22 -0
- package/templates/registry/registry.json +2945 -0
- package/templates/registry/route-error.json +65 -0
- package/templates/registry/select-morph.json +23 -0
- package/templates/registry/select.json +23 -0
- package/templates/registry/settings-shell.json +47 -0
- package/templates/registry/sheet.json +24 -0
- package/templates/registry/sidebar.json +28 -0
- package/templates/registry/smoke.json +34 -0
- package/templates/registry/spinner.json +16 -0
- package/templates/registry/stat-card.json +18 -0
- package/templates/registry/status-badge.json +18 -0
- package/templates/registry/switch.json +20 -0
- package/templates/registry/tabs.json +23 -0
- package/templates/registry/team-settings.json +97 -0
- package/templates/registry/textarea.json +16 -0
- package/templates/registry/tooltip.json +22 -0
- package/templates/registry/trial-banner.json +43 -0
- package/templates/registry/usage.json +84 -0
- package/templates/registry/workspace-settings.json +71 -0
- package/templates/registry-items.json +116 -0
- package/templates/registry-requires.json +180 -0
- package/templates/usage-page/usage-page.tsx.tpl +23 -8
- package/templates/vitest/server-only.ts.tpl +7 -0
- package/templates/vitest/vitest.config.ts.tpl +37 -0
- 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
|
+
}
|
package/src/env-files.ts
ADDED
|
@@ -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";
|
package/src/manifest.ts
ADDED
|
@@ -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
|
+
}
|