@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
package/package.json
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intelligo-dev/cli",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.14",
|
|
4
|
+
"description": "The Intelligo CLI: scaffold an AI SaaS app, generate owned source, and check its migrations and upgrades.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"intelligo",
|
|
7
|
+
"ai-saas",
|
|
8
|
+
"saas",
|
|
9
|
+
"typescript",
|
|
10
|
+
"cli",
|
|
11
|
+
"scaffold",
|
|
12
|
+
"shadcn",
|
|
13
|
+
"migrations",
|
|
14
|
+
"nextjs"
|
|
15
|
+
],
|
|
4
16
|
"license": "Apache-2.0",
|
|
17
|
+
"author": "Turtuvshin Byambaa <toroo.byamba@gmail.com>",
|
|
5
18
|
"repository": {
|
|
6
19
|
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/intelligo-
|
|
20
|
+
"url": "git+https://github.com/intelligo-dev/intelligo.git",
|
|
8
21
|
"directory": "packages/cli"
|
|
9
22
|
},
|
|
10
|
-
"homepage": "https://
|
|
23
|
+
"homepage": "https://intelligo.dev/docs/packages/cli",
|
|
11
24
|
"bugs": {
|
|
12
|
-
"url": "https://github.com/intelligo-
|
|
25
|
+
"url": "https://github.com/intelligo-dev/intelligo/issues"
|
|
13
26
|
},
|
|
14
27
|
"type": "module",
|
|
15
28
|
"bin": {
|
|
@@ -22,23 +35,36 @@
|
|
|
22
35
|
}
|
|
23
36
|
},
|
|
24
37
|
"dependencies": {
|
|
25
|
-
"
|
|
38
|
+
"@clack/prompts": "^1.8.1",
|
|
39
|
+
"pg": "8.18.0"
|
|
26
40
|
},
|
|
27
41
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^
|
|
29
|
-
"@types/pg": "^8.
|
|
30
|
-
"typescript": "^5.
|
|
42
|
+
"@types/node": "^26.6.1",
|
|
43
|
+
"@types/pg": "^8.23.1",
|
|
44
|
+
"typescript": "^5.9.3"
|
|
31
45
|
},
|
|
32
46
|
"files": [
|
|
33
47
|
"dist",
|
|
34
|
-
"
|
|
48
|
+
"src",
|
|
49
|
+
"!src/**/*.test.ts",
|
|
50
|
+
"!src/**/*.test.tsx",
|
|
51
|
+
"!src/**/__tests__/**",
|
|
52
|
+
"templates",
|
|
53
|
+
"LICENSE",
|
|
54
|
+
"NOTICE",
|
|
55
|
+
"README.md",
|
|
56
|
+
"!dist/**/*.tsbuildinfo"
|
|
35
57
|
],
|
|
36
58
|
"publishConfig": {
|
|
37
59
|
"access": "public"
|
|
38
60
|
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=22.14"
|
|
63
|
+
},
|
|
64
|
+
"sideEffects": false,
|
|
39
65
|
"scripts": {
|
|
40
66
|
"type-check": "tsc --noEmit",
|
|
41
67
|
"lint": "eslint .",
|
|
42
|
-
"build": "tsc -p tsconfig.build.json"
|
|
68
|
+
"build": "node scripts/sync-registry-requires.mjs && tsc -p tsconfig.build.json && node ../../scripts/fix-esm-extensions.mjs dist"
|
|
43
69
|
}
|
|
44
70
|
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* intelligo <command>
|
|
4
|
+
*
|
|
5
|
+
* The shebang targets node because that is what runs the *published*
|
|
6
|
+
* binary from dist/. In this workspace the source is run with
|
|
7
|
+
* `pnpm exec tsx packages/cli/src/bin.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately tiny: the commands are library functions, and this file
|
|
10
|
+
* only maps argv onto them, opens a database connection when one is
|
|
11
|
+
* needed, and picks an exit code.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
addFeature,
|
|
20
|
+
formatAddResult,
|
|
21
|
+
addExitCode,
|
|
22
|
+
readCatalogue,
|
|
23
|
+
} from "./commands/add.js";
|
|
24
|
+
import { exitCodeFor, formatResults, runChecks } from "./commands/doctor.js";
|
|
25
|
+
import {
|
|
26
|
+
formatMigrateCheck,
|
|
27
|
+
formatMigrateCheckJson,
|
|
28
|
+
migrateCheck,
|
|
29
|
+
migrateCheckExitCode,
|
|
30
|
+
} from "./commands/migrate-check.js";
|
|
31
|
+
import {
|
|
32
|
+
MIGRATIONS_TABLE_SQL,
|
|
33
|
+
SCHEMA_PROBE_SQL,
|
|
34
|
+
applyExitCode,
|
|
35
|
+
applyMigrations,
|
|
36
|
+
formatApplyResult,
|
|
37
|
+
} from "./commands/migrate.js";
|
|
38
|
+
import {
|
|
39
|
+
formatSyncReport,
|
|
40
|
+
installedFrameworkVersion,
|
|
41
|
+
selectItems,
|
|
42
|
+
syncApply,
|
|
43
|
+
syncCheck,
|
|
44
|
+
syncCheckExitCode,
|
|
45
|
+
type SyncContext,
|
|
46
|
+
} from "./commands/sync.js";
|
|
47
|
+
import {
|
|
48
|
+
formatUpgradeReport,
|
|
49
|
+
upgradeCheck,
|
|
50
|
+
upgradeCheckExitCode,
|
|
51
|
+
} from "./commands/upgrade-check.js";
|
|
52
|
+
|
|
53
|
+
import { loadAppEnv } from "./env-files.js";
|
|
54
|
+
import { resolveRegistryDir } from "./registry-bundle.js";
|
|
55
|
+
import { readRegistryCatalogue } from "./registry-items.js";
|
|
56
|
+
import { MIGRATION_LOCATIONS, resolveMigrationsDir } from "./migrations-dir.js";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Templates ship with the CLI package.
|
|
60
|
+
*
|
|
61
|
+
* `fileURLToPath`, not `new URL(...).pathname`: on Windows the latter
|
|
62
|
+
* yields `/C:/Users/...`, which every path operation after it treats as
|
|
63
|
+
* a root-relative path that does not exist. The CLI is the first thing
|
|
64
|
+
* a new user runs, so it is the worst place to be silently
|
|
65
|
+
* platform-specific.
|
|
66
|
+
*/
|
|
67
|
+
const TEMPLATES_DIR = path.resolve(
|
|
68
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
69
|
+
"..",
|
|
70
|
+
"templates"
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Recorded in the manifest so an upgrade knows what wrote a file, and
|
|
75
|
+
* emitted as the dependency range of a scaffolded app. Read from this
|
|
76
|
+
* package's own manifest so a release cannot ship with a stale literal
|
|
77
|
+
* — `../package.json` resolves from src/ and from dist/ alike.
|
|
78
|
+
*/
|
|
79
|
+
const FRAMEWORK_VERSION: string = createRequire(import.meta.url)(
|
|
80
|
+
"../package.json"
|
|
81
|
+
).version;
|
|
82
|
+
|
|
83
|
+
function usage(): string {
|
|
84
|
+
return [
|
|
85
|
+
"intelligo <command>",
|
|
86
|
+
"",
|
|
87
|
+
" create [dir] Scaffold an app, then install the registry pages you pick",
|
|
88
|
+
" (--items a,b | --all, --yes, --no-install)",
|
|
89
|
+
" doctor Report configuration and migration-chain problems",
|
|
90
|
+
" migrate Apply the framework's migration chain to DATABASE_URL",
|
|
91
|
+
" migrate --check Compare the framework's migrations to a database",
|
|
92
|
+
" (--json: one object whose `state` is up_to_date | pending |",
|
|
93
|
+
" fresh | ahead | unmanaged | legacy)",
|
|
94
|
+
" add <feature> Generate consumer-owned source (--force to overwrite)",
|
|
95
|
+
" upgrade --check Show what a template upgrade would change",
|
|
96
|
+
" sync [items…] Install registry pages from this release's registry,",
|
|
97
|
+
" keeping seams and merging messages (--force replaces",
|
|
98
|
+
" hand-edited files; --check only reports, exit 1 on drift)",
|
|
99
|
+
"",
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function runMigrate(
|
|
104
|
+
mode: "check" | "apply",
|
|
105
|
+
json = false
|
|
106
|
+
): Promise<number> {
|
|
107
|
+
const url = process.env.DATABASE_URL;
|
|
108
|
+
if (!url) {
|
|
109
|
+
console.error(
|
|
110
|
+
`DATABASE_URL is required for \`migrate${mode === "check" ? " --check" : ""}\`.`
|
|
111
|
+
);
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const migrationsDir = resolveMigrationsDir(process.cwd());
|
|
116
|
+
if (!migrationsDir) {
|
|
117
|
+
console.error(
|
|
118
|
+
`No migrations directory (looked in ${MIGRATION_LOCATIONS.join(", ")}) — run from the workspace root, with @intelligo-dev/core installed.`
|
|
119
|
+
);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Imported lazily so `doctor` — the command you reach for when the
|
|
124
|
+
// app will not start — never needs a database driver to load.
|
|
125
|
+
const { Client } = await import("pg");
|
|
126
|
+
const client = new Client({ connectionString: url });
|
|
127
|
+
await client.connect();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
if (mode === "check") {
|
|
131
|
+
const result = await migrateCheck(
|
|
132
|
+
migrationsDir,
|
|
133
|
+
async (sql) => (await client.query<{ hash: string }>(sql)).rows
|
|
134
|
+
);
|
|
135
|
+
const probe = await client.query<{ rel: string | null }>(
|
|
136
|
+
SCHEMA_PROBE_SQL
|
|
137
|
+
);
|
|
138
|
+
const schemaExists = probe.rows[0]?.rel != null;
|
|
139
|
+
console.log(
|
|
140
|
+
json
|
|
141
|
+
? formatMigrateCheckJson(result, schemaExists)
|
|
142
|
+
: formatMigrateCheck(result, schemaExists)
|
|
143
|
+
);
|
|
144
|
+
return migrateCheckExitCode(result);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Applied here rather than by drizzle's migrator: the pending set is
|
|
148
|
+
// chosen by content hash — what `migrate --check` compares — and
|
|
149
|
+
// recorded in drizzle's own table, in one transaction, so a failure
|
|
150
|
+
// part-way leaves neither statements nor records behind.
|
|
151
|
+
const result = await applyMigrations({
|
|
152
|
+
migrationsDir,
|
|
153
|
+
query: async (sql) => (await client.query(sql)).rows,
|
|
154
|
+
run: async (pending) => {
|
|
155
|
+
await client.query("BEGIN");
|
|
156
|
+
try {
|
|
157
|
+
for (const ddl of MIGRATIONS_TABLE_SQL) await client.query(ddl);
|
|
158
|
+
for (const migration of pending) {
|
|
159
|
+
for (const statement of migration.statements) {
|
|
160
|
+
await client.query(statement);
|
|
161
|
+
}
|
|
162
|
+
await client.query(
|
|
163
|
+
`INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at") VALUES ($1, $2)`,
|
|
164
|
+
[migration.hash, migration.createdAt]
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
await client.query("COMMIT");
|
|
168
|
+
} catch (error) {
|
|
169
|
+
await client.query("ROLLBACK");
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
console.log(formatApplyResult(result));
|
|
175
|
+
return applyExitCode(result);
|
|
176
|
+
} finally {
|
|
177
|
+
await client.end();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function main(): Promise<number> {
|
|
182
|
+
const [, , command = "help", ...rest] = process.argv;
|
|
183
|
+
|
|
184
|
+
// The commands that read the app's configuration see what the app
|
|
185
|
+
// itself would: its .env.local and .env, under anything the shell set.
|
|
186
|
+
if (command === "doctor" || command === "migrate" || command === "upgrade") {
|
|
187
|
+
loadAppEnv(process.cwd());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
switch (command) {
|
|
191
|
+
case "doctor": {
|
|
192
|
+
const results = runChecks();
|
|
193
|
+
console.log(formatResults(results));
|
|
194
|
+
return exitCodeFor(results);
|
|
195
|
+
}
|
|
196
|
+
case "migrate": {
|
|
197
|
+
// Applying is the default, so a mistyped flag (`--dry-run`,
|
|
198
|
+
// `--chek`) must not fall through to it.
|
|
199
|
+
const unknown = rest.filter(
|
|
200
|
+
(arg) => arg !== "--check" && arg !== "--json"
|
|
201
|
+
);
|
|
202
|
+
if (unknown.length > 0) {
|
|
203
|
+
console.error(
|
|
204
|
+
`intelligo migrate: unknown argument ${unknown.join(" ")}. ` +
|
|
205
|
+
"Use `intelligo migrate` to apply, `--check` to only report, " +
|
|
206
|
+
"and `--json` with `--check` for a machine-readable answer."
|
|
207
|
+
);
|
|
208
|
+
return 2;
|
|
209
|
+
}
|
|
210
|
+
return runMigrate(
|
|
211
|
+
rest.includes("--check") ? "check" : "apply",
|
|
212
|
+
rest.includes("--json")
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
case "create": {
|
|
216
|
+
// Imported lazily so the prompts library loads only for the one
|
|
217
|
+
// command that converses.
|
|
218
|
+
const { parseCreateFlags, runCreate } =
|
|
219
|
+
await import("./commands/create-flow.js");
|
|
220
|
+
return runCreate(parseCreateFlags(rest), {
|
|
221
|
+
templatesDir: TEMPLATES_DIR,
|
|
222
|
+
frameworkVersion: FRAMEWORK_VERSION,
|
|
223
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
case "add": {
|
|
227
|
+
const feature = rest.find((a) => !a.startsWith("--"));
|
|
228
|
+
if (!feature) {
|
|
229
|
+
const catalogue = readCatalogue(TEMPLATES_DIR);
|
|
230
|
+
console.error("Usage: intelligo add <feature>\n");
|
|
231
|
+
for (const [name, spec] of Object.entries(catalogue)) {
|
|
232
|
+
console.error(` ${name.padEnd(16)} ${spec.description}`);
|
|
233
|
+
}
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
const result = addFeature(feature, {
|
|
237
|
+
appRoot: process.cwd(),
|
|
238
|
+
templatesDir: TEMPLATES_DIR,
|
|
239
|
+
frameworkVersion: FRAMEWORK_VERSION,
|
|
240
|
+
force: rest.includes("--force"),
|
|
241
|
+
});
|
|
242
|
+
console.log(formatAddResult(result));
|
|
243
|
+
return addExitCode(result);
|
|
244
|
+
}
|
|
245
|
+
case "upgrade": {
|
|
246
|
+
if (!rest.includes("--check")) {
|
|
247
|
+
console.error("Only `upgrade --check` is implemented.");
|
|
248
|
+
console.error(
|
|
249
|
+
"Applying an upgrade means re-running `intelligo add` for the " +
|
|
250
|
+
"features it reports as outdated — deliberately your call, " +
|
|
251
|
+
"since the files are yours."
|
|
252
|
+
);
|
|
253
|
+
return 1;
|
|
254
|
+
}
|
|
255
|
+
const report = upgradeCheck({
|
|
256
|
+
appRoot: process.cwd(),
|
|
257
|
+
templatesDir: TEMPLATES_DIR,
|
|
258
|
+
});
|
|
259
|
+
console.log(formatUpgradeReport(report));
|
|
260
|
+
return upgradeCheckExitCode(report);
|
|
261
|
+
}
|
|
262
|
+
case "sync": {
|
|
263
|
+
const registryDir = resolveRegistryDir(TEMPLATES_DIR);
|
|
264
|
+
if (!registryDir) {
|
|
265
|
+
console.error(
|
|
266
|
+
"This CLI has no bundled registry — in the framework repository, run `pnpm registry:build` first."
|
|
267
|
+
);
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
const context: SyncContext = {
|
|
271
|
+
appRoot: process.cwd(),
|
|
272
|
+
registryDir,
|
|
273
|
+
requires: readRegistryCatalogue(TEMPLATES_DIR).requires,
|
|
274
|
+
frameworkVersion: FRAMEWORK_VERSION,
|
|
275
|
+
};
|
|
276
|
+
const installed = installedFrameworkVersion(context.appRoot);
|
|
277
|
+
if (installed && installed !== FRAMEWORK_VERSION) {
|
|
278
|
+
console.error(
|
|
279
|
+
`! @intelligo-dev/core is ${installed} but this CLI carries the ${FRAMEWORK_VERSION} registry — ` +
|
|
280
|
+
"run the CLI of the same version (`pnpm exec intelligo`), or pages and packages will disagree."
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const selection = selectItems(
|
|
284
|
+
rest.filter((a) => !a.startsWith("-")),
|
|
285
|
+
context
|
|
286
|
+
);
|
|
287
|
+
if (!selection.ok) {
|
|
288
|
+
console.error(selection.message);
|
|
289
|
+
return 1;
|
|
290
|
+
}
|
|
291
|
+
if (rest.includes("--check")) {
|
|
292
|
+
const report = syncCheck(selection.items, context);
|
|
293
|
+
console.log(formatSyncReport(report));
|
|
294
|
+
return syncCheckExitCode(report);
|
|
295
|
+
}
|
|
296
|
+
return syncApply(selection.items, context, {
|
|
297
|
+
force: rest.includes("--force"),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
case "help":
|
|
301
|
+
console.log(usage());
|
|
302
|
+
return 0;
|
|
303
|
+
default:
|
|
304
|
+
console.error(`Unknown command: ${command}\n`);
|
|
305
|
+
console.error(usage());
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
main().then(
|
|
311
|
+
(code) => process.exit(code),
|
|
312
|
+
(error) => {
|
|
313
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
);
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `intelligo add <feature>` — generate consumer-owned source.
|
|
3
|
+
*
|
|
4
|
+
* "Owned" is the operative word: the files land in the
|
|
5
|
+
* consumer's repository and Intelligo stops deciding what is in them.
|
|
6
|
+
* The manifest records what was written and its hash so a later
|
|
7
|
+
* upgrade can tell an untouched file from one the consumer has made
|
|
8
|
+
* their own, and refuse to overwrite the latter.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
emptyManifest,
|
|
16
|
+
hashContents,
|
|
17
|
+
readManifest,
|
|
18
|
+
recordFeature,
|
|
19
|
+
writeManifest,
|
|
20
|
+
type GeneratedFile,
|
|
21
|
+
} from "../manifest.js";
|
|
22
|
+
|
|
23
|
+
export type TemplateFile = { template: string; target: string };
|
|
24
|
+
/** A route the feature needs called on a schedule (five-field cron). */
|
|
25
|
+
export type TemplateCron = { path: string; schedule: string };
|
|
26
|
+
export type TemplateFeature = {
|
|
27
|
+
templateVersion: string;
|
|
28
|
+
description: string;
|
|
29
|
+
/** Set when a registry item supersedes this template feature. */
|
|
30
|
+
deprecated?: string;
|
|
31
|
+
files: TemplateFile[];
|
|
32
|
+
cron?: TemplateCron;
|
|
33
|
+
/** What the developer still does by hand once the files are written. */
|
|
34
|
+
nextSteps?: string[];
|
|
35
|
+
};
|
|
36
|
+
export type TemplateCatalogue = Record<string, TemplateFeature>;
|
|
37
|
+
|
|
38
|
+
export function readCatalogue(templatesDir: string): TemplateCatalogue {
|
|
39
|
+
return JSON.parse(
|
|
40
|
+
readFileSync(path.join(templatesDir, "manifest.json"), "utf8")
|
|
41
|
+
) as TemplateCatalogue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type AddResult = {
|
|
45
|
+
feature: string;
|
|
46
|
+
/** The catalogue's deprecation notice, when the feature has one. */
|
|
47
|
+
deprecated?: string;
|
|
48
|
+
written: string[];
|
|
49
|
+
/** Present and modified by the consumer — left untouched. */
|
|
50
|
+
skippedCustomized: string[];
|
|
51
|
+
/** Present but not ours (no manifest record) — left untouched. */
|
|
52
|
+
skippedUnknown: string[];
|
|
53
|
+
/**
|
|
54
|
+
* The feature's schedule and what became of it: `written` to a new
|
|
55
|
+
* vercel.json, already `present` in the app's own, or `manual` — the
|
|
56
|
+
* app has a vercel.json without it, which is the consumer's to edit.
|
|
57
|
+
*/
|
|
58
|
+
cron?: TemplateCron & { status: "written" | "present" | "manual" };
|
|
59
|
+
/** The catalogue's steps left to the developer, printed after the files. */
|
|
60
|
+
nextSteps?: string[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type AddOptions = {
|
|
64
|
+
appRoot: string;
|
|
65
|
+
templatesDir: string;
|
|
66
|
+
frameworkVersion: string;
|
|
67
|
+
/** Re-write files the consumer has customized. Off by default. */
|
|
68
|
+
force?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Placeholder substitutions applied to template contents, e.g.
|
|
71
|
+
* `{ __APP_NAME__: "acme" }`. Applied before hashing, so the
|
|
72
|
+
* recorded hash describes what actually landed on disk.
|
|
73
|
+
*/
|
|
74
|
+
variables?: Record<string, string>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function substitute(
|
|
78
|
+
contents: string,
|
|
79
|
+
variables: Record<string, string> | undefined
|
|
80
|
+
): string {
|
|
81
|
+
if (!variables) return contents;
|
|
82
|
+
return Object.entries(variables).reduce(
|
|
83
|
+
(out, [key, value]) => out.split(key).join(value),
|
|
84
|
+
contents
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const VERCEL_CONFIG = "vercel.json";
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Schedule the cron in vercel.json when the app has none. An existing
|
|
92
|
+
* one is deployment configuration the consumer wrote: it is read to see
|
|
93
|
+
* whether the path is already scheduled, never written. The file is not
|
|
94
|
+
* recorded in the manifest either — it is the consumer's from the start.
|
|
95
|
+
*/
|
|
96
|
+
function scheduleCron(
|
|
97
|
+
appRoot: string,
|
|
98
|
+
cron: TemplateCron
|
|
99
|
+
): "written" | "present" | "manual" {
|
|
100
|
+
const file = path.join(appRoot, VERCEL_CONFIG);
|
|
101
|
+
if (!existsSync(file)) {
|
|
102
|
+
const config = {
|
|
103
|
+
$schema: "https://openapi.vercel.sh/vercel.json",
|
|
104
|
+
crons: [cron],
|
|
105
|
+
};
|
|
106
|
+
writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
|
|
107
|
+
return "written";
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const config = JSON.parse(readFileSync(file, "utf8")) as {
|
|
111
|
+
crons?: Array<{ path?: string }>;
|
|
112
|
+
};
|
|
113
|
+
return config.crons?.some((c) => c.path === cron.path)
|
|
114
|
+
? "present"
|
|
115
|
+
: "manual";
|
|
116
|
+
} catch {
|
|
117
|
+
return "manual";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function addFeature(feature: string, options: AddOptions): AddResult {
|
|
122
|
+
const catalogue = readCatalogue(options.templatesDir);
|
|
123
|
+
const spec = catalogue[feature];
|
|
124
|
+
if (!spec) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Unknown feature "${feature}". Available: ${Object.keys(catalogue).join(", ")}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const manifest =
|
|
131
|
+
readManifest(options.appRoot) ?? emptyManifest(options.frameworkVersion);
|
|
132
|
+
const previous = manifest.features[feature];
|
|
133
|
+
const previousByPath = new Map(
|
|
134
|
+
(previous?.files ?? []).map((f) => [f.path, f])
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const result: AddResult = {
|
|
138
|
+
feature,
|
|
139
|
+
written: [],
|
|
140
|
+
skippedCustomized: [],
|
|
141
|
+
skippedUnknown: [],
|
|
142
|
+
};
|
|
143
|
+
if (spec.deprecated) result.deprecated = spec.deprecated;
|
|
144
|
+
if (spec.nextSteps?.length) result.nextSteps = spec.nextSteps;
|
|
145
|
+
const recorded: GeneratedFile[] = [];
|
|
146
|
+
const handedOver = new Set(previous?.handedOver ?? []);
|
|
147
|
+
|
|
148
|
+
for (const file of spec.files) {
|
|
149
|
+
// A registry item replaced it; `intelligo sync` keeps it now.
|
|
150
|
+
if (handedOver.has(file.target)) continue;
|
|
151
|
+
const source = path.join(options.templatesDir, file.template);
|
|
152
|
+
const target = path.join(options.appRoot, file.target);
|
|
153
|
+
const contents = substitute(
|
|
154
|
+
readFileSync(source, "utf8"),
|
|
155
|
+
options.variables
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (existsSync(target) && !options.force) {
|
|
159
|
+
const current = hashContents(readFileSync(target, "utf8"));
|
|
160
|
+
const known = previousByPath.get(file.target);
|
|
161
|
+
|
|
162
|
+
if (!known) {
|
|
163
|
+
// Not ours: a file we never generated already occupies the
|
|
164
|
+
// path. Overwriting it would destroy work we cannot attribute.
|
|
165
|
+
result.skippedUnknown.push(file.target);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (current !== known.hash) {
|
|
169
|
+
result.skippedCustomized.push(file.target);
|
|
170
|
+
// Keep the ORIGINAL hash: it is the baseline that says "this is
|
|
171
|
+
// customized", and replacing it with the current hash would
|
|
172
|
+
// silently re-adopt the consumer's file as ours.
|
|
173
|
+
recorded.push(known);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
179
|
+
writeFileSync(target, contents);
|
|
180
|
+
recorded.push({ path: file.target, hash: hashContents(contents) });
|
|
181
|
+
result.written.push(file.target);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (spec.cron) {
|
|
185
|
+
result.cron = {
|
|
186
|
+
...spec.cron,
|
|
187
|
+
status: scheduleCron(options.appRoot, spec.cron),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
writeManifest(
|
|
192
|
+
options.appRoot,
|
|
193
|
+
recordFeature(
|
|
194
|
+
manifest,
|
|
195
|
+
feature,
|
|
196
|
+
spec.templateVersion,
|
|
197
|
+
recorded,
|
|
198
|
+
options.variables
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function formatAddResult(r: AddResult): string {
|
|
206
|
+
const lines = [`${r.feature}:`];
|
|
207
|
+
if (r.deprecated) lines.push(` ⚠ deprecated: ${r.deprecated}`);
|
|
208
|
+
for (const f of r.written) lines.push(` + ${f}`);
|
|
209
|
+
for (const f of r.skippedCustomized)
|
|
210
|
+
lines.push(` = ${f} (yours — left alone; use --force to overwrite)`);
|
|
211
|
+
for (const f of r.skippedUnknown)
|
|
212
|
+
lines.push(` ! ${f} (already exists and was not generated by Intelligo)`);
|
|
213
|
+
if (r.cron?.status === "written")
|
|
214
|
+
lines.push(` + ${VERCEL_CONFIG} (${r.cron.path} on "${r.cron.schedule}")`);
|
|
215
|
+
if (
|
|
216
|
+
r.written.length === 0 &&
|
|
217
|
+
r.skippedCustomized.length === 0 &&
|
|
218
|
+
r.skippedUnknown.length === 0 &&
|
|
219
|
+
r.cron?.status !== "written"
|
|
220
|
+
)
|
|
221
|
+
lines.push(" nothing to do");
|
|
222
|
+
if (r.cron) lines.push(...cronNextSteps(r.cron));
|
|
223
|
+
if (r.nextSteps?.length) {
|
|
224
|
+
lines.push("", " Next:", ...r.nextSteps.map((step) => ` - ${step}`));
|
|
225
|
+
}
|
|
226
|
+
return lines.join("\n");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Non-zero when a file the feature needs is still missing: one that
|
|
231
|
+
* exists but was not generated by Intelligo was left alone, so the
|
|
232
|
+
* feature is not installed as the catalogue describes it.
|
|
233
|
+
*/
|
|
234
|
+
export function addExitCode(r: AddResult): number {
|
|
235
|
+
return r.skippedUnknown.length > 0 ? 1 : 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function cronNextSteps(cron: NonNullable<AddResult["cron"]>): string[] {
|
|
239
|
+
const entry = JSON.stringify({ path: cron.path, schedule: cron.schedule });
|
|
240
|
+
const lines = ["", " Next:"];
|
|
241
|
+
if (cron.status === "manual") {
|
|
242
|
+
lines.push(
|
|
243
|
+
` - ${VERCEL_CONFIG} is yours and was left alone. Add to its "crons":`,
|
|
244
|
+
` ${entry}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
lines.push(
|
|
248
|
+
" - Set CRON_SECRET (32+ characters). Vercel sends it as the Bearer token itself.",
|
|
249
|
+
" - Vercel Hobby runs a cron at most once a day and refuses a deployment that",
|
|
250
|
+
` asks for more. On Hobby, or on any other host, remove the ${VERCEL_CONFIG} entry`,
|
|
251
|
+
" and call the route from your own scheduler at the same cadence:",
|
|
252
|
+
` curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://<your-app>${cron.path}`
|
|
253
|
+
);
|
|
254
|
+
return lines;
|
|
255
|
+
}
|