@intelligo-dev/cli 1.0.0-beta.14 → 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 +24 -0
- 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 +69 -25
- package/dist/bin.js.map +1 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +19 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/create-flow.d.ts +4 -0
- package/dist/commands/create-flow.d.ts.map +1 -1
- package/dist/commands/create-flow.js +39 -4
- package/dist/commands/create-flow.js.map +1 -1
- package/dist/commands/create.d.ts +16 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +42 -7
- 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 +102 -16
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/sync.d.ts +6 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +34 -3
- package/dist/commands/sync.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/package.json +1 -1
- package/src/args.ts +25 -0
- package/src/bin.ts +78 -28
- package/src/commands/add.ts +28 -12
- package/src/commands/create-flow.ts +38 -4
- package/src/commands/create.ts +65 -7
- package/src/commands/doctor.ts +129 -20
- package/src/commands/sync.ts +42 -2
- package/src/env-files.ts +28 -7
- package/templates/admin-page/admin-page.tsx.tpl +96 -24
- package/templates/app-scaffold/gitignore.tpl +25 -0
- package/templates/app-scaffold/next.config.mjs.tpl +31 -2
- package/templates/app-scaffold/package.json.tpl +2 -2
- package/templates/manifest.json +21 -3
- package/templates/pnpm-standalone/npmrc.tpl +6 -0
- package/templates/pnpm-standalone/pnpm-workspace.yaml.tpl +10 -0
- package/templates/registry/app-shell.json +2 -2
- package/templates/registry/billing-settings.json +8 -2
- package/templates/registry/chat.json +1 -1
- package/templates/registry/payment-poll.json +11 -5
- package/templates/registry/pricing.json +8 -2
- package/templates/registry/registry.json +18 -3
- package/templates/registry/trial-banner.json +2 -2
- package/templates/registry-items.json +3 -3
- package/templates/registry-requires.json +7 -5
package/src/bin.ts
CHANGED
|
@@ -50,9 +50,11 @@ import {
|
|
|
50
50
|
upgradeCheckExitCode,
|
|
51
51
|
} from "./commands/upgrade-check.js";
|
|
52
52
|
|
|
53
|
+
import { itemNames, unknownFlags, wantsHelp } from "./args.js";
|
|
53
54
|
import { loadAppEnv } from "./env-files.js";
|
|
55
|
+
import { workspaceRootVariable } from "./commands/create.js";
|
|
54
56
|
import { resolveRegistryDir } from "./registry-bundle.js";
|
|
55
|
-
import { readRegistryCatalogue } from "./registry-items.js";
|
|
57
|
+
import { findWorkspaceRoot, readRegistryCatalogue } from "./registry-items.js";
|
|
56
58
|
import { MIGRATION_LOCATIONS, resolveMigrationsDir } from "./migrations-dir.js";
|
|
57
59
|
|
|
58
60
|
/**
|
|
@@ -85,7 +87,7 @@ function usage(): string {
|
|
|
85
87
|
"intelligo <command>",
|
|
86
88
|
"",
|
|
87
89
|
" create [dir] Scaffold an app, then install the registry pages you pick",
|
|
88
|
-
" (--items a,b | --all, --yes, --no-install)",
|
|
90
|
+
" (--items a,b | --all, --yes, --no-install, --name <name>)",
|
|
89
91
|
" doctor Report configuration and migration-chain problems",
|
|
90
92
|
" migrate Apply the framework's migration chain to DATABASE_URL",
|
|
91
93
|
" migrate --check Compare the framework's migrations to a database",
|
|
@@ -93,7 +95,8 @@ function usage(): string {
|
|
|
93
95
|
" fresh | ahead | unmanaged | legacy)",
|
|
94
96
|
" add <feature> Generate consumer-owned source (--force to overwrite)",
|
|
95
97
|
" upgrade --check Show what a template upgrade would change",
|
|
96
|
-
" sync [items…] Install registry pages from this release's registry
|
|
98
|
+
" sync [items…] Install registry pages from this release's registry",
|
|
99
|
+
" (names space- or comma-separated),",
|
|
97
100
|
" keeping seams and merging messages (--force replaces",
|
|
98
101
|
" hand-edited files; --check only reports, exit 1 on drift)",
|
|
99
102
|
"",
|
|
@@ -178,17 +181,72 @@ async function runMigrate(
|
|
|
178
181
|
}
|
|
179
182
|
}
|
|
180
183
|
|
|
184
|
+
function flagsOk(
|
|
185
|
+
command: string,
|
|
186
|
+
args: readonly string[],
|
|
187
|
+
allowed: readonly string[]
|
|
188
|
+
): boolean {
|
|
189
|
+
const unknown = unknownFlags(args, allowed);
|
|
190
|
+
if (unknown.length === 0) return true;
|
|
191
|
+
console.error(
|
|
192
|
+
`intelligo ${command}: unknown argument ${unknown.join(" ")}` +
|
|
193
|
+
(allowed.length ? ` (it takes ${allowed.join(", ")}).` : ".")
|
|
194
|
+
);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function runAdd(rest: readonly string[]): number {
|
|
199
|
+
if (!flagsOk("add", rest, ["--force"])) return 2;
|
|
200
|
+
const feature = rest.find((a) => !a.startsWith("--"));
|
|
201
|
+
if (!feature) {
|
|
202
|
+
const catalogue = readCatalogue(TEMPLATES_DIR);
|
|
203
|
+
console.error("Usage: intelligo add <feature>\n");
|
|
204
|
+
for (const [name, spec] of Object.entries(catalogue)) {
|
|
205
|
+
console.error(` ${name.padEnd(16)} ${spec.description}`);
|
|
206
|
+
}
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
if (feature === "pnpm-standalone" && findWorkspaceRoot(process.cwd())) {
|
|
210
|
+
console.error(
|
|
211
|
+
"intelligo add pnpm-standalone: this app is a member of a pnpm workspace, and a " +
|
|
212
|
+
"workspace file of its own would take it out; the workspace root's settings apply."
|
|
213
|
+
);
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
const result = addFeature(feature, {
|
|
217
|
+
appRoot: process.cwd(),
|
|
218
|
+
templatesDir: TEMPLATES_DIR,
|
|
219
|
+
frameworkVersion: FRAMEWORK_VERSION,
|
|
220
|
+
force: rest.includes("--force"),
|
|
221
|
+
// Computed, not recorded: an app made before the variable existed
|
|
222
|
+
// has no value for it.
|
|
223
|
+
variables:
|
|
224
|
+
feature === "app-scaffold"
|
|
225
|
+
? workspaceRootVariable(process.cwd())
|
|
226
|
+
: undefined,
|
|
227
|
+
});
|
|
228
|
+
console.log(formatAddResult(result));
|
|
229
|
+
return addExitCode(result);
|
|
230
|
+
}
|
|
231
|
+
|
|
181
232
|
async function main(): Promise<number> {
|
|
182
233
|
const [, , command = "help", ...rest] = process.argv;
|
|
183
234
|
|
|
235
|
+
if (wantsHelp(rest)) {
|
|
236
|
+
console.log(usage());
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
184
240
|
// The commands that read the app's configuration see what the app
|
|
185
|
-
// itself would: its .env.local and .env,
|
|
241
|
+
// itself would: its .env.local and .env, then the pnpm workspace
|
|
242
|
+
// root's, under anything the shell set.
|
|
186
243
|
if (command === "doctor" || command === "migrate" || command === "upgrade") {
|
|
187
244
|
loadAppEnv(process.cwd());
|
|
188
245
|
}
|
|
189
246
|
|
|
190
247
|
switch (command) {
|
|
191
248
|
case "doctor": {
|
|
249
|
+
if (!flagsOk("doctor", rest, [])) return 2;
|
|
192
250
|
const results = runChecks();
|
|
193
251
|
console.log(formatResults(results));
|
|
194
252
|
return exitCodeFor(results);
|
|
@@ -217,32 +275,24 @@ async function main(): Promise<number> {
|
|
|
217
275
|
// command that converses.
|
|
218
276
|
const { parseCreateFlags, runCreate } =
|
|
219
277
|
await import("./commands/create-flow.js");
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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;
|
|
278
|
+
const flags = parseCreateFlags(rest);
|
|
279
|
+
if (flags.unknown.length > 0) {
|
|
280
|
+
console.error(
|
|
281
|
+
`intelligo create: unknown argument ${flags.unknown.join(" ")}.\n`
|
|
282
|
+
);
|
|
283
|
+
console.error(usage());
|
|
284
|
+
return 2;
|
|
235
285
|
}
|
|
236
|
-
|
|
237
|
-
appRoot: process.cwd(),
|
|
286
|
+
return runCreate(flags, {
|
|
238
287
|
templatesDir: TEMPLATES_DIR,
|
|
239
288
|
frameworkVersion: FRAMEWORK_VERSION,
|
|
240
|
-
|
|
289
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
241
290
|
});
|
|
242
|
-
console.log(formatAddResult(result));
|
|
243
|
-
return addExitCode(result);
|
|
244
291
|
}
|
|
292
|
+
case "add":
|
|
293
|
+
return runAdd(rest);
|
|
245
294
|
case "upgrade": {
|
|
295
|
+
if (!flagsOk("upgrade", rest, ["--check"])) return 2;
|
|
246
296
|
if (!rest.includes("--check")) {
|
|
247
297
|
console.error("Only `upgrade --check` is implemented.");
|
|
248
298
|
console.error(
|
|
@@ -260,6 +310,7 @@ async function main(): Promise<number> {
|
|
|
260
310
|
return upgradeCheckExitCode(report);
|
|
261
311
|
}
|
|
262
312
|
case "sync": {
|
|
313
|
+
if (!flagsOk("sync", rest, ["--check", "--force"])) return 2;
|
|
263
314
|
const registryDir = resolveRegistryDir(TEMPLATES_DIR);
|
|
264
315
|
if (!registryDir) {
|
|
265
316
|
console.error(
|
|
@@ -280,10 +331,7 @@ async function main(): Promise<number> {
|
|
|
280
331
|
"run the CLI of the same version (`pnpm exec intelligo`), or pages and packages will disagree."
|
|
281
332
|
);
|
|
282
333
|
}
|
|
283
|
-
const selection = selectItems(
|
|
284
|
-
rest.filter((a) => !a.startsWith("-")),
|
|
285
|
-
context
|
|
286
|
-
);
|
|
334
|
+
const selection = selectItems(itemNames(rest), context);
|
|
287
335
|
if (!selection.ok) {
|
|
288
336
|
console.error(selection.message);
|
|
289
337
|
return 1;
|
|
@@ -298,6 +346,8 @@ async function main(): Promise<number> {
|
|
|
298
346
|
});
|
|
299
347
|
}
|
|
300
348
|
case "help":
|
|
349
|
+
case "--help":
|
|
350
|
+
case "-h":
|
|
301
351
|
console.log(usage());
|
|
302
352
|
return 0;
|
|
303
353
|
default:
|
package/src/commands/add.ts
CHANGED
|
@@ -85,6 +85,9 @@ export function substitute(
|
|
|
85
85
|
);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/** A `__NAME__` placeholder the variables did not fill. */
|
|
89
|
+
const PLACEHOLDER = /__[A-Z][A-Z0-9_]*__/;
|
|
90
|
+
|
|
88
91
|
const VERCEL_CONFIG = "vercel.json";
|
|
89
92
|
|
|
90
93
|
/**
|
|
@@ -145,16 +148,35 @@ export function addFeature(feature: string, options: AddOptions): AddResult {
|
|
|
145
148
|
const recorded: GeneratedFile[] = [];
|
|
146
149
|
const handedOver = new Set(previous?.handedOver ?? []);
|
|
147
150
|
|
|
151
|
+
// Re-generating a feature substitutes what it was generated with, so
|
|
152
|
+
// `add app-scaffold` in an app `create` made takes the app's own name;
|
|
153
|
+
// values passed now fill in or replace them.
|
|
154
|
+
const variables =
|
|
155
|
+
previous?.variables || options.variables
|
|
156
|
+
? { ...previous?.variables, ...options.variables }
|
|
157
|
+
: undefined;
|
|
158
|
+
|
|
159
|
+
// Checked before anything is written, so a refusal leaves no half-made
|
|
160
|
+
// feature behind.
|
|
161
|
+
for (const file of spec.files) {
|
|
162
|
+
const unfilled = substitute(
|
|
163
|
+
readFileSync(path.join(options.templatesDir, file.template), "utf8"),
|
|
164
|
+
variables
|
|
165
|
+
).match(PLACEHOLDER);
|
|
166
|
+
if (unfilled) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`${feature} needs a value for ${unfilled[0]} that only \`intelligo create\` supplies — ` +
|
|
169
|
+
"run `intelligo create <directory>`, or `intelligo create .` in an empty one."
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
148
174
|
for (const file of spec.files) {
|
|
149
175
|
// A registry item replaced it; `intelligo sync` keeps it now.
|
|
150
176
|
if (handedOver.has(file.target)) continue;
|
|
151
177
|
const source = path.join(options.templatesDir, file.template);
|
|
152
178
|
const target = path.join(options.appRoot, file.target);
|
|
153
|
-
const contents = substitute(
|
|
154
|
-
readFileSync(source, "utf8"),
|
|
155
|
-
options.variables
|
|
156
|
-
);
|
|
157
|
-
|
|
179
|
+
const contents = substitute(readFileSync(source, "utf8"), variables);
|
|
158
180
|
if (existsSync(target) && !options.force) {
|
|
159
181
|
const current = hashContents(readFileSync(target, "utf8"));
|
|
160
182
|
const known = previousByPath.get(file.target);
|
|
@@ -190,13 +212,7 @@ export function addFeature(feature: string, options: AddOptions): AddResult {
|
|
|
190
212
|
|
|
191
213
|
writeManifest(
|
|
192
214
|
options.appRoot,
|
|
193
|
-
recordFeature(
|
|
194
|
-
manifest,
|
|
195
|
-
feature,
|
|
196
|
-
spec.templateVersion,
|
|
197
|
-
recorded,
|
|
198
|
-
options.variables
|
|
199
|
-
)
|
|
215
|
+
recordFeature(manifest, feature, spec.templateVersion, recorded, variables)
|
|
200
216
|
);
|
|
201
217
|
|
|
202
218
|
return result;
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
formatNextSteps,
|
|
25
25
|
isOccupied,
|
|
26
26
|
} from "./create.js";
|
|
27
|
-
import { syncApply, type SyncContext } from "./sync.js";
|
|
27
|
+
import { recordItems, syncApply, type SyncContext } from "./sync.js";
|
|
28
28
|
import { resolveRegistryDir } from "../registry-bundle.js";
|
|
29
29
|
import {
|
|
30
30
|
detectPackageManager,
|
|
@@ -54,11 +54,25 @@ export type CreateFlags = {
|
|
|
54
54
|
* range, because that is what works everywhere else.
|
|
55
55
|
*/
|
|
56
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[];
|
|
57
61
|
};
|
|
58
62
|
|
|
63
|
+
const CREATE_FLAGS = [
|
|
64
|
+
"--all",
|
|
65
|
+
"--yes",
|
|
66
|
+
"-y",
|
|
67
|
+
"--no-install",
|
|
68
|
+
"--link-workspace",
|
|
69
|
+
];
|
|
70
|
+
|
|
59
71
|
export function parseCreateFlags(args: readonly string[]): CreateFlags {
|
|
60
72
|
let target: string | undefined;
|
|
61
73
|
let items: string[] | undefined;
|
|
74
|
+
let name: string | undefined;
|
|
75
|
+
const unknown: string[] = [];
|
|
62
76
|
for (let i = 0; i < args.length; i++) {
|
|
63
77
|
const arg = args[i]!;
|
|
64
78
|
if (arg === "--items" || arg.startsWith("--items=")) {
|
|
@@ -67,13 +81,23 @@ export function parseCreateFlags(args: readonly string[]): CreateFlags {
|
|
|
67
81
|
.split(",")
|
|
68
82
|
.map((s) => s.trim())
|
|
69
83
|
.filter(Boolean);
|
|
70
|
-
} 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) {
|
|
71
91
|
target = arg;
|
|
92
|
+
} else {
|
|
93
|
+
unknown.push(arg);
|
|
72
94
|
}
|
|
73
95
|
}
|
|
74
96
|
return {
|
|
75
97
|
target,
|
|
76
98
|
items,
|
|
99
|
+
name,
|
|
100
|
+
unknown,
|
|
77
101
|
all: args.includes("--all"),
|
|
78
102
|
yes: args.includes("--yes") || args.includes("-y"),
|
|
79
103
|
install: !args.includes("--no-install"),
|
|
@@ -164,7 +188,7 @@ export async function runCreate(
|
|
|
164
188
|
if (!target) {
|
|
165
189
|
if (!interactive) {
|
|
166
190
|
console.error(
|
|
167
|
-
"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>]"
|
|
168
192
|
);
|
|
169
193
|
return 1;
|
|
170
194
|
}
|
|
@@ -218,15 +242,25 @@ export async function runCreate(
|
|
|
218
242
|
templatesDir: context.templatesDir,
|
|
219
243
|
frameworkVersion: context.frameworkVersion,
|
|
220
244
|
linkWorkspace: flags.linkWorkspace,
|
|
245
|
+
name: flags.name,
|
|
246
|
+
packageManager,
|
|
221
247
|
});
|
|
222
248
|
const appRoot = path.resolve(target);
|
|
223
249
|
if (interactive) {
|
|
224
250
|
p.log.success(`Scaffolded ${result.written.length} files in ${target}`);
|
|
225
251
|
}
|
|
226
252
|
|
|
227
|
-
// 4. The pages — only with approval
|
|
253
|
+
// 4. The pages — only with approval, but recorded now, so a bare
|
|
254
|
+
// `intelligo sync` installs them if this install does not.
|
|
228
255
|
const workspaceRoot = findWorkspaceRoot(appRoot);
|
|
229
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
|
+
}
|
|
230
264
|
const commands: Command[] = plan
|
|
231
265
|
? [...(plan.install ? [plan.install] : []), plan.sync]
|
|
232
266
|
: [];
|
package/src/commands/create.ts
CHANGED
|
@@ -20,8 +20,14 @@
|
|
|
20
20
|
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
|
|
23
|
-
import { addFeature, formatAddResult, type AddResult } from "./add.js";
|
|
24
23
|
import {
|
|
24
|
+
addFeature,
|
|
25
|
+
formatAddResult,
|
|
26
|
+
readCatalogue,
|
|
27
|
+
type AddResult,
|
|
28
|
+
} from "./add.js";
|
|
29
|
+
import {
|
|
30
|
+
findWorkspaceRoot,
|
|
25
31
|
formatCommand,
|
|
26
32
|
type Command,
|
|
27
33
|
type PackageManager,
|
|
@@ -39,6 +45,15 @@ export type CreateOptions = {
|
|
|
39
45
|
* protocol used outside a workspace".
|
|
40
46
|
*/
|
|
41
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;
|
|
42
57
|
};
|
|
43
58
|
|
|
44
59
|
/** A package name and product slug derived from the directory name. */
|
|
@@ -57,17 +72,23 @@ export function deriveNames(target: string): {
|
|
|
57
72
|
return { appName: slug, appSlug: slug };
|
|
58
73
|
}
|
|
59
74
|
|
|
75
|
+
/** A fresh repository — `git init`, then `intelligo create .` — is still empty. */
|
|
76
|
+
const IGNORED_ENTRIES = new Set([".git"]);
|
|
77
|
+
|
|
60
78
|
export function isOccupied(target: string): boolean {
|
|
61
79
|
const dir = path.resolve(target);
|
|
62
|
-
return
|
|
80
|
+
return (
|
|
81
|
+
existsSync(dir) &&
|
|
82
|
+
readdirSync(dir).some((entry) => !IGNORED_ENTRIES.has(entry))
|
|
83
|
+
);
|
|
63
84
|
}
|
|
64
85
|
|
|
65
86
|
/** Scaffolding into an occupied directory is how people lose work. */
|
|
66
87
|
export function assertNotOccupied(target: string): void {
|
|
67
88
|
if (isOccupied(target)) {
|
|
68
89
|
throw new Error(
|
|
69
|
-
`${path.resolve(target)} is not empty. Create the app in a new directory,
|
|
70
|
-
|
|
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)."
|
|
71
92
|
);
|
|
72
93
|
}
|
|
73
94
|
}
|
|
@@ -76,11 +97,13 @@ export function createApp(options: CreateOptions): AddResult {
|
|
|
76
97
|
const target = path.resolve(options.target);
|
|
77
98
|
|
|
78
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);
|
|
79
103
|
mkdirSync(target, { recursive: true });
|
|
104
|
+
const workspaceRoot = findWorkspaceRoot(target);
|
|
80
105
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
return addFeature("app-scaffold", {
|
|
106
|
+
const scaffold = addFeature("app-scaffold", {
|
|
84
107
|
appRoot: target,
|
|
85
108
|
templatesDir: options.templatesDir,
|
|
86
109
|
frameworkVersion: options.frameworkVersion,
|
|
@@ -90,8 +113,43 @@ export function createApp(options: CreateOptions): AddResult {
|
|
|
90
113
|
__INTELLIGO_DEP__: options.linkWorkspace
|
|
91
114
|
? "workspace:*"
|
|
92
115
|
: `^${options.frameworkVersion}`,
|
|
116
|
+
...workspaceRootVariable(target),
|
|
93
117
|
},
|
|
94
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
|
+
};
|
|
95
153
|
}
|
|
96
154
|
|
|
97
155
|
export type NextSteps = {
|
package/src/commands/doctor.ts
CHANGED
|
@@ -41,6 +41,8 @@ type ItemRequires = {
|
|
|
41
41
|
|
|
42
42
|
export type RegistryRequires = {
|
|
43
43
|
scaffold: string[];
|
|
44
|
+
/** Files an item ships once and the deployment then owns. */
|
|
45
|
+
seams?: Record<string, string>;
|
|
44
46
|
items: Record<string, ItemRequires>;
|
|
45
47
|
};
|
|
46
48
|
|
|
@@ -256,6 +258,103 @@ function checkModelIds(root: string, source: string): CheckResult[] {
|
|
|
256
258
|
const OPAQUE_REGISTRATION =
|
|
257
259
|
/\bregisterModels?\s*\(\s*(?![\s[{]|DEFAULT_MODELS\s*[,)])/;
|
|
258
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
|
+
|
|
259
358
|
export function runChecks(options: DoctorOptions = {}): CheckResult[] {
|
|
260
359
|
const root = options.root ?? process.cwd();
|
|
261
360
|
const env = options.env ?? process.env;
|
|
@@ -294,22 +393,15 @@ export function runChecks(options: DoctorOptions = {}): CheckResult[] {
|
|
|
294
393
|
|
|
295
394
|
// 3. Billing product. The engine has no built-in default catalogue;
|
|
296
395
|
// an unset product means every plan lookup returns nothing and
|
|
297
|
-
// quotas silently read as zero.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
name: "billing",
|
|
307
|
-
status: "warn",
|
|
308
|
-
detail:
|
|
309
|
-
"INTELLIGO_BILLING_PRODUCT unset — the composition root must call " +
|
|
310
|
-
"setDefaultProductSlug(), or plan lookups resolve to nothing",
|
|
311
|
-
}
|
|
312
|
-
);
|
|
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));
|
|
313
405
|
|
|
314
406
|
// 4. Installed registry items against registry/requires.json: the
|
|
315
407
|
// sibling items they import from, the scaffold files they import,
|
|
@@ -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);
|