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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +102 -0
  3. package/dist/bin.js +122 -37
  4. package/dist/bin.js.map +1 -1
  5. package/dist/commands/add.d.ts +26 -1
  6. package/dist/commands/add.d.ts.map +1 -1
  7. package/dist/commands/add.js +70 -4
  8. package/dist/commands/add.js.map +1 -1
  9. package/dist/commands/create-flow.d.ts +37 -0
  10. package/dist/commands/create-flow.d.ts.map +1 -0
  11. package/dist/commands/create-flow.js +212 -0
  12. package/dist/commands/create-flow.js.map +1 -0
  13. package/dist/commands/create.d.ts +27 -6
  14. package/dist/commands/create.d.ts.map +1 -1
  15. package/dist/commands/create.js +53 -13
  16. package/dist/commands/create.js.map +1 -1
  17. package/dist/commands/doctor.d.ts +14 -3
  18. package/dist/commands/doctor.d.ts.map +1 -1
  19. package/dist/commands/doctor.js +295 -37
  20. package/dist/commands/doctor.js.map +1 -1
  21. package/dist/commands/migrate-check.d.ts +73 -1
  22. package/dist/commands/migrate-check.d.ts.map +1 -1
  23. package/dist/commands/migrate-check.js +130 -5
  24. package/dist/commands/migrate-check.js.map +1 -1
  25. package/dist/commands/migrate.d.ts +97 -0
  26. package/dist/commands/migrate.d.ts.map +1 -0
  27. package/dist/commands/migrate.js +141 -0
  28. package/dist/commands/migrate.js.map +1 -0
  29. package/dist/commands/sync-messages.d.ts +37 -0
  30. package/dist/commands/sync-messages.d.ts.map +1 -0
  31. package/dist/commands/sync-messages.js +88 -0
  32. package/dist/commands/sync-messages.js.map +1 -0
  33. package/dist/commands/sync-scaffold.d.ts +36 -0
  34. package/dist/commands/sync-scaffold.d.ts.map +1 -0
  35. package/dist/commands/sync-scaffold.js +53 -0
  36. package/dist/commands/sync-scaffold.js.map +1 -0
  37. package/dist/commands/sync.d.ts +103 -0
  38. package/dist/commands/sync.d.ts.map +1 -0
  39. package/dist/commands/sync.js +361 -0
  40. package/dist/commands/sync.js.map +1 -0
  41. package/dist/commands/upgrade-check.d.ts +5 -5
  42. package/dist/commands/upgrade-check.d.ts.map +1 -1
  43. package/dist/commands/upgrade-check.js +22 -6
  44. package/dist/commands/upgrade-check.js.map +1 -1
  45. package/dist/env-files.d.ts +12 -0
  46. package/dist/env-files.d.ts.map +1 -0
  47. package/dist/env-files.js +29 -0
  48. package/dist/env-files.js.map +1 -0
  49. package/dist/index.d.ts +9 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +9 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/manifest.d.ts +36 -15
  54. package/dist/manifest.d.ts.map +1 -1
  55. package/dist/manifest.js +32 -16
  56. package/dist/manifest.js.map +1 -1
  57. package/dist/migrations.d.ts +3 -6
  58. package/dist/migrations.d.ts.map +1 -1
  59. package/dist/migrations.js +3 -6
  60. package/dist/migrations.js.map +1 -1
  61. package/dist/model-catalogue.d.ts +14 -0
  62. package/dist/model-catalogue.d.ts.map +1 -0
  63. package/dist/model-catalogue.js +34 -0
  64. package/dist/model-catalogue.js.map +1 -0
  65. package/dist/module-exports.d.ts +63 -0
  66. package/dist/module-exports.d.ts.map +1 -0
  67. package/dist/module-exports.js +231 -0
  68. package/dist/module-exports.js.map +1 -0
  69. package/dist/registry-bundle.d.ts +43 -0
  70. package/dist/registry-bundle.d.ts.map +1 -0
  71. package/dist/registry-bundle.js +74 -0
  72. package/dist/registry-bundle.js.map +1 -0
  73. package/dist/registry-items.d.ts +84 -0
  74. package/dist/registry-items.d.ts.map +1 -0
  75. package/dist/registry-items.js +187 -0
  76. package/dist/registry-items.js.map +1 -0
  77. package/package.json +36 -10
  78. package/src/bin.ts +316 -0
  79. package/src/commands/add.ts +255 -0
  80. package/src/commands/create-flow.ts +284 -0
  81. package/src/commands/create.ts +149 -0
  82. package/src/commands/doctor.ts +487 -0
  83. package/src/commands/migrate-check.ts +298 -0
  84. package/src/commands/migrate.ts +228 -0
  85. package/src/commands/sync-messages.ts +114 -0
  86. package/src/commands/sync-scaffold.ts +83 -0
  87. package/src/commands/sync.ts +534 -0
  88. package/src/commands/upgrade-check.ts +160 -0
  89. package/src/env-files.ts +31 -0
  90. package/src/index.ts +39 -0
  91. package/src/manifest.ts +147 -0
  92. package/src/migrations-dir.ts +25 -0
  93. package/src/migrations.ts +133 -0
  94. package/src/model-catalogue.ts +35 -0
  95. package/src/module-exports.ts +266 -0
  96. package/src/registry-bundle.ts +103 -0
  97. package/src/registry-items.ts +237 -0
  98. package/templates/admin-page/admin-page.tsx.tpl +2 -3
  99. package/templates/app-scaffold/assistant-route.ts.tpl +2 -2
  100. package/templates/app-scaffold/auth-route.ts.tpl +10 -0
  101. package/templates/app-scaffold/components.json.tpl +6 -2
  102. package/templates/app-scaffold/db-schema.ts.tpl +29 -0
  103. package/templates/app-scaffold/drizzle-journal.json.tpl +5 -0
  104. package/templates/app-scaffold/drizzle.config.ts.tpl +45 -0
  105. package/templates/app-scaffold/env.example.tpl +65 -1
  106. package/templates/app-scaffold/globals.css.tpl +172 -281
  107. package/templates/app-scaffold/i18n-request.ts.tpl +25 -0
  108. package/templates/app-scaffold/instrumentation.ts.tpl +21 -0
  109. package/templates/app-scaffold/intelligo.ts.tpl +95 -41
  110. package/templates/app-scaffold/layout.tsx.tpl +18 -1
  111. package/templates/app-scaffold/lib-utils.ts.tpl +1 -6
  112. package/templates/app-scaffold/next.config.mjs.tpl +2 -0
  113. package/templates/app-scaffold/package.json.tpl +31 -17
  114. package/templates/app-scaffold/page.tsx.tpl +16 -0
  115. package/templates/app-scaffold/plans.ts.tpl +32 -13
  116. package/templates/app-scaffold/{middleware.ts.tpl → proxy.ts.tpl} +6 -0
  117. package/templates/app-scaffold/sonner.tsx.tpl +42 -20
  118. package/templates/app-scaffold/stripe-webhook-route.ts.tpl +24 -0
  119. package/templates/app-scaffold/theme-provider.tsx.tpl +6 -4
  120. package/templates/app-scaffold/tsconfig.json.tpl +20 -4
  121. package/templates/app-scaffold/use-mobile.ts.tpl +19 -0
  122. package/templates/app-scaffold/workspace-bootstrap.ts.tpl +30 -0
  123. package/templates/maintenance/maintenance-route.ts.tpl +119 -0
  124. package/templates/manifest.json +75 -17
  125. package/templates/registry/ai-agent-activity.json +23 -0
  126. package/templates/registry/ai-agent-progress.json +21 -0
  127. package/templates/registry/ai-approval-card.json +27 -0
  128. package/templates/registry/ai-artifact.json +22 -0
  129. package/templates/registry/ai-branch.json +21 -0
  130. package/templates/registry/ai-citations.json +24 -0
  131. package/templates/registry/ai-code-block.json +23 -0
  132. package/templates/registry/ai-composer-menu.json +19 -0
  133. package/templates/registry/ai-file-diff.json +25 -0
  134. package/templates/registry/ai-image-generation.json +23 -0
  135. package/templates/registry/ai-markdown.json +23 -0
  136. package/templates/registry/ai-message-bubble.json +23 -0
  137. package/templates/registry/ai-message-scroller.json +22 -0
  138. package/templates/registry/ai-message.json +21 -0
  139. package/templates/registry/ai-motion.json +19 -0
  140. package/templates/registry/ai-prompt-input.json +28 -0
  141. package/templates/registry/ai-reasoning-text.json +22 -0
  142. package/templates/registry/ai-reasoning.json +22 -0
  143. package/templates/registry/ai-shimmer-text.json +19 -0
  144. package/templates/registry/ai-sidebar.json +25 -0
  145. package/templates/registry/ai-speech-input.json +21 -0
  146. package/templates/registry/ai-streaming-response.json +24 -0
  147. package/templates/registry/ai-suggestion.json +19 -0
  148. package/templates/registry/ai-todo-list.json +22 -0
  149. package/templates/registry/ai-tool-approval.json +26 -0
  150. package/templates/registry/ai-tool-result.json +25 -0
  151. package/templates/registry/alert-dialog.json +23 -0
  152. package/templates/registry/animated-list.json +21 -0
  153. package/templates/registry/app-shell.json +93 -0
  154. package/templates/registry/artifacts.json +82 -0
  155. package/templates/registry/attachment.json +22 -0
  156. package/templates/registry/auth-email-verification.json +39 -0
  157. package/templates/registry/auth-login.json +72 -0
  158. package/templates/registry/auth-password-reset.json +53 -0
  159. package/templates/registry/auth-signup.json +41 -0
  160. package/templates/registry/billing-settings.json +60 -0
  161. package/templates/registry/button.json +22 -0
  162. package/templates/registry/chat-eve.json +19 -0
  163. package/templates/registry/chat-panel.json +30 -0
  164. package/templates/registry/chat-share.json +42 -0
  165. package/templates/registry/chat-widget.json +34 -0
  166. package/templates/registry/chat.json +326 -0
  167. package/templates/registry/checkbox.json +22 -0
  168. package/templates/registry/checkout.json +40 -0
  169. package/templates/registry/collapsible.json +19 -0
  170. package/templates/registry/command.json +23 -0
  171. package/templates/registry/copy-button.json +21 -0
  172. package/templates/registry/dashboard.json +69 -0
  173. package/templates/registry/dialog.json +24 -0
  174. package/templates/registry/document-viewer.json +22 -0
  175. package/templates/registry/dropdown-menu.json +23 -0
  176. package/templates/registry/expandable-tabs.json +22 -0
  177. package/templates/registry/feature-gating.json +73 -0
  178. package/templates/registry/file-upload.json +24 -0
  179. package/templates/registry/hold-action-button.json +22 -0
  180. package/templates/registry/input-group.json +23 -0
  181. package/templates/registry/input.json +19 -0
  182. package/templates/registry/intelligo.json +187 -0
  183. package/templates/registry/invitation-accept.json +64 -0
  184. package/templates/registry/language-switcher.json +29 -0
  185. package/templates/registry/morphing-modal.json +24 -0
  186. package/templates/registry/notification-stack.json +24 -0
  187. package/templates/registry/notifications.json +87 -0
  188. package/templates/registry/onboarding.json +83 -0
  189. package/templates/registry/otp-input.json +22 -0
  190. package/templates/registry/page-header.json +15 -0
  191. package/templates/registry/payment-poll.json +52 -0
  192. package/templates/registry/popover-morph.json +22 -0
  193. package/templates/registry/popover.json +22 -0
  194. package/templates/registry/pricing.json +111 -0
  195. package/templates/registry/privacy-settings.json +65 -0
  196. package/templates/registry/profile-settings.json +68 -0
  197. package/templates/registry/progress.json +19 -0
  198. package/templates/registry/radio-group.json +22 -0
  199. package/templates/registry/registry.json +2945 -0
  200. package/templates/registry/route-error.json +65 -0
  201. package/templates/registry/select-morph.json +23 -0
  202. package/templates/registry/select.json +23 -0
  203. package/templates/registry/settings-shell.json +47 -0
  204. package/templates/registry/sheet.json +24 -0
  205. package/templates/registry/sidebar.json +28 -0
  206. package/templates/registry/smoke.json +34 -0
  207. package/templates/registry/spinner.json +16 -0
  208. package/templates/registry/stat-card.json +18 -0
  209. package/templates/registry/status-badge.json +18 -0
  210. package/templates/registry/switch.json +20 -0
  211. package/templates/registry/tabs.json +23 -0
  212. package/templates/registry/team-settings.json +97 -0
  213. package/templates/registry/textarea.json +16 -0
  214. package/templates/registry/tooltip.json +22 -0
  215. package/templates/registry/trial-banner.json +43 -0
  216. package/templates/registry/usage.json +84 -0
  217. package/templates/registry/workspace-settings.json +71 -0
  218. package/templates/registry-items.json +116 -0
  219. package/templates/registry-requires.json +180 -0
  220. package/templates/usage-page/usage-page.tsx.tpl +23 -8
  221. package/templates/vitest/server-only.ts.tpl +7 -0
  222. package/templates/vitest/vitest.config.ts.tpl +37 -0
  223. package/templates/billing-page/billing-page.tsx.tpl +0 -72
package/package.json CHANGED
@@ -1,15 +1,28 @@
1
1
  {
2
2
  "name": "@intelligo-dev/cli",
3
- "version": "1.0.0-beta.1",
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-mn/framework.git",
20
+ "url": "git+https://github.com/intelligo-dev/intelligo.git",
8
21
  "directory": "packages/cli"
9
22
  },
10
- "homepage": "https://github.com/intelligo-mn/framework#readme",
23
+ "homepage": "https://intelligo.dev/docs/packages/cli",
11
24
  "bugs": {
12
- "url": "https://github.com/intelligo-mn/framework/issues"
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
- "pg": "^8.18.0"
38
+ "@clack/prompts": "^1.8.1",
39
+ "pg": "8.18.0"
26
40
  },
27
41
  "devDependencies": {
28
- "@types/node": "^22.0.0",
29
- "@types/pg": "^8.16.0",
30
- "typescript": "^5.7.2"
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
- "templates"
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
+ }