@intelligo-dev/cli 1.0.0-beta.13 → 1.0.0-beta.15
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/README.md +65 -2
- package/dist/args.d.ts +10 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +20 -0
- package/dist/args.js.map +1 -0
- package/dist/bin.js +103 -22
- package/dist/bin.js.map +1 -1
- package/dist/commands/add.d.ts +4 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +28 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/create-flow.d.ts +8 -1
- package/dist/commands/create-flow.d.ts.map +1 -1
- package/dist/commands/create-flow.js +101 -34
- package/dist/commands/create-flow.js.map +1 -1
- package/dist/commands/create.d.ts +24 -4
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +57 -14
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +123 -35
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/migrate-check.d.ts +20 -1
- package/dist/commands/migrate-check.d.ts.map +1 -1
- package/dist/commands/migrate-check.js +35 -6
- package/dist/commands/migrate-check.js.map +1 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +2 -8
- package/dist/commands/migrate.js.map +1 -1
- 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 +109 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +392 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/upgrade-check.d.ts.map +1 -1
- package/dist/commands/upgrade-check.js +4 -1
- package/dist/commands/upgrade-check.js.map +1 -1
- package/dist/env-files.d.ts +11 -0
- package/dist/env-files.d.ts.map +1 -1
- package/dist/env-files.js +28 -9
- package/dist/env-files.js.map +1 -1
- package/dist/manifest.d.ts +27 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +30 -3
- package/dist/manifest.js.map +1 -1
- 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 +41 -13
- package/dist/registry-items.d.ts.map +1 -1
- package/dist/registry-items.js +117 -33
- package/dist/registry-items.js.map +1 -1
- package/package.json +1 -1
- package/src/args.ts +25 -0
- package/src/bin.ts +124 -22
- package/src/commands/add.ts +39 -12
- package/src/commands/create-flow.ts +119 -29
- package/src/commands/create.ts +82 -14
- package/src/commands/doctor.ts +156 -47
- package/src/commands/migrate-check.ts +51 -10
- package/src/commands/migrate.ts +2 -8
- package/src/commands/sync-messages.ts +114 -0
- package/src/commands/sync-scaffold.ts +83 -0
- package/src/commands/sync.ts +574 -0
- package/src/commands/upgrade-check.ts +4 -1
- package/src/env-files.ts +28 -7
- package/src/manifest.ts +57 -3
- package/src/module-exports.ts +266 -0
- package/src/registry-bundle.ts +103 -0
- package/src/registry-items.ts +141 -36
- package/templates/admin-page/admin-page.tsx.tpl +96 -24
- package/templates/app-scaffold/gitignore.tpl +25 -0
- package/templates/app-scaffold/intelligo.ts.tpl +6 -47
- package/templates/app-scaffold/next.config.mjs.tpl +31 -2
- package/templates/app-scaffold/package.json.tpl +2 -2
- package/templates/manifest.json +40 -3
- package/templates/pnpm-standalone/npmrc.tpl +6 -0
- package/templates/pnpm-standalone/pnpm-workspace.yaml.tpl +10 -0
- 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 +66 -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 +58 -0
- package/templates/registry/popover-morph.json +22 -0
- package/templates/registry/popover.json +22 -0
- package/templates/registry/pricing.json +117 -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 +2960 -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 +3 -3
- package/templates/registry-requires.json +30 -4
- package/templates/vitest/server-only.ts.tpl +7 -0
- package/templates/vitest/vitest.config.ts.tpl +37 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The conversation around `createApp`: ask what the flags did not say,
|
|
3
3
|
* scaffold, and install the chosen registry items once the developer
|
|
4
|
-
* has approved the commands that do it.
|
|
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.
|
|
5
8
|
*
|
|
6
9
|
* Nothing runs on the developer's machine without their say-so. In a
|
|
7
10
|
* terminal the commands are shown and confirmed; without one (CI, a
|
|
@@ -21,15 +24,18 @@ import {
|
|
|
21
24
|
formatNextSteps,
|
|
22
25
|
isOccupied,
|
|
23
26
|
} from "./create.js";
|
|
27
|
+
import { recordItems, syncApply, type SyncContext } from "./sync.js";
|
|
28
|
+
import { resolveRegistryDir } from "../registry-bundle.js";
|
|
24
29
|
import {
|
|
25
30
|
detectPackageManager,
|
|
31
|
+
findWorkspaceRoot,
|
|
26
32
|
formatCommand,
|
|
27
|
-
hasLocalShadcn,
|
|
28
33
|
installPlan,
|
|
29
34
|
readRegistryCatalogue,
|
|
30
35
|
shortDescription,
|
|
31
36
|
withDependencies,
|
|
32
37
|
type Command,
|
|
38
|
+
type InstallPlan,
|
|
33
39
|
} from "../registry-items.js";
|
|
34
40
|
|
|
35
41
|
export type CreateFlags = {
|
|
@@ -48,11 +54,25 @@ export type CreateFlags = {
|
|
|
48
54
|
* range, because that is what works everywhere else.
|
|
49
55
|
*/
|
|
50
56
|
linkWorkspace: boolean;
|
|
57
|
+
/** `--name <name>` — the project name, when it is not the directory's. */
|
|
58
|
+
name?: string;
|
|
59
|
+
/** Flags `create` does not know. */
|
|
60
|
+
unknown: string[];
|
|
51
61
|
};
|
|
52
62
|
|
|
63
|
+
const CREATE_FLAGS = [
|
|
64
|
+
"--all",
|
|
65
|
+
"--yes",
|
|
66
|
+
"-y",
|
|
67
|
+
"--no-install",
|
|
68
|
+
"--link-workspace",
|
|
69
|
+
];
|
|
70
|
+
|
|
53
71
|
export function parseCreateFlags(args: readonly string[]): CreateFlags {
|
|
54
72
|
let target: string | undefined;
|
|
55
73
|
let items: string[] | undefined;
|
|
74
|
+
let name: string | undefined;
|
|
75
|
+
const unknown: string[] = [];
|
|
56
76
|
for (let i = 0; i < args.length; i++) {
|
|
57
77
|
const arg = args[i]!;
|
|
58
78
|
if (arg === "--items" || arg.startsWith("--items=")) {
|
|
@@ -61,13 +81,23 @@ export function parseCreateFlags(args: readonly string[]): CreateFlags {
|
|
|
61
81
|
.split(",")
|
|
62
82
|
.map((s) => s.trim())
|
|
63
83
|
.filter(Boolean);
|
|
64
|
-
} else if (
|
|
84
|
+
} else if (arg === "--name" || arg.startsWith("--name=")) {
|
|
85
|
+
const value = arg === "--name" ? args[++i] : arg.slice(7);
|
|
86
|
+
if (!value?.trim() || value.startsWith("-")) unknown.push(arg);
|
|
87
|
+
else name = value;
|
|
88
|
+
} else if (arg.startsWith("-")) {
|
|
89
|
+
if (!CREATE_FLAGS.includes(arg)) unknown.push(arg);
|
|
90
|
+
} else if (target === undefined) {
|
|
65
91
|
target = arg;
|
|
92
|
+
} else {
|
|
93
|
+
unknown.push(arg);
|
|
66
94
|
}
|
|
67
95
|
}
|
|
68
96
|
return {
|
|
69
97
|
target,
|
|
70
98
|
items,
|
|
99
|
+
name,
|
|
100
|
+
unknown,
|
|
71
101
|
all: args.includes("--all"),
|
|
72
102
|
yes: args.includes("--yes") || args.includes("-y"),
|
|
73
103
|
install: !args.includes("--no-install"),
|
|
@@ -97,6 +127,47 @@ function run(c: Command, cwd: string): boolean {
|
|
|
97
127
|
return result.status === 0;
|
|
98
128
|
}
|
|
99
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Run the plan: the dependency install, then the sync. Returns the
|
|
132
|
+
* commands still to run by hand — none when both succeeded.
|
|
133
|
+
*/
|
|
134
|
+
async function installPages(
|
|
135
|
+
plan: InstallPlan,
|
|
136
|
+
options: {
|
|
137
|
+
appRoot: string;
|
|
138
|
+
interactive: boolean;
|
|
139
|
+
show: (c: Command) => string;
|
|
140
|
+
syncContext: SyncContext;
|
|
141
|
+
}
|
|
142
|
+
): Promise<Command[]> {
|
|
143
|
+
const { appRoot, interactive, show } = options;
|
|
144
|
+
const say = (line: string) =>
|
|
145
|
+
interactive ? p.log.step(line) : console.log(line);
|
|
146
|
+
const fail = (message: string) =>
|
|
147
|
+
interactive ? p.log.error(message) : console.error(message);
|
|
148
|
+
if (plan.install) {
|
|
149
|
+
say(`› ${show(plan.install)}`);
|
|
150
|
+
if (!run(plan.install, plan.install.cwd ?? appRoot)) {
|
|
151
|
+
fail(
|
|
152
|
+
`\`${show(plan.install)}\` failed — the scaffold is in place; run the rest by hand.`
|
|
153
|
+
);
|
|
154
|
+
return [plan.install, plan.sync];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
say(`› ${show(plan.sync)}`);
|
|
158
|
+
const code = await syncApply(plan.items, options.syncContext, {
|
|
159
|
+
force: true,
|
|
160
|
+
log: say,
|
|
161
|
+
});
|
|
162
|
+
if (code !== 0) {
|
|
163
|
+
fail(
|
|
164
|
+
"The pages did not all install — the scaffold is in place; re-run the sync by hand."
|
|
165
|
+
);
|
|
166
|
+
return [plan.sync];
|
|
167
|
+
}
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
|
|
100
171
|
export async function runCreate(
|
|
101
172
|
flags: CreateFlags,
|
|
102
173
|
context: {
|
|
@@ -117,7 +188,7 @@ export async function runCreate(
|
|
|
117
188
|
if (!target) {
|
|
118
189
|
if (!interactive) {
|
|
119
190
|
console.error(
|
|
120
|
-
"Usage: intelligo create <directory> [--items a,b | --all] [--yes] [--no-install]"
|
|
191
|
+
"Usage: intelligo create <directory> [--items a,b | --all] [--yes] [--no-install] [--name <name>]"
|
|
121
192
|
);
|
|
122
193
|
return 1;
|
|
123
194
|
}
|
|
@@ -171,21 +242,41 @@ export async function runCreate(
|
|
|
171
242
|
templatesDir: context.templatesDir,
|
|
172
243
|
frameworkVersion: context.frameworkVersion,
|
|
173
244
|
linkWorkspace: flags.linkWorkspace,
|
|
245
|
+
name: flags.name,
|
|
246
|
+
packageManager,
|
|
174
247
|
});
|
|
175
248
|
const appRoot = path.resolve(target);
|
|
176
249
|
if (interactive) {
|
|
177
250
|
p.log.success(`Scaffolded ${result.written.length} files in ${target}`);
|
|
178
251
|
}
|
|
179
252
|
|
|
180
|
-
// 4. The pages — only with approval
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
253
|
+
// 4. The pages — only with approval, but recorded now, so a bare
|
|
254
|
+
// `intelligo sync` installs them if this install does not.
|
|
255
|
+
const workspaceRoot = findWorkspaceRoot(appRoot);
|
|
256
|
+
const plan = installPlan(items, { appRoot, packageManager, workspaceRoot });
|
|
257
|
+
if (plan) {
|
|
258
|
+
recordItems(plan.items, {
|
|
259
|
+
appRoot,
|
|
260
|
+
requires: catalogue.requires,
|
|
261
|
+
frameworkVersion: context.frameworkVersion,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
const commands: Command[] = plan
|
|
265
|
+
? [...(plan.install ? [plan.install] : []), plan.sync]
|
|
266
|
+
: [];
|
|
267
|
+
const show = (c: Command) => formatCommand(c, appRoot);
|
|
268
|
+
const registryDir = resolveRegistryDir(context.templatesDir);
|
|
269
|
+
if (plan && flags.install && !registryDir) {
|
|
270
|
+
console.error(
|
|
271
|
+
"This CLI has no bundled registry to install pages from — in the framework repository, run `pnpm registry:build` first."
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
let approved = plan !== null && flags.install && flags.yes && !!registryDir;
|
|
275
|
+
if (plan && flags.install && registryDir && !flags.yes && interactive) {
|
|
276
|
+
p.note(commands.map(show).join("\n"), "Commands to run in " + target);
|
|
186
277
|
const answer = await p.confirm({
|
|
187
|
-
message:
|
|
188
|
-
? `shadcn is not installed yet — install the dependencies (shadcn included) with ${
|
|
278
|
+
message: plan.install
|
|
279
|
+
? `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)?`
|
|
189
280
|
: `Add ${items.length} page(s) with shadcn?`,
|
|
190
281
|
});
|
|
191
282
|
if (bail(answer, `The scaffold is in ${target}; nothing was installed.`))
|
|
@@ -193,27 +284,26 @@ export async function runCreate(
|
|
|
193
284
|
approved = answer;
|
|
194
285
|
}
|
|
195
286
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
pending = plan;
|
|
211
|
-
}
|
|
287
|
+
const pending =
|
|
288
|
+
approved && plan && registryDir
|
|
289
|
+
? await installPages(plan, {
|
|
290
|
+
appRoot,
|
|
291
|
+
interactive,
|
|
292
|
+
show,
|
|
293
|
+
syncContext: {
|
|
294
|
+
appRoot,
|
|
295
|
+
registryDir,
|
|
296
|
+
requires: catalogue.requires,
|
|
297
|
+
frameworkVersion: context.frameworkVersion,
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
: commands;
|
|
212
301
|
|
|
213
302
|
const next = {
|
|
214
|
-
packageManager,
|
|
303
|
+
packageManager: workspaceRoot ? ("pnpm" as const) : packageManager,
|
|
215
304
|
installed: approved && pending.length === 0,
|
|
216
305
|
pending,
|
|
306
|
+
workspaceRoot,
|
|
217
307
|
};
|
|
218
308
|
if (interactive) {
|
|
219
309
|
p.note(formatNextSteps(target, next), "Next");
|
package/src/commands/create.ts
CHANGED
|
@@ -9,17 +9,25 @@
|
|
|
9
9
|
*
|
|
10
10
|
* In a terminal it asks for the project name when none is given, then
|
|
11
11
|
* which registry pages to install (`--items a,b` or `--all` answer that
|
|
12
|
-
* without asking). The pages are installed by
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
19
21
|
import path from "node:path";
|
|
20
22
|
|
|
21
|
-
import { addFeature, formatAddResult, type AddResult } from "./add.js";
|
|
22
23
|
import {
|
|
24
|
+
addFeature,
|
|
25
|
+
formatAddResult,
|
|
26
|
+
readCatalogue,
|
|
27
|
+
type AddResult,
|
|
28
|
+
} from "./add.js";
|
|
29
|
+
import {
|
|
30
|
+
findWorkspaceRoot,
|
|
23
31
|
formatCommand,
|
|
24
32
|
type Command,
|
|
25
33
|
type PackageManager,
|
|
@@ -37,6 +45,15 @@ export type CreateOptions = {
|
|
|
37
45
|
* protocol used outside a workspace".
|
|
38
46
|
*/
|
|
39
47
|
linkWorkspace?: boolean;
|
|
48
|
+
/** The project name, when it is not the directory's. */
|
|
49
|
+
name?: string;
|
|
50
|
+
/**
|
|
51
|
+
* The package manager that will install the app. Under pnpm, an app
|
|
52
|
+
* outside any workspace also gets `pnpm-standalone`: its own
|
|
53
|
+
* workspace file declining the dependency build scripts pnpm 10+
|
|
54
|
+
* would otherwise stop the install over.
|
|
55
|
+
*/
|
|
56
|
+
packageManager?: PackageManager;
|
|
40
57
|
};
|
|
41
58
|
|
|
42
59
|
/** A package name and product slug derived from the directory name. */
|
|
@@ -55,17 +72,23 @@ export function deriveNames(target: string): {
|
|
|
55
72
|
return { appName: slug, appSlug: slug };
|
|
56
73
|
}
|
|
57
74
|
|
|
75
|
+
/** A fresh repository — `git init`, then `intelligo create .` — is still empty. */
|
|
76
|
+
const IGNORED_ENTRIES = new Set([".git"]);
|
|
77
|
+
|
|
58
78
|
export function isOccupied(target: string): boolean {
|
|
59
79
|
const dir = path.resolve(target);
|
|
60
|
-
return
|
|
80
|
+
return (
|
|
81
|
+
existsSync(dir) &&
|
|
82
|
+
readdirSync(dir).some((entry) => !IGNORED_ENTRIES.has(entry))
|
|
83
|
+
);
|
|
61
84
|
}
|
|
62
85
|
|
|
63
86
|
/** Scaffolding into an occupied directory is how people lose work. */
|
|
64
87
|
export function assertNotOccupied(target: string): void {
|
|
65
88
|
if (isOccupied(target)) {
|
|
66
89
|
throw new Error(
|
|
67
|
-
`${path.resolve(target)} is not empty. Create the app in a new directory,
|
|
68
|
-
|
|
90
|
+
`${path.resolve(target)} is not empty. Create the app in a new directory, ` +
|
|
91
|
+
"or in an empty one (a .git directory may already be there)."
|
|
69
92
|
);
|
|
70
93
|
}
|
|
71
94
|
}
|
|
@@ -74,11 +97,13 @@ export function createApp(options: CreateOptions): AddResult {
|
|
|
74
97
|
const target = path.resolve(options.target);
|
|
75
98
|
|
|
76
99
|
assertNotOccupied(target);
|
|
100
|
+
// Before the directory exists, so a name that slugifies to nothing
|
|
101
|
+
// leaves nothing behind.
|
|
102
|
+
const { appName, appSlug } = deriveNames(options.name || target);
|
|
77
103
|
mkdirSync(target, { recursive: true });
|
|
104
|
+
const workspaceRoot = findWorkspaceRoot(target);
|
|
78
105
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
return addFeature("app-scaffold", {
|
|
106
|
+
const scaffold = addFeature("app-scaffold", {
|
|
82
107
|
appRoot: target,
|
|
83
108
|
templatesDir: options.templatesDir,
|
|
84
109
|
frameworkVersion: options.frameworkVersion,
|
|
@@ -88,8 +113,43 @@ export function createApp(options: CreateOptions): AddResult {
|
|
|
88
113
|
__INTELLIGO_DEP__: options.linkWorkspace
|
|
89
114
|
? "workspace:*"
|
|
90
115
|
: `^${options.frameworkVersion}`,
|
|
116
|
+
...workspaceRootVariable(target),
|
|
91
117
|
},
|
|
92
118
|
});
|
|
119
|
+
|
|
120
|
+
// A member of a parent workspace installs from that root; a workspace
|
|
121
|
+
// file of its own would take it out of it.
|
|
122
|
+
const standalone =
|
|
123
|
+
options.packageManager === "pnpm" &&
|
|
124
|
+
workspaceRoot === null &&
|
|
125
|
+
STANDALONE in readCatalogue(options.templatesDir);
|
|
126
|
+
if (!standalone) return scaffold;
|
|
127
|
+
|
|
128
|
+
const pnpm = addFeature(STANDALONE, {
|
|
129
|
+
appRoot: target,
|
|
130
|
+
templatesDir: options.templatesDir,
|
|
131
|
+
frameworkVersion: options.frameworkVersion,
|
|
132
|
+
});
|
|
133
|
+
return { ...scaffold, written: [...scaffold.written, ...pnpm.written] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const STANDALONE = "pnpm-standalone";
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `__WORKSPACE_ROOT__` for the scaffold's next.config: a JavaScript
|
|
140
|
+
* literal naming the enclosing pnpm workspace's root relative to the
|
|
141
|
+
* app, or null when the app is not a member of one — the rule `doctor`
|
|
142
|
+
* and `migrate` load env files by.
|
|
143
|
+
*/
|
|
144
|
+
export function workspaceRootVariable(target: string): Record<string, string> {
|
|
145
|
+
const root = findWorkspaceRoot(path.resolve(target));
|
|
146
|
+
return {
|
|
147
|
+
__WORKSPACE_ROOT__: root
|
|
148
|
+
? JSON.stringify(
|
|
149
|
+
path.relative(path.resolve(target), root).split(path.sep).join("/")
|
|
150
|
+
)
|
|
151
|
+
: "null",
|
|
152
|
+
};
|
|
93
153
|
}
|
|
94
154
|
|
|
95
155
|
export type NextSteps = {
|
|
@@ -98,6 +158,8 @@ export type NextSteps = {
|
|
|
98
158
|
installed: boolean;
|
|
99
159
|
/** Install commands the developer declined, or that did not get to run. */
|
|
100
160
|
pending?: Command[];
|
|
161
|
+
/** The parent pnpm workspace the app is a member of, installed from its root. */
|
|
162
|
+
workspaceRoot?: string | null;
|
|
101
163
|
};
|
|
102
164
|
|
|
103
165
|
/** A path the reader can paste into a shell, spaces and quotes included. */
|
|
@@ -109,14 +171,20 @@ function shellQuote(value: string): string {
|
|
|
109
171
|
|
|
110
172
|
export function formatNextSteps(target: string, next: NextSteps): string {
|
|
111
173
|
const pm = next.packageManager;
|
|
174
|
+
const appRoot = path.resolve(target);
|
|
175
|
+
const install: Command = {
|
|
176
|
+
command: pm,
|
|
177
|
+
args: ["install"],
|
|
178
|
+
cwd: next.workspaceRoot ?? appRoot,
|
|
179
|
+
};
|
|
112
180
|
return [
|
|
113
|
-
`cd ${shellQuote(path.relative(process.cwd(),
|
|
181
|
+
`cd ${shellQuote(path.relative(process.cwd(), appRoot) || ".")}`,
|
|
114
182
|
"cp .env.example .env.local # then fill it in",
|
|
115
183
|
...(next.pending?.length
|
|
116
|
-
? next.pending.map(formatCommand)
|
|
184
|
+
? next.pending.map((c) => formatCommand(c, appRoot))
|
|
117
185
|
: next.installed
|
|
118
186
|
? []
|
|
119
|
-
: [
|
|
187
|
+
: [formatCommand(install, appRoot)]),
|
|
120
188
|
`${pm === "npm" ? "npm run" : pm} dev`,
|
|
121
189
|
].join("\n");
|
|
122
190
|
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -10,6 +10,12 @@ import { parseEnv } from "node:util";
|
|
|
10
10
|
|
|
11
11
|
import { inspectMigrationChain, readMigrationChain } from "../migrations.js";
|
|
12
12
|
import { readManifest } from "../manifest.js";
|
|
13
|
+
import {
|
|
14
|
+
checkExports,
|
|
15
|
+
hasObjectKey,
|
|
16
|
+
reexportedSource,
|
|
17
|
+
stripComments,
|
|
18
|
+
} from "../module-exports.js";
|
|
13
19
|
import {
|
|
14
20
|
MODEL_CATALOGUE_LOCATIONS,
|
|
15
21
|
readCatalogueModelIds,
|
|
@@ -35,20 +41,11 @@ type ItemRequires = {
|
|
|
35
41
|
|
|
36
42
|
export type RegistryRequires = {
|
|
37
43
|
scaffold: string[];
|
|
44
|
+
/** Files an item ships once and the deployment then owns. */
|
|
45
|
+
seams?: Record<string, string>;
|
|
38
46
|
items: Record<string, ItemRequires>;
|
|
39
47
|
};
|
|
40
48
|
|
|
41
|
-
/**
|
|
42
|
-
* Block and line comments removed, so a check cannot be satisfied — or
|
|
43
|
-
* defeated — by prose. The composition root's own doc comment names
|
|
44
|
-
* `registerModels` in several of the templates.
|
|
45
|
-
*/
|
|
46
|
-
function stripComments(text: string): string {
|
|
47
|
-
return text
|
|
48
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
49
|
-
.replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
49
|
/**
|
|
53
50
|
* templates/registry-requires.json is a build-time copy of
|
|
54
51
|
* packages/registry/requires.json (scripts/sync-registry-requires.mjs),
|
|
@@ -261,6 +258,103 @@ function checkModelIds(root: string, source: string): CheckResult[] {
|
|
|
261
258
|
const OPAQUE_REGISTRATION =
|
|
262
259
|
/\bregisterModels?\s*\(\s*(?![\s[{]|DEFAULT_MODELS\s*[,)])/;
|
|
263
260
|
|
|
261
|
+
/**
|
|
262
|
+
* A seam's feature keys are the deployment's own choice, so they are
|
|
263
|
+
* not an item requirement; a literal key the app's copy still names but
|
|
264
|
+
* lib/plans.ts does not register is worth a warning.
|
|
265
|
+
*/
|
|
266
|
+
function checkSeamFeatures(
|
|
267
|
+
root: string,
|
|
268
|
+
seams: Record<string, string> | undefined,
|
|
269
|
+
plans: ReturnType<typeof reexportedSource> | null
|
|
270
|
+
): CheckResult[] {
|
|
271
|
+
if (!plans || plans.unresolved.length) return [];
|
|
272
|
+
const results: CheckResult[] = [];
|
|
273
|
+
for (const seam of Object.keys(seams ?? {})) {
|
|
274
|
+
const abs = path.join(root, seam);
|
|
275
|
+
if (!existsSync(abs)) continue;
|
|
276
|
+
const keys = new Set(
|
|
277
|
+
[
|
|
278
|
+
...readFileSync(abs, "utf8").matchAll(
|
|
279
|
+
/featureKey:\s*["']([^"']+)["']/g
|
|
280
|
+
),
|
|
281
|
+
].map((m) => m[1]!)
|
|
282
|
+
);
|
|
283
|
+
const unregistered = [...keys].filter(
|
|
284
|
+
(key) => !hasObjectKey(plans.text, key)
|
|
285
|
+
);
|
|
286
|
+
if (unregistered.length === 0) continue;
|
|
287
|
+
results.push({
|
|
288
|
+
name: `seam:${seam}`,
|
|
289
|
+
status: "warn",
|
|
290
|
+
detail: `${unregistered.map((key) => `"${key}"`).join(", ")} ${unregistered.length === 1 ? "is" : "are"} not registered in lib/plans.ts — a gate on an unregistered key is denied (403)`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return results;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const VITEST_CONFIGS = [
|
|
297
|
+
"vitest.config.ts",
|
|
298
|
+
"vitest.config.mts",
|
|
299
|
+
"vitest.config.cts",
|
|
300
|
+
"vitest.config.js",
|
|
301
|
+
"vitest.config.mjs",
|
|
302
|
+
"vitest.config.cjs",
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
/** `PRODUCT_SLUG = "acme"` or `setDefaultProductSlug("acme")` in the composition root. */
|
|
306
|
+
const PRODUCT_SLUG_LITERAL =
|
|
307
|
+
/(?:\bPRODUCT_SLUG\s*=|\bsetDefaultProductSlug\s*\()\s*["'`]([^"'`]+)["'`]/;
|
|
308
|
+
|
|
309
|
+
function checkBillingProduct(
|
|
310
|
+
fromEnv: string | undefined,
|
|
311
|
+
rootSource: string | null
|
|
312
|
+
): CheckResult {
|
|
313
|
+
const setsDefault =
|
|
314
|
+
rootSource !== null && /\bsetDefaultProductSlug\s*\(/.test(rootSource);
|
|
315
|
+
const inCode = rootSource?.match(PRODUCT_SLUG_LITERAL)?.[1];
|
|
316
|
+
if (fromEnv && setsDefault && inCode && fromEnv !== inCode) {
|
|
317
|
+
return {
|
|
318
|
+
name: "billing",
|
|
319
|
+
status: "warn",
|
|
320
|
+
detail:
|
|
321
|
+
`INTELLIGO_BILLING_PRODUCT is "${fromEnv}" but the composition root sets "${inCode}". ` +
|
|
322
|
+
"At runtime the composition root wins; the variable only misleads tooling that reads it — make them agree",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (fromEnv) {
|
|
326
|
+
return { name: "billing", status: "ok", detail: `Product: ${fromEnv}` };
|
|
327
|
+
}
|
|
328
|
+
if (setsDefault) {
|
|
329
|
+
return {
|
|
330
|
+
name: "billing",
|
|
331
|
+
status: "ok",
|
|
332
|
+
detail: `Product: ${inCode ?? "set"} by setDefaultProductSlug() in the composition root`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
name: "billing",
|
|
337
|
+
status: "warn",
|
|
338
|
+
detail:
|
|
339
|
+
"no product — set INTELLIGO_BILLING_PRODUCT, or call setDefaultProductSlug() " +
|
|
340
|
+
"in the composition root, or plan lookups resolve to nothing",
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Whether `name` is installed where the app resolves packages from: its
|
|
346
|
+
* own node_modules or any directory above it, a workspace root's
|
|
347
|
+
* included.
|
|
348
|
+
*/
|
|
349
|
+
function resolvesFrom(root: string, name: string): boolean {
|
|
350
|
+
for (let dir = path.resolve(root); ; dir = path.dirname(dir)) {
|
|
351
|
+
if (existsSync(path.join(dir, "node_modules", name, "package.json"))) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
if (path.dirname(dir) === dir) return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
264
358
|
export function runChecks(options: DoctorOptions = {}): CheckResult[] {
|
|
265
359
|
const root = options.root ?? process.cwd();
|
|
266
360
|
const env = options.env ?? process.env;
|
|
@@ -299,22 +393,15 @@ export function runChecks(options: DoctorOptions = {}): CheckResult[] {
|
|
|
299
393
|
|
|
300
394
|
// 3. Billing product. The engine has no built-in default catalogue;
|
|
301
395
|
// an unset product means every plan lookup returns nothing and
|
|
302
|
-
// quotas silently read as zero.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
name: "billing",
|
|
312
|
-
status: "warn",
|
|
313
|
-
detail:
|
|
314
|
-
"INTELLIGO_BILLING_PRODUCT unset — the composition root must call " +
|
|
315
|
-
"setDefaultProductSlug(), or plan lookups resolve to nothing",
|
|
316
|
-
}
|
|
317
|
-
);
|
|
396
|
+
// quotas silently read as zero. The composition root sets it at
|
|
397
|
+
// boot; the variable lets tooling see it without booting.
|
|
398
|
+
const compositionRoot = ["lib/intelligo.ts", "lib/intelligo.tsx"]
|
|
399
|
+
.map((rel) => path.join(root, rel))
|
|
400
|
+
.find((file) => existsSync(file));
|
|
401
|
+
const rootSource = compositionRoot
|
|
402
|
+
? stripComments(readFileSync(compositionRoot, "utf8"))
|
|
403
|
+
: null;
|
|
404
|
+
results.push(checkBillingProduct(env.INTELLIGO_BILLING_PRODUCT, rootSource));
|
|
318
405
|
|
|
319
406
|
// 4. Installed registry items against registry/requires.json: the
|
|
320
407
|
// sibling items they import from, the scaffold files they import,
|
|
@@ -328,13 +415,17 @@ export function runChecks(options: DoctorOptions = {}): CheckResult[] {
|
|
|
328
415
|
[".ts", ".tsx", ".js", ".jsx", ".json", ""].some((ext) =>
|
|
329
416
|
existsSync(path.join(root, rel + ext))
|
|
330
417
|
);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
418
|
+
// lib/plans.ts and whatever it re-exports — a catalogue defined in
|
|
419
|
+
// a workspace package counts — comments stripped.
|
|
420
|
+
const plansFile = ["lib/plans.ts", "lib/plans.tsx"]
|
|
421
|
+
.map((rel) => path.join(root, rel))
|
|
422
|
+
.find((file) => existsSync(file));
|
|
423
|
+
const plans = plansFile ? reexportedSource(plansFile, root) : null;
|
|
334
424
|
|
|
335
425
|
for (const [name, item] of Object.entries(requires.items)) {
|
|
336
426
|
if (!existsSync(path.join(root, item.marker))) continue;
|
|
337
427
|
const problems: string[] = [];
|
|
428
|
+
const notes: string[] = [];
|
|
338
429
|
|
|
339
430
|
for (const dep of item.items ?? []) {
|
|
340
431
|
const marker = requires.items[dep]?.marker;
|
|
@@ -350,23 +441,22 @@ export function runChecks(options: DoctorOptions = {}): CheckResult[] {
|
|
|
350
441
|
.map((ext) => path.join(root, file + ext))
|
|
351
442
|
.find((p) => existsSync(p));
|
|
352
443
|
if (!abs) continue; // reported above as a missing file
|
|
353
|
-
const
|
|
354
|
-
const missing = names.filter(
|
|
355
|
-
(n) =>
|
|
356
|
-
!new RegExp(
|
|
357
|
-
`export\\s+(?:default\\s+)?(?:async\\s+)?(?:const|function\\*?|let|var|class)\\s+${n}\\b`
|
|
358
|
-
).test(source) &&
|
|
359
|
-
!new RegExp(`export\\s*\\{[^}]*\\b${n}\\b`).test(source)
|
|
360
|
-
);
|
|
444
|
+
const { missing, unresolved } = checkExports(abs, names, root);
|
|
361
445
|
if (missing.length) {
|
|
362
446
|
problems.push(`${file} must export ${missing.join(", ")}`);
|
|
447
|
+
} else if (unresolved.length) {
|
|
448
|
+
notes.push(
|
|
449
|
+
`${file}: could not read ${unresolved.map((u) => `\`export * from "${u}"\``).join(", ")} — assumed to provide ${names.join(", ")}`
|
|
450
|
+
);
|
|
363
451
|
}
|
|
364
452
|
}
|
|
365
453
|
for (const feature of item.features ?? []) {
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
454
|
+
if (!plans || hasObjectKey(plans.text, feature)) continue;
|
|
455
|
+
if (plans.unresolved.length) {
|
|
456
|
+
notes.push(
|
|
457
|
+
`the "${feature}" feature key is not in lib/plans.ts, and ${plans.unresolved.join(", ")} could not be read to confirm it`
|
|
458
|
+
);
|
|
459
|
+
} else {
|
|
370
460
|
problems.push(
|
|
371
461
|
`register the "${feature}" feature key in lib/plans.ts — an unregistered key is denied (403)`
|
|
372
462
|
);
|
|
@@ -375,7 +465,9 @@ export function runChecks(options: DoctorOptions = {}): CheckResult[] {
|
|
|
375
465
|
|
|
376
466
|
results.push(
|
|
377
467
|
problems.length === 0
|
|
378
|
-
?
|
|
468
|
+
? notes.length === 0
|
|
469
|
+
? { name: `item:${name}`, status: "ok", detail: "requirements met" }
|
|
470
|
+
: { name: `item:${name}`, status: "warn", detail: notes.join("; ") }
|
|
379
471
|
: {
|
|
380
472
|
name: `item:${name}`,
|
|
381
473
|
status: "error",
|
|
@@ -383,6 +475,8 @@ export function runChecks(options: DoctorOptions = {}): CheckResult[] {
|
|
|
383
475
|
}
|
|
384
476
|
);
|
|
385
477
|
}
|
|
478
|
+
|
|
479
|
+
results.push(...checkSeamFeatures(root, requires.seams, plans));
|
|
386
480
|
}
|
|
387
481
|
|
|
388
482
|
// 4b. Maintenance route. It refuses to serve without a strong
|
|
@@ -428,10 +522,6 @@ export function runChecks(options: DoctorOptions = {}): CheckResult[] {
|
|
|
428
522
|
// admission with no price to estimate against: every request is
|
|
429
523
|
// refused with `unknown_model`, at runtime, on a deployment whose
|
|
430
524
|
// only mistake was omitting one line.
|
|
431
|
-
const compositionRoot = ["lib/intelligo.ts", "lib/intelligo.tsx"]
|
|
432
|
-
.map((rel) => path.join(root, rel))
|
|
433
|
-
.find((file) => existsSync(file));
|
|
434
|
-
|
|
435
525
|
if (compositionRoot) {
|
|
436
526
|
const source = readFileSync(compositionRoot, "utf8");
|
|
437
527
|
const registers = /\bregisterModels?\s*\(/.test(stripComments(source));
|
|
@@ -456,6 +546,25 @@ export function runChecks(options: DoctorOptions = {}): CheckResult[] {
|
|
|
456
546
|
if (registers) results.push(...checkModelIds(root, source));
|
|
457
547
|
}
|
|
458
548
|
|
|
549
|
+
// 4e. A test config whose runner is not installed fails at the first
|
|
550
|
+
// `vitest run`, with a module error that does not say why.
|
|
551
|
+
const vitestConfig = VITEST_CONFIGS.find((file) =>
|
|
552
|
+
existsSync(path.join(root, file))
|
|
553
|
+
);
|
|
554
|
+
if (vitestConfig) {
|
|
555
|
+
results.push(
|
|
556
|
+
resolvesFrom(root, "vitest")
|
|
557
|
+
? { name: "tests", status: "ok", detail: "vitest is installed" }
|
|
558
|
+
: {
|
|
559
|
+
name: "tests",
|
|
560
|
+
status: "warn",
|
|
561
|
+
detail:
|
|
562
|
+
`${vitestConfig} is here but vitest does not resolve from the app — ` +
|
|
563
|
+
"`pnpm add -D vitest`",
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
459
568
|
// 5. Generated source. A conflict — template and consumer both
|
|
460
569
|
// moved — is the one state an upgrade cannot resolve on its own.
|
|
461
570
|
const manifest = readManifest(root);
|