@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
@@ -0,0 +1,284 @@
1
+ /**
2
+ * The conversation around `createApp`: ask what the flags did not say,
3
+ * scaffold, and install the chosen registry items once the developer
4
+ * has approved the commands that do it. The items go in through
5
+ * `intelligo sync` — one `shadcn add` per item from the registry this
6
+ * CLI carries, recorded in the manifest — exactly as a later sync
7
+ * would put them back.
8
+ *
9
+ * Nothing runs on the developer's machine without their say-so. In a
10
+ * terminal the commands are shown and confirmed; without one (CI, a
11
+ * pipe) they run only under `--yes`, and are otherwise printed.
12
+ */
13
+
14
+ import { spawnSync } from "node:child_process";
15
+ import path from "node:path";
16
+
17
+ import * as p from "@clack/prompts";
18
+
19
+ import {
20
+ assertNotOccupied,
21
+ createApp,
22
+ deriveNames,
23
+ formatCreateResult,
24
+ formatNextSteps,
25
+ isOccupied,
26
+ } from "./create.js";
27
+ import { syncApply, type SyncContext } from "./sync.js";
28
+ import { resolveRegistryDir } from "../registry-bundle.js";
29
+ import {
30
+ detectPackageManager,
31
+ findWorkspaceRoot,
32
+ formatCommand,
33
+ installPlan,
34
+ readRegistryCatalogue,
35
+ shortDescription,
36
+ withDependencies,
37
+ type Command,
38
+ type InstallPlan,
39
+ } from "../registry-items.js";
40
+
41
+ export type CreateFlags = {
42
+ target?: string;
43
+ /** `--items a,b` — install these without asking. */
44
+ items?: string[];
45
+ /** `--all` — every page item. */
46
+ all: boolean;
47
+ /** `--yes` — approve the install commands without asking. */
48
+ yes: boolean;
49
+ /** `--no-install` — scaffold only; print the install commands. */
50
+ install: boolean;
51
+ /**
52
+ * `--link-workspace` — for scaffolding inside the Intelligo monorepo,
53
+ * where the packages are workspace members. The default is a version
54
+ * range, because that is what works everywhere else.
55
+ */
56
+ linkWorkspace: boolean;
57
+ };
58
+
59
+ export function parseCreateFlags(args: readonly string[]): CreateFlags {
60
+ let target: string | undefined;
61
+ let items: string[] | undefined;
62
+ for (let i = 0; i < args.length; i++) {
63
+ const arg = args[i]!;
64
+ if (arg === "--items" || arg.startsWith("--items=")) {
65
+ const list = arg === "--items" ? (args[++i] ?? "") : arg.slice(8);
66
+ items = list
67
+ .split(",")
68
+ .map((s) => s.trim())
69
+ .filter(Boolean);
70
+ } else if (!arg.startsWith("-") && target === undefined) {
71
+ target = arg;
72
+ }
73
+ }
74
+ return {
75
+ target,
76
+ items,
77
+ all: args.includes("--all"),
78
+ yes: args.includes("--yes") || args.includes("-y"),
79
+ install: !args.includes("--no-install"),
80
+ linkWorkspace: args.includes("--link-workspace"),
81
+ };
82
+ }
83
+
84
+ /** The pages a first app most likely wants; the picker starts with them ticked. */
85
+ const SUGGESTED = ["app-shell", "auth-login", "auth-signup", "dashboard"];
86
+
87
+ function bail(
88
+ value: unknown,
89
+ message = "Nothing was created."
90
+ ): value is symbol {
91
+ if (!p.isCancel(value)) return false;
92
+ p.cancel(message);
93
+ return true;
94
+ }
95
+
96
+ function run(c: Command, cwd: string): boolean {
97
+ const result = spawnSync(c.command, c.args, {
98
+ cwd,
99
+ stdio: "inherit",
100
+ // pnpm, npx and friends are .cmd shims on Windows.
101
+ shell: process.platform === "win32",
102
+ });
103
+ return result.status === 0;
104
+ }
105
+
106
+ /**
107
+ * Run the plan: the dependency install, then the sync. Returns the
108
+ * commands still to run by hand — none when both succeeded.
109
+ */
110
+ async function installPages(
111
+ plan: InstallPlan,
112
+ options: {
113
+ appRoot: string;
114
+ interactive: boolean;
115
+ show: (c: Command) => string;
116
+ syncContext: SyncContext;
117
+ }
118
+ ): Promise<Command[]> {
119
+ const { appRoot, interactive, show } = options;
120
+ const say = (line: string) =>
121
+ interactive ? p.log.step(line) : console.log(line);
122
+ const fail = (message: string) =>
123
+ interactive ? p.log.error(message) : console.error(message);
124
+ if (plan.install) {
125
+ say(`› ${show(plan.install)}`);
126
+ if (!run(plan.install, plan.install.cwd ?? appRoot)) {
127
+ fail(
128
+ `\`${show(plan.install)}\` failed — the scaffold is in place; run the rest by hand.`
129
+ );
130
+ return [plan.install, plan.sync];
131
+ }
132
+ }
133
+ say(`› ${show(plan.sync)}`);
134
+ const code = await syncApply(plan.items, options.syncContext, {
135
+ force: true,
136
+ log: say,
137
+ });
138
+ if (code !== 0) {
139
+ fail(
140
+ "The pages did not all install — the scaffold is in place; re-run the sync by hand."
141
+ );
142
+ return [plan.sync];
143
+ }
144
+ return [];
145
+ }
146
+
147
+ export async function runCreate(
148
+ flags: CreateFlags,
149
+ context: {
150
+ templatesDir: string;
151
+ frameworkVersion: string;
152
+ /** stdin and stdout are a terminal: prompts are allowed. */
153
+ interactive: boolean;
154
+ }
155
+ ): Promise<number> {
156
+ const { interactive } = context;
157
+ const catalogue = readRegistryCatalogue(context.templatesDir);
158
+ const packageManager = detectPackageManager();
159
+
160
+ if (interactive) p.intro("intelligo create");
161
+
162
+ // 1. Where.
163
+ let target = flags.target;
164
+ if (!target) {
165
+ if (!interactive) {
166
+ console.error(
167
+ "Usage: intelligo create <directory> [--items a,b | --all] [--yes] [--no-install]"
168
+ );
169
+ return 1;
170
+ }
171
+ const answer = await p.text({
172
+ message: "Project name",
173
+ placeholder: "my-app",
174
+ validate: (v) => {
175
+ if (!v) return "A name is required.";
176
+ try {
177
+ deriveNames(v);
178
+ } catch (error) {
179
+ return (error as Error).message;
180
+ }
181
+ return isOccupied(v) ? `${v} is not empty.` : undefined;
182
+ },
183
+ });
184
+ if (bail(answer)) return 1;
185
+ target = answer;
186
+ }
187
+ // Before any question about items, so nobody chooses pages for an
188
+ // app that is then refused.
189
+ assertNotOccupied(target);
190
+
191
+ // 2. What.
192
+ let selected: string[] = [];
193
+ if (flags.all) selected = Object.keys(catalogue.items);
194
+ else if (flags.items) selected = flags.items;
195
+ else if (interactive) {
196
+ const answer = await p.multiselect({
197
+ message: "Which pages should be installed? (space to toggle)",
198
+ options: Object.entries(catalogue.items).map(([name, item]) => ({
199
+ value: name,
200
+ label: item.title,
201
+ hint: shortDescription(item.description),
202
+ })),
203
+ initialValues: SUGGESTED.filter((n) => n in catalogue.items),
204
+ required: false,
205
+ });
206
+ if (bail(answer)) return 1;
207
+ selected = answer;
208
+ }
209
+ const items = withDependencies(selected, catalogue.requires);
210
+ const added = items.filter((n) => !selected.includes(n));
211
+ if (interactive && added.length > 0) {
212
+ p.log.info(`Also installing what they build on: ${added.join(", ")}`);
213
+ }
214
+
215
+ // 3. The scaffold.
216
+ const result = createApp({
217
+ target,
218
+ templatesDir: context.templatesDir,
219
+ frameworkVersion: context.frameworkVersion,
220
+ linkWorkspace: flags.linkWorkspace,
221
+ });
222
+ const appRoot = path.resolve(target);
223
+ if (interactive) {
224
+ p.log.success(`Scaffolded ${result.written.length} files in ${target}`);
225
+ }
226
+
227
+ // 4. The pages — only with approval.
228
+ const workspaceRoot = findWorkspaceRoot(appRoot);
229
+ const plan = installPlan(items, { appRoot, packageManager, workspaceRoot });
230
+ const commands: Command[] = plan
231
+ ? [...(plan.install ? [plan.install] : []), plan.sync]
232
+ : [];
233
+ const show = (c: Command) => formatCommand(c, appRoot);
234
+ const registryDir = resolveRegistryDir(context.templatesDir);
235
+ if (plan && flags.install && !registryDir) {
236
+ console.error(
237
+ "This CLI has no bundled registry to install pages from — in the framework repository, run `pnpm registry:build` first."
238
+ );
239
+ }
240
+ let approved = plan !== null && flags.install && flags.yes && !!registryDir;
241
+ if (plan && flags.install && registryDir && !flags.yes && interactive) {
242
+ p.note(commands.map(show).join("\n"), "Commands to run in " + target);
243
+ const answer = await p.confirm({
244
+ message: plan.install
245
+ ? `shadcn is not installed yet — install the dependencies (shadcn included) with ${plan.install.command}${workspaceRoot ? " from the workspace root" : ""} and add ${items.length} page(s)?`
246
+ : `Add ${items.length} page(s) with shadcn?`,
247
+ });
248
+ if (bail(answer, `The scaffold is in ${target}; nothing was installed.`))
249
+ return 1;
250
+ approved = answer;
251
+ }
252
+
253
+ const pending =
254
+ approved && plan && registryDir
255
+ ? await installPages(plan, {
256
+ appRoot,
257
+ interactive,
258
+ show,
259
+ syncContext: {
260
+ appRoot,
261
+ registryDir,
262
+ requires: catalogue.requires,
263
+ frameworkVersion: context.frameworkVersion,
264
+ },
265
+ })
266
+ : commands;
267
+
268
+ const next = {
269
+ packageManager: workspaceRoot ? ("pnpm" as const) : packageManager,
270
+ installed: approved && pending.length === 0,
271
+ pending,
272
+ workspaceRoot,
273
+ };
274
+ if (interactive) {
275
+ p.note(formatNextSteps(target, next), "Next");
276
+ p.outro(
277
+ next.installed ? `${items.length} page(s) installed.` : "Scaffold ready."
278
+ );
279
+ } else {
280
+ console.log(formatCreateResult(target, result, next));
281
+ }
282
+ // A failed install is a failure; a declined one is a choice.
283
+ return approved && pending.length > 0 ? 1 : 0;
284
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * `intelligo create <name>` — a new application on the framework.
3
+ *
4
+ * This is onboarding, not the product boundary: what it writes is the
5
+ * consumer's from the moment it lands, and the enduring relationship is the versioned packages, not this
6
+ * scaffold. So it generates through the same manifest machinery as
7
+ * `add`, which means the very first upgrade already knows which files
8
+ * you have since edited.
9
+ *
10
+ * In a terminal it asks for the project name when none is given, then
11
+ * which registry pages to install (`--items a,b` or `--all` answer that
12
+ * without asking). The pages are installed by `intelligo sync` — one
13
+ * `shadcn add` per item, from the registry this CLI carries — after the
14
+ * dependencies (from the root of a parent pnpm workspace when the app is
15
+ * a member of one), and only once you approve the exact commands, or
16
+ * pass `--yes`. `--no-install` stops after the scaffold and prints them
17
+ * instead.
18
+ */
19
+
20
+ import { existsSync, mkdirSync, readdirSync } from "node:fs";
21
+ import path from "node:path";
22
+
23
+ import { addFeature, formatAddResult, type AddResult } from "./add.js";
24
+ import {
25
+ formatCommand,
26
+ type Command,
27
+ type PackageManager,
28
+ } from "../registry-items.js";
29
+
30
+ export type CreateOptions = {
31
+ /** Directory to create the app in; created if absent. */
32
+ target: string;
33
+ templatesDir: string;
34
+ frameworkVersion: string;
35
+ /**
36
+ * Emit `workspace:*` for the @intelligo-dev/* dependencies instead of a
37
+ * version range. Only meaningful when scaffolding inside this
38
+ * monorepo — outside it, pnpm fails the install with "workspace
39
+ * protocol used outside a workspace".
40
+ */
41
+ linkWorkspace?: boolean;
42
+ };
43
+
44
+ /** A package name and product slug derived from the directory name. */
45
+ export function deriveNames(target: string): {
46
+ appName: string;
47
+ appSlug: string;
48
+ } {
49
+ const base = path.basename(path.resolve(target));
50
+ const slug = base
51
+ .toLowerCase()
52
+ .replace(/[^a-z0-9]+/g, "-")
53
+ .replace(/^-+|-+$/g, "");
54
+ if (!slug) {
55
+ throw new Error(`Cannot derive a project name from "${target}".`);
56
+ }
57
+ return { appName: slug, appSlug: slug };
58
+ }
59
+
60
+ export function isOccupied(target: string): boolean {
61
+ const dir = path.resolve(target);
62
+ return existsSync(dir) && readdirSync(dir).length > 0;
63
+ }
64
+
65
+ /** Scaffolding into an occupied directory is how people lose work. */
66
+ export function assertNotOccupied(target: string): void {
67
+ if (isOccupied(target)) {
68
+ throw new Error(
69
+ `${path.resolve(target)} is not empty. Create the app in a new directory, or use ` +
70
+ `\`intelligo add app-scaffold\` inside an existing one.`
71
+ );
72
+ }
73
+ }
74
+
75
+ export function createApp(options: CreateOptions): AddResult {
76
+ const target = path.resolve(options.target);
77
+
78
+ assertNotOccupied(target);
79
+ mkdirSync(target, { recursive: true });
80
+
81
+ const { appName, appSlug } = deriveNames(target);
82
+
83
+ return addFeature("app-scaffold", {
84
+ appRoot: target,
85
+ templatesDir: options.templatesDir,
86
+ frameworkVersion: options.frameworkVersion,
87
+ variables: {
88
+ __APP_NAME__: appName,
89
+ __APP_SLUG__: appSlug,
90
+ __INTELLIGO_DEP__: options.linkWorkspace
91
+ ? "workspace:*"
92
+ : `^${options.frameworkVersion}`,
93
+ },
94
+ });
95
+ }
96
+
97
+ export type NextSteps = {
98
+ packageManager: PackageManager;
99
+ /** Whether `create` already installed the dependencies. */
100
+ installed: boolean;
101
+ /** Install commands the developer declined, or that did not get to run. */
102
+ pending?: Command[];
103
+ /** The parent pnpm workspace the app is a member of, installed from its root. */
104
+ workspaceRoot?: string | null;
105
+ };
106
+
107
+ /** A path the reader can paste into a shell, spaces and quotes included. */
108
+ function shellQuote(value: string): string {
109
+ return /^[\w@%+=:,./-]+$/.test(value)
110
+ ? value
111
+ : `'${value.replace(/'/g, "'\\''")}'`;
112
+ }
113
+
114
+ export function formatNextSteps(target: string, next: NextSteps): string {
115
+ const pm = next.packageManager;
116
+ const appRoot = path.resolve(target);
117
+ const install: Command = {
118
+ command: pm,
119
+ args: ["install"],
120
+ cwd: next.workspaceRoot ?? appRoot,
121
+ };
122
+ return [
123
+ `cd ${shellQuote(path.relative(process.cwd(), appRoot) || ".")}`,
124
+ "cp .env.example .env.local # then fill it in",
125
+ ...(next.pending?.length
126
+ ? next.pending.map((c) => formatCommand(c, appRoot))
127
+ : next.installed
128
+ ? []
129
+ : [formatCommand(install, appRoot)]),
130
+ `${pm === "npm" ? "npm run" : pm} dev`,
131
+ ].join("\n");
132
+ }
133
+
134
+ export function formatCreateResult(
135
+ target: string,
136
+ r: AddResult,
137
+ next: NextSteps = { packageManager: "pnpm", installed: false }
138
+ ): string {
139
+ return [
140
+ formatAddResult(r),
141
+ "",
142
+ "Next:",
143
+ ...formatNextSteps(target, next)
144
+ .split("\n")
145
+ .map((line) => ` ${line}`),
146
+ "",
147
+ "`intelligo doctor` will tell you what is still missing.",
148
+ ].join("\n");
149
+ }