@mizen-labs/cli 0.0.0-stage → 0.1.0

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/dist/cli.js ADDED
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { parseArgs } from "node:util";
4
+ import { formatInfo, formatSearch, info, search } from "./discover.js";
5
+ import { CliError, commandLine, errorDocument, formatError, invalidUsage, normalizeError, } from "./errors.js";
6
+ import { argumentOptions, help, isCommand, unknownCommand, validateOptions, } from "./help.js";
7
+ import { nextStep, styleFor, unstyled } from "./output.js";
8
+ // --no-color applies before any stream decides its styling.
9
+ if (process.argv.includes("--no-color")) {
10
+ process.env.NO_COLOR = "1";
11
+ }
12
+ const out = styleFor(process.stdout);
13
+ const err = styleFor(process.stderr);
14
+ function runSearch(catalog, query) {
15
+ const components = search(catalog.components, query);
16
+ const data = { components, query, uiVersion: catalog.uiVersion };
17
+ if (components.length) {
18
+ return {
19
+ command: "search",
20
+ data,
21
+ text: formatSearch(out, components, process.stdout),
22
+ };
23
+ }
24
+ return {
25
+ command: "search",
26
+ data,
27
+ note: [
28
+ `No components match "${query}".`,
29
+ "",
30
+ nextStep(err, "List every component", "mizen search"),
31
+ ].join("\n"),
32
+ // A pipe still gets the table header, so a parser sees the columns.
33
+ text: process.stdout.isTTY ? "" : "Component\tDescription",
34
+ };
35
+ }
36
+ function runInfo(catalog, query) {
37
+ if (!query.trim()) {
38
+ throw new CliError("missing_name", "Can't show a component without its name.", 2, {
39
+ next: { argv: ["mizen", "search"], benefit: "Find a component" },
40
+ });
41
+ }
42
+ const component = info(catalog.components, query);
43
+ return {
44
+ command: "info",
45
+ data: { component, uiVersion: catalog.uiVersion },
46
+ text: formatInfo(out, component),
47
+ };
48
+ }
49
+ async function runDoctor(catalog, values) {
50
+ const cwd = values.cwd ?? process.cwd();
51
+ const { doctor } = await import("./doctor/doctor.js");
52
+ const { formatReport } = await import("./doctor/report.js");
53
+ const { formatWorkspace, workspaceDoctor } = await import("./doctor/workspace.js");
54
+ const failed = (summary) => summary.errors > 0 || Boolean(values.strict && summary.unverified);
55
+ if (values.workspace) {
56
+ const data = await workspaceDoctor(cwd, catalog.nodeRange);
57
+ const text = formatWorkspace(out, data);
58
+ return {
59
+ command: "doctor",
60
+ data,
61
+ exitCode: failed(data.summary) ? 1 : 0,
62
+ text,
63
+ };
64
+ }
65
+ const data = await doctor(cwd, catalog.nodeRange);
66
+ const text = formatReport(out, data);
67
+ return {
68
+ command: "doctor",
69
+ data,
70
+ exitCode: failed(data.summary) ? 1 : 0,
71
+ text,
72
+ };
73
+ }
74
+ // What to run after init: apply a plan, or check the app after a failure.
75
+ function initNext(values, project, state) {
76
+ if (state === "planned") {
77
+ const argv = [
78
+ "mizen",
79
+ "init",
80
+ "--cwd",
81
+ project,
82
+ ...(values.package ? ["--package", values.package] : []),
83
+ ...(values["package-manager"]
84
+ ? ["--package-manager", values["package-manager"]]
85
+ : []),
86
+ ...(values.feature ?? []).flatMap((feature) => ["--feature", feature]),
87
+ "--yes",
88
+ ];
89
+ return { argv, benefit: "Apply the plan" };
90
+ }
91
+ if (state === "install-failed" || state === "verification-failed") {
92
+ return {
93
+ argv: ["mizen", "doctor", "--cwd", project],
94
+ benefit: "See what needs attention",
95
+ };
96
+ }
97
+ return null;
98
+ }
99
+ async function runInit(catalog, values) {
100
+ const { formatInit, init } = await import("./init/init.js");
101
+ const data = await init({
102
+ cwd: values.cwd,
103
+ dryRun: values["dry-run"],
104
+ feature: values.feature,
105
+ package: values.package,
106
+ packageManager: values["package-manager"],
107
+ yes: values.yes,
108
+ }, catalog);
109
+ const next = initNext(values, data.plan.project, data.status);
110
+ return {
111
+ command: "init",
112
+ data,
113
+ exitCode: ["install-failed", "verification-failed"].includes(data.status)
114
+ ? 1
115
+ : 0,
116
+ text: formatInit(out, data),
117
+ ...(next
118
+ ? { note: nextStep(err, next.benefit, commandLine(next.argv)) }
119
+ : {}),
120
+ };
121
+ }
122
+ function dispatch(catalog, positionals, values) {
123
+ const [command, ...args] = positionals;
124
+ if (!isCommand(command)) {
125
+ return unknownCommand(command ?? "");
126
+ }
127
+ validateOptions(command, values);
128
+ if ((command === "init" || command === "doctor") && args.length) {
129
+ invalidUsage(`Can't use arguments with mizen ${command}. Select the app with --cwd.`, command);
130
+ }
131
+ if (command === "init") {
132
+ return runInit(catalog, values);
133
+ }
134
+ if (command === "doctor") {
135
+ return runDoctor(catalog, values);
136
+ }
137
+ return command === "search"
138
+ ? runSearch(catalog, args.join(" "))
139
+ : runInfo(catalog, args.join(" "));
140
+ }
141
+ function json(value, stream) {
142
+ return JSON.stringify(value, null, stream.isTTY ? 2 : undefined);
143
+ }
144
+ function write(asJson, result) {
145
+ if (asJson) {
146
+ const document = {
147
+ command: result.command,
148
+ data: result.data,
149
+ schemaVersion: 1,
150
+ };
151
+ process.stdout.write(`${json(document, process.stdout)}\n`);
152
+ }
153
+ else {
154
+ if (result.text) {
155
+ process.stdout.write(`${result.text}\n`);
156
+ }
157
+ if (result.note) {
158
+ process.stderr.write(`${result.text ? "\n" : ""}${result.note}\n`);
159
+ }
160
+ }
161
+ process.exitCode = result.exitCode ?? 0;
162
+ }
163
+ async function version() {
164
+ const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
165
+ return manifest.version;
166
+ }
167
+ // `mizen help [command]` and `--help` at any depth print the same help;
168
+ // bare `mizen` prints it too, unless it asks for the version.
169
+ function helpTopic(positionals, values) {
170
+ if (positionals[0] === "help") {
171
+ return { topic: positionals[1] };
172
+ }
173
+ const bare = positionals.length === 0 && !values.version;
174
+ return values.help || bare ? { topic: positionals[0] } : null;
175
+ }
176
+ async function main() {
177
+ const { values, positionals } = parseArgs({
178
+ allowPositionals: true,
179
+ options: argumentOptions,
180
+ });
181
+ const asked = helpTopic(positionals, values);
182
+ if (asked) {
183
+ const text = help(out, asked.topic);
184
+ const usage = values.json ? help(unstyled, asked.topic) : text;
185
+ write(values.json, { command: "help", data: { usage }, text });
186
+ return;
187
+ }
188
+ if (values.version) {
189
+ const current = await version();
190
+ write(values.json, {
191
+ command: "version",
192
+ data: { version: current },
193
+ text: current,
194
+ });
195
+ return;
196
+ }
197
+ const catalog = JSON.parse(await readFile(new URL("./catalog.json", import.meta.url), "utf8"));
198
+ write(values.json, await dispatch(catalog, positionals, values));
199
+ }
200
+ function requestedJson(args) {
201
+ const terminator = args.indexOf("--");
202
+ return (terminator < 0 ? args : args.slice(0, terminator)).includes("--json");
203
+ }
204
+ main().catch((error) => {
205
+ const args = process.argv.slice(2);
206
+ const failure = normalizeError(error, isCommand(args[0]) ? args[0] : undefined);
207
+ const text = requestedJson(args)
208
+ ? JSON.stringify(errorDocument(failure))
209
+ : formatError(err, failure);
210
+ process.stderr.write(`${text}\n`);
211
+ process.exitCode = failure.exitCode;
212
+ });
@@ -0,0 +1,108 @@
1
+ import { CliError } from "./errors.js";
2
+ import { closest } from "./help.js";
3
+ import { indent, row } from "./output.js";
4
+ function normalize(value) {
5
+ return value.toLowerCase().trim().replaceAll(/\s+/gu, " ");
6
+ }
7
+ /** Components matching every query word, in slug order. */
8
+ export function search(components, query) {
9
+ const words = normalize(query).split(" ").filter(Boolean);
10
+ return components
11
+ .filter((component) => {
12
+ const text = normalize([
13
+ component.slug,
14
+ component.name,
15
+ component.category,
16
+ component.description,
17
+ ...component.guidance.recommendations,
18
+ ...component.guidance.boundaries,
19
+ ].join(" "));
20
+ return words.every((word) => text.includes(word));
21
+ })
22
+ .toSorted((a, b) => a.slug.localeCompare(b.slug, "en"))
23
+ .map(({ slug, name, category, description, status, entryPoint }) => ({
24
+ category,
25
+ description,
26
+ entryPoint,
27
+ name,
28
+ slug,
29
+ status,
30
+ }));
31
+ }
32
+ /** One component by slug or name; a near miss gets one suggestion. */
33
+ export function info(components, query) {
34
+ const slug = normalize(query).replaceAll(" ", "-");
35
+ const component = components.find((item) => item.slug === slug);
36
+ if (component) {
37
+ return component;
38
+ }
39
+ const suggestion = closest(slug, components.map((item) => item.slug));
40
+ throw new CliError("not_found", `Couldn't find component ${query}.${suggestion ? ` Did you mean ${suggestion}?` : ""}`, 1, {
41
+ ...(suggestion ? { data: { suggestion } } : {}),
42
+ next: suggestion
43
+ ? { argv: ["mizen", "info", suggestion], benefit: "Show it" }
44
+ : { argv: ["mizen", "search", query], benefit: "Search for it" },
45
+ });
46
+ }
47
+ function fit(text, width) {
48
+ return text.length > width
49
+ ? `${text.slice(0, Math.max(width - 1, 0))}…`
50
+ : text;
51
+ }
52
+ /**
53
+ * A table on a terminal, the description shortened to fit; tab-separated
54
+ * values, never shortened, anywhere else.
55
+ */
56
+ export function formatSearch(style, results, stream) {
57
+ if (!stream.isTTY) {
58
+ return [
59
+ "Component\tDescription",
60
+ ...results.map((item) => `${item.slug}\t${item.description}`),
61
+ ].join("\n");
62
+ }
63
+ const width = Math.max(...results.map((item) => item.slug.length), 9) + 3;
64
+ const room = (stream.columns || 80) - width - 2;
65
+ return [
66
+ ` ${style.bold(`${"Component".padEnd(width)}Description`)}`,
67
+ ...results.map((item) => ` ${item.slug.padEnd(width)}${fit(item.description, room)}`),
68
+ ].join("\n");
69
+ }
70
+ function propertyRow(property) {
71
+ const fallback = property.defaultValue.replaceAll("—", "").trim();
72
+ return row(property.name, [
73
+ property.type,
74
+ `${property.description}${fallback ? ` (default: ${fallback})` : ""}`,
75
+ ]);
76
+ }
77
+ function heading(style, title, lines) {
78
+ return lines.length ? ["", style.bold(title), ...lines] : [];
79
+ }
80
+ export function formatInfo(style, component) {
81
+ const examples = (component.usageSections ?? []).flatMap((section) => [
82
+ "",
83
+ style.bold(section.title),
84
+ ...(section.description ? [indent(section.description)] : []),
85
+ indent(section.code, 2),
86
+ ]);
87
+ return [
88
+ style.bold(component.name),
89
+ component.description,
90
+ "",
91
+ row("Category", component.category),
92
+ row("Import", component.entryPoint),
93
+ row("Docs", component.documentationPath),
94
+ ...heading(style, "Guidance", component.guidance.recommendations.map((line) => indent(line))),
95
+ ...heading(style, "Boundaries", component.guidance.boundaries.map((line) => indent(line))),
96
+ ...heading(style, "API", component.api.map(propertyRow)),
97
+ ...(component.apiSections ?? []).flatMap((section) => heading(style, section.name, [
98
+ indent(section.description),
99
+ ...section.properties.map(propertyRow),
100
+ ])),
101
+ ...(component.contract
102
+ ? heading(style, "Contract", [
103
+ indent(JSON.stringify(component.contract, null, 2)),
104
+ ])
105
+ : []),
106
+ ...examples,
107
+ ].join("\n");
108
+ }
@@ -0,0 +1,44 @@
1
+ import { resolve } from "node:path";
2
+ import { InvalidManifestError } from "./files.js";
3
+ import { frameworkVersionCheck, PackageResolutionError, packageChecks, versionCheck, } from "./packages.js";
4
+ import { frameworkOf, packageManagerCheck, targetProject } from "./project.js";
5
+ import { check, report } from "./report.js";
6
+ import { setupChecks } from "./setup.js";
7
+ export async function doctor(cwd, nodeRange, options = {}) {
8
+ try {
9
+ return await inspectProject(cwd, nodeRange, options);
10
+ }
11
+ catch (error) {
12
+ if (error instanceof PackageResolutionError) {
13
+ return report(resolve(cwd), [
14
+ check("package.metadata", "error", error.message, [error.directory], "Repair the installed package metadata or reinstall dependencies, then rerun doctor."),
15
+ ]);
16
+ }
17
+ if (!(error instanceof InvalidManifestError)) {
18
+ throw error;
19
+ }
20
+ return report(resolve(cwd), [
21
+ check("package.metadata", "error", "An installed or workspace package manifest is invalid.", [error.path], "Repair the named manifest or reinstall dependencies, then rerun doctor."),
22
+ ]);
23
+ }
24
+ }
25
+ async function inspectProject(cwd, nodeRange, options) {
26
+ const target = await targetProject(resolve(cwd), options);
27
+ if (!(target.manifest && target.project)) {
28
+ return report(target.project, target.diagnostic ? [target.diagnostic] : []);
29
+ }
30
+ const { project, manifest } = target;
31
+ const framework = frameworkOf(manifest);
32
+ const { diagnostics, ui } = await packageChecks(project);
33
+ diagnostics.unshift(versionCheck("runtime.node", process.versions.node, nodeRange, []));
34
+ diagnostics.push(await packageManagerCheck(project));
35
+ diagnostics.push(check("project.framework", framework === "unknown" ? "unverified" : "pass", `Framework: ${framework}.`, [project], "Setup checks target Next.js App Router, TanStack Start, and React + Vite. Select the application package when using a workspace."));
36
+ if (framework !== "unknown") {
37
+ diagnostics.push(await frameworkVersionCheck(project, framework), ...(await setupChecks(project, framework)));
38
+ }
39
+ return {
40
+ ...report(project, diagnostics),
41
+ framework,
42
+ uiVersion: ui?.manifest.version ?? null,
43
+ };
44
+ }
@@ -0,0 +1,77 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ export class InvalidManifestError extends Error {
4
+ path;
5
+ constructor(path, options) {
6
+ super(`Invalid package manifest: ${path}`, options);
7
+ this.path = path;
8
+ }
9
+ }
10
+ function isMissing(error) {
11
+ return (error instanceof Error &&
12
+ "code" in error &&
13
+ ["ENOENT", "ENOTDIR"].includes(String(error.code)));
14
+ }
15
+ export async function exists(path) {
16
+ try {
17
+ return (await stat(path)).isFile();
18
+ }
19
+ catch (error) {
20
+ if (isMissing(error)) {
21
+ return false;
22
+ }
23
+ throw error;
24
+ }
25
+ }
26
+ export async function readOptional(path) {
27
+ try {
28
+ return await readFile(path, "utf8");
29
+ }
30
+ catch (error) {
31
+ if (isMissing(error)) {
32
+ return null;
33
+ }
34
+ throw error;
35
+ }
36
+ }
37
+ function isRecord(value) {
38
+ return !!value && typeof value === "object" && !Array.isArray(value);
39
+ }
40
+ function validManifest(value) {
41
+ const maps = [
42
+ value.dependencies,
43
+ value.devDependencies,
44
+ value.peerDependencies,
45
+ ];
46
+ const strings = [value.name, value.version, value.packageManager];
47
+ return (maps.every((map) => map === undefined ||
48
+ (isRecord(map) &&
49
+ Object.values(map).every((item) => typeof item === "string"))) && strings.every((item) => item === undefined || typeof item === "string"));
50
+ }
51
+ export async function manifestAt(path) {
52
+ const source = await readOptional(path);
53
+ if (source === null) {
54
+ return null;
55
+ }
56
+ return parseManifest(source, path);
57
+ }
58
+ export function parseManifest(source, path) {
59
+ let value;
60
+ try {
61
+ value = JSON.parse(source);
62
+ }
63
+ catch (error) {
64
+ throw new InvalidManifestError(path, { cause: error });
65
+ }
66
+ if (!(isRecord(value) && validManifest(value))) {
67
+ throw new InvalidManifestError(path);
68
+ }
69
+ return value;
70
+ }
71
+ export async function findProject(directory) {
72
+ if (await exists(join(directory, "package.json"))) {
73
+ return directory;
74
+ }
75
+ const parent = dirname(directory);
76
+ return parent === directory ? null : findProject(parent);
77
+ }
@@ -0,0 +1,114 @@
1
+ import { createRequire } from "node:module";
2
+ import { dirname, join } from "node:path";
3
+ import semver from "semver";
4
+ import { manifestAt } from "./files.js";
5
+ import { check } from "./report.js";
6
+ export class PackageResolutionError extends Error {
7
+ directory;
8
+ constructor(message, options) {
9
+ super(message, options);
10
+ this.directory = options.directory;
11
+ }
12
+ }
13
+ async function findPackage(directory, name) {
14
+ const path = join(directory, "package.json");
15
+ const manifest = await manifestAt(path);
16
+ if (manifest?.name === name) {
17
+ return { manifest, path };
18
+ }
19
+ const parent = dirname(directory);
20
+ return parent === directory ? null : findPackage(parent, name);
21
+ }
22
+ export async function installedFrom(directory, name) {
23
+ const { resolve } = createRequire(join(directory, "package.json"));
24
+ try {
25
+ const path = resolve(`${name}/package.json`);
26
+ const manifest = await manifestAt(path);
27
+ return manifest ? { manifest, path } : null;
28
+ }
29
+ catch (error) {
30
+ if (!(error instanceof Error && "code" in error)) {
31
+ throw error;
32
+ }
33
+ if (error.code === "ERR_INVALID_PACKAGE_CONFIG") {
34
+ throw new PackageResolutionError(`Node could not read package metadata while resolving ${name}.`, { cause: error, directory });
35
+ }
36
+ if (error.code === "MODULE_NOT_FOUND") {
37
+ return null;
38
+ }
39
+ if (error.code !== "ERR_PACKAGE_PATH_NOT_EXPORTED") {
40
+ throw error;
41
+ }
42
+ }
43
+ return findPackage(dirname(resolve(name)), name);
44
+ }
45
+ export function versionCheck(id, version, range, evidence) {
46
+ if (typeof version !== "string" ||
47
+ typeof range !== "string" ||
48
+ !semver.valid(version) ||
49
+ !semver.validRange(range)) {
50
+ return check(id, "unverified", "No valid version or compatibility range is available.", evidence, "Check the installed package metadata.");
51
+ }
52
+ return check(id, semver.satisfies(version, range) ? "pass" : "error", `Installed ${version}; required ${range}.`, evidence, `Use a version satisfying ${range}.`);
53
+ }
54
+ async function peerChecks(project, ui, name) {
55
+ const [host, library] = await Promise.all([
56
+ installedFrom(project, name),
57
+ installedFrom(dirname(ui.path), name),
58
+ ]);
59
+ if (!host) {
60
+ return [
61
+ check(`package.${name}`, "error", `${name} cannot be resolved from this app.`, [], `Install ${name} using the Mizen peer range.`),
62
+ ];
63
+ }
64
+ const same = host.path === library?.path;
65
+ return [
66
+ versionCheck(`package.${name}`, host.manifest.version, ui.manifest.peerDependencies?.[name], [host.path, ui.path]),
67
+ check(`identity.${name}`, same ? "pass" : "error", `${name} ${same ? "shares" : "does not share"} the app's module identity.`, [host.path, ...(library ? [library.path] : [])], "Align workspace dependencies and deduplicate React; do not bundle a separate React copy in Mizen."),
68
+ ];
69
+ }
70
+ export async function packageChecks(project) {
71
+ const ui = await installedFrom(project, "@mizen-labs/ui");
72
+ if (!ui) {
73
+ return {
74
+ diagnostics: [
75
+ check("package.mizen", "error", "@mizen-labs/ui cannot be resolved from this app.", [join(project, "package.json")], "Install Mizen in the target app, then rerun doctor."),
76
+ ],
77
+ ui: null,
78
+ };
79
+ }
80
+ const peers = await Promise.all(["react", "react-dom"].map((name) => peerChecks(project, ui, name)));
81
+ return {
82
+ diagnostics: [
83
+ check("package.mizen", "pass", `Resolved @mizen-labs/ui ${ui.manifest.version ?? "(unknown version)"}.`, [ui.path]),
84
+ ...peers.flat(),
85
+ ...(await reactPairCheck(project)),
86
+ ],
87
+ ui,
88
+ };
89
+ }
90
+ export async function frameworkVersionCheck(project, framework) {
91
+ const name = { next: "next", start: "@tanstack/react-start", vite: "vite" }[framework];
92
+ const installed = await installedFrom(project, name);
93
+ if (!installed) {
94
+ return check("project.framework-version", "error", `${name} is declared but cannot be resolved.`, [], "Install the app's framework dependencies.");
95
+ }
96
+ return check("project.framework-version", "pass", `Resolved ${name} ${installed.manifest.version ?? "(unknown version)"}. No broader framework compatibility range is claimed.`, [installed.path]);
97
+ }
98
+ async function reactPairCheck(project) {
99
+ const [react, dom] = await Promise.all([
100
+ installedFrom(project, "react"),
101
+ installedFrom(project, "react-dom"),
102
+ ]);
103
+ if (!(react && dom)) {
104
+ return [];
105
+ }
106
+ const versions = [react.manifest.version, dom.manifest.version];
107
+ if (!versions.every((version) => typeof version === "string" && semver.valid(version))) {
108
+ return [];
109
+ }
110
+ const same = versions[0] === versions[1];
111
+ return [
112
+ check("package.react-pair", same ? "pass" : "error", `React ${versions[0]} and ReactDOM ${versions[1]} ${same ? "match" : "differ"}.`, [react.path, dom.path], "Install matching React and ReactDOM versions."),
113
+ ];
114
+ }
@@ -0,0 +1,133 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { exists, findProject, manifestAt } from "./files.js";
4
+ import { check } from "./report.js";
5
+ import { hasPnpmWorkspacePackages, WorkspaceConfigError, } from "./workspace-config.js";
6
+ const LOCKFILES = {
7
+ "bun.lock": "bun",
8
+ "bun.lockb": "bun",
9
+ "npm-shrinkwrap.json": "npm",
10
+ "package-lock.json": "npm",
11
+ "pnpm-lock.yaml": "pnpm",
12
+ "yarn.lock": "yarn",
13
+ };
14
+ export function frameworkOf(manifest) {
15
+ const dependencies = {
16
+ ...manifest.dependencies,
17
+ ...manifest.devDependencies,
18
+ };
19
+ const next = "next" in dependencies;
20
+ const start = "@tanstack/react-start" in dependencies;
21
+ if (next && start) {
22
+ return "unknown";
23
+ }
24
+ if (next) {
25
+ return "next";
26
+ }
27
+ if (start) {
28
+ return "start";
29
+ }
30
+ if (["astro", "@react-router/dev", "@sveltejs/kit"].some((name) => name in dependencies)) {
31
+ return "unknown";
32
+ }
33
+ return "vite" in dependencies && "react" in dependencies ? "vite" : "unknown";
34
+ }
35
+ const MANAGER = /^(?<manager>npm|pnpm|yarn|bun)@/u;
36
+ async function managerMetadata(directory) {
37
+ const path = join(directory, "package.json");
38
+ const manifest = await manifestAt(path);
39
+ const field = manifest?.packageManager;
40
+ const manager = field?.match(MANAGER)?.[1];
41
+ const workspace = manifest?.workspaces ||
42
+ (await exists(join(directory, "pnpm-workspace.yaml")));
43
+ const diagnostic = field && !manager
44
+ ? check("project.package-manager", "unverified", "The declared package manager is not recognized.", [path], "Declare npm, pnpm, yarn, or bun with a version.")
45
+ : null;
46
+ return {
47
+ declaration: manager ? { manager, path } : undefined,
48
+ diagnostic,
49
+ workspace,
50
+ };
51
+ }
52
+ export async function packageManagerCheck(directory, declaration) {
53
+ const metadata = await managerMetadata(directory);
54
+ if (!declaration && metadata.diagnostic) {
55
+ return metadata.diagnostic;
56
+ }
57
+ const selected = declaration ?? metadata.declaration;
58
+ const files = await Promise.all(Object.keys(LOCKFILES).map(async (name) => (await exists(join(directory, name))) ? name : null));
59
+ const found = files.filter((item) => item !== null);
60
+ if (found.length) {
61
+ return lockfileCheck(directory, found, selected);
62
+ }
63
+ const parent = dirname(directory);
64
+ if (parent !== directory && !metadata.workspace) {
65
+ return packageManagerCheck(parent, selected);
66
+ }
67
+ return missingLockfile(selected);
68
+ }
69
+ function missingLockfile(declaration) {
70
+ return {
71
+ ...check("project.package-manager", "unverified", `No package-manager lockfile was found${declaration ? `; declared manager: ${declaration.manager}` : ""}.`, declaration ? [declaration.path] : [], "Install dependencies with your chosen package manager first."),
72
+ packageManager: declaration?.manager,
73
+ };
74
+ }
75
+ function lockfileCheck(directory, files, declaration) {
76
+ const managers = [...new Set(files.map((name) => LOCKFILES[name]))];
77
+ const matches = !declaration || managers[0] === declaration.manager;
78
+ const verified = managers.length === 1 && matches;
79
+ return {
80
+ ...check("project.package-manager", verified ? "pass" : "unverified", `Lockfiles indicate ${managers.join(", ")}${declaration ? `; declared manager: ${declaration.manager}` : ""}.`, [
81
+ ...files.map((name) => join(directory, name)),
82
+ ...(declaration ? [declaration.path] : []),
83
+ ], "Keep the intended package manager's lockfile and align the packageManager field before setup."),
84
+ packageManager: verified ? managers[0] : undefined,
85
+ };
86
+ }
87
+ export async function targetProject(requested, options = {}) {
88
+ try {
89
+ if (!(await stat(requested)).isDirectory()) {
90
+ throw new Error("Not a directory.");
91
+ }
92
+ }
93
+ catch {
94
+ return {
95
+ diagnostic: check("project.target", "error", "The target directory is unavailable.", [requested], "Use --cwd with an existing, readable app directory."),
96
+ project: null,
97
+ };
98
+ }
99
+ const project = await findProject(requested);
100
+ if (!project) {
101
+ return {
102
+ diagnostic: check("project.target", "error", "No package.json was found.", [requested], "Run the command in an app directory, or pass one with --cwd <APP_DIR>."),
103
+ project: null,
104
+ };
105
+ }
106
+ return inspectManifest(project, options.allowWorkspaceRoot);
107
+ }
108
+ async function inspectManifest(project, allowWorkspaceRoot = false) {
109
+ const path = join(project, "package.json");
110
+ try {
111
+ const manifest = await manifestAt(path);
112
+ if (!manifest) {
113
+ throw new Error("Missing manifest.");
114
+ }
115
+ if (!allowWorkspaceRoot &&
116
+ (manifest.workspaces || (await hasPnpmWorkspacePackages(project)))) {
117
+ return {
118
+ diagnostic: check("project.target", "error", "This directory is a workspace root; select an app explicitly.", [path], "Run mizen doctor --cwd <APP_DIR>."),
119
+ project,
120
+ };
121
+ }
122
+ return { manifest, project };
123
+ }
124
+ catch (error) {
125
+ if (error instanceof WorkspaceConfigError) {
126
+ return { diagnostic: error.diagnostic, project };
127
+ }
128
+ return {
129
+ diagnostic: check("project.manifest", "error", "The app package.json is invalid or unreadable.", [path], "Repair package.json, then rerun mizen doctor."),
130
+ project,
131
+ };
132
+ }
133
+ }