@mnci/cli 1.14.0 → 1.15.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/README.md +23 -1
- package/dist/cli.js +177 -16
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ first-party (or established community) Nx equivalent:
|
|
|
24
24
|
| Hand-written Azure Function templates | `@nx/node:application` (plain Node app) + a thin Azure Functions v4 overlay |
|
|
25
25
|
| doctor/drift sync of tool-owned files | Nothing to drift: this CLI owns 5 small files, Nx owns the rest |
|
|
26
26
|
|
|
27
|
-
## Commands (deliberately just
|
|
27
|
+
## Commands (deliberately just four)
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
30
|
mnci new my-repo # create a monorepo (prompts scope + registry)
|
|
@@ -61,8 +61,30 @@ mnci add flutter-internal-lib core # private shared package -> libs/
|
|
|
61
61
|
|
|
62
62
|
mnci upgrade # re-apply the latest overlay (see below)
|
|
63
63
|
mnci upgrade --agent windows-latest # ...with an explicit override
|
|
64
|
+
|
|
65
|
+
mnci doctor # check this workspace's invariants (read-only)
|
|
64
66
|
```
|
|
65
67
|
|
|
68
|
+
## `mnci doctor`: checking the invariants actually hold
|
|
69
|
+
|
|
70
|
+
Read-only — it never edits the workspace. Every failing finding names the command
|
|
71
|
+
that fixes it (usually `mnci upgrade`), and it exits non-zero when anything failed,
|
|
72
|
+
so it works as a CI step as well as a local command.
|
|
73
|
+
|
|
74
|
+
Every check corresponds to an invariant that has **actually** been violated, in
|
|
75
|
+
this repo or in a workspace it generated. None are hypothetical; a check nobody has
|
|
76
|
+
ever needed is noise that trains people to ignore the output.
|
|
77
|
+
|
|
78
|
+
| Check | The failure it catches |
|
|
79
|
+
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| Exactly one root ESLint config, and no per-project ones | The config fragmenting — every `@nx/*` generator writes one, so each project ends up linting against whichever config sits nearest |
|
|
81
|
+
| No stray `.prettierrc` | It outranks `.prettierrc.json`, so the whole formatting opinion is silently discarded while both files look fine |
|
|
82
|
+
| `@nx/eslint/plugin` registered in `nx.json` | Without it `npm run lint` exits 0 while linting nothing |
|
|
83
|
+
| The **resolved** `eslint` major | A declared range and an installed version are different things — manifests once said `^10` while the pin said 9 |
|
|
84
|
+
| `.npmrc` matches the recorded registry | The two registry kinds get different files; an Azure workspace also needs its scope routed |
|
|
85
|
+
| `versionActions` on publishable Dart/Python packages | Its absence aborts `nx release` for the **whole** workspace, not just that project |
|
|
86
|
+
| `nx sync:check` | A stale TypeScript project reference that was never committed |
|
|
87
|
+
|
|
66
88
|
Everything else is plain Nx, surfaced as a small curated set of root scripts —
|
|
67
89
|
each a single cross-platform command:
|
|
68
90
|
|
package/dist/cli.js
CHANGED
|
@@ -1234,11 +1234,8 @@ function readWorkspaceStack(workspaceRoot) {
|
|
|
1234
1234
|
};
|
|
1235
1235
|
}
|
|
1236
1236
|
|
|
1237
|
-
// src/commands/
|
|
1238
|
-
var
|
|
1239
|
-
var import_prompts8 = require("@inquirer/prompts");
|
|
1240
|
-
|
|
1241
|
-
// src/commands/new.ts
|
|
1237
|
+
// src/commands/doctor.ts
|
|
1238
|
+
var import_node_fs7 = require("fs");
|
|
1242
1239
|
var import_node_path12 = require("path");
|
|
1243
1240
|
|
|
1244
1241
|
// src/overlay.ts
|
|
@@ -2053,7 +2050,166 @@ function applyOverlay(workspaceRoot, options) {
|
|
|
2053
2050
|
}
|
|
2054
2051
|
}
|
|
2055
2052
|
|
|
2053
|
+
// src/commands/doctor.ts
|
|
2054
|
+
var SUPPORTED_ESLINT_MAJOR = ESLINT_VERSION.replace(/^\D*/, "").split(".")[0];
|
|
2055
|
+
function checkEslintConfigs(workspaceRoot) {
|
|
2056
|
+
const rootConfigs = (0, import_node_fs7.globSync)("eslint.config.{js,mjs,cjs,ts,mts,cts}", { cwd: workspaceRoot });
|
|
2057
|
+
const projectConfigs = (0, import_node_fs7.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
|
|
2058
|
+
cwd: workspaceRoot
|
|
2059
|
+
});
|
|
2060
|
+
return [
|
|
2061
|
+
{
|
|
2062
|
+
check: "root ESLint config",
|
|
2063
|
+
ok: rootConfigs.length === 1,
|
|
2064
|
+
detail: rootConfigs.length === 0 ? "no eslint.config.* at the workspace root" : `${rootConfigs.length} root configs: ${rootConfigs.join(", ")}`,
|
|
2065
|
+
remedy: "run `mnci upgrade` to rewrite the root config"
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
check: "no per-project ESLint configs",
|
|
2069
|
+
ok: projectConfigs.length === 0,
|
|
2070
|
+
detail: `found ${projectConfigs.length}: ${projectConfigs.join(", ")}`,
|
|
2071
|
+
remedy: "run `mnci upgrade`, which sweeps {apps,libs,packages}/*/eslint.config.*"
|
|
2072
|
+
}
|
|
2073
|
+
];
|
|
2074
|
+
}
|
|
2075
|
+
function checkPrettierConfig(workspaceRoot) {
|
|
2076
|
+
const strayExists = fileExists((0, import_node_path12.join)(workspaceRoot, ".prettierrc"));
|
|
2077
|
+
return {
|
|
2078
|
+
check: "Prettier config is mnci\u2019s",
|
|
2079
|
+
ok: !strayExists,
|
|
2080
|
+
detail: ".prettierrc exists and outranks .prettierrc.json, so mnci\u2019s config is ignored",
|
|
2081
|
+
remedy: "delete .prettierrc (or run `mnci upgrade`, which deletes it)"
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
function checkEslintPlugin(nxJson) {
|
|
2085
|
+
const plugins = nxJson.plugins ?? [];
|
|
2086
|
+
const registered = plugins.some(
|
|
2087
|
+
(entry) => typeof entry === "string" ? entry === "@nx/eslint/plugin" : entry.plugin === "@nx/eslint/plugin"
|
|
2088
|
+
);
|
|
2089
|
+
return {
|
|
2090
|
+
check: "@nx/eslint/plugin registered",
|
|
2091
|
+
ok: registered,
|
|
2092
|
+
detail: "not in nx.json plugins \u2014 every project silently loses its lint target",
|
|
2093
|
+
remedy: "run `mnci upgrade`"
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
function checkResolvedEslint(workspaceRoot) {
|
|
2097
|
+
const manifestPath = (0, import_node_path12.join)(workspaceRoot, "node_modules/eslint/package.json");
|
|
2098
|
+
if (!fileExists(manifestPath)) {
|
|
2099
|
+
return void 0;
|
|
2100
|
+
}
|
|
2101
|
+
try {
|
|
2102
|
+
const { version } = readJson(manifestPath);
|
|
2103
|
+
const major = version.split(".")[0];
|
|
2104
|
+
return {
|
|
2105
|
+
check: `resolved eslint is ${SUPPORTED_ESLINT_MAJOR}.x`,
|
|
2106
|
+
ok: major === SUPPORTED_ESLINT_MAJOR,
|
|
2107
|
+
detail: `node_modules/eslint is ${version}, but this stack supports ${SUPPORTED_ESLINT_MAJOR}.x (eslint-plugin-react has no release beyond it)`,
|
|
2108
|
+
remedy: `pin eslint to ${ESLINT_VERSION} in every package manifest, then reinstall`
|
|
2109
|
+
};
|
|
2110
|
+
} catch {
|
|
2111
|
+
return void 0;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
function checkNpmrc(workspaceRoot, registry, scope) {
|
|
2115
|
+
const npmrcPath = (0, import_node_path12.join)(workspaceRoot, ".npmrc");
|
|
2116
|
+
if (!registry || !fileExists(npmrcPath)) {
|
|
2117
|
+
return void 0;
|
|
2118
|
+
}
|
|
2119
|
+
const npmrc = (0, import_node_fs7.readFileSync)(npmrcPath, "utf8");
|
|
2120
|
+
if (registry.kind === "npm") {
|
|
2121
|
+
return {
|
|
2122
|
+
check: ".npmrc authenticates the public registry",
|
|
2123
|
+
ok: npmrc.includes("//registry.npmjs.org/:_authToken="),
|
|
2124
|
+
detail: "no npmjs.org token line \u2014 `npm publish` cannot authenticate",
|
|
2125
|
+
remedy: "run `mnci upgrade`"
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
return {
|
|
2129
|
+
check: ".npmrc routes the scope to the feed",
|
|
2130
|
+
ok: scope !== void 0 && npmrc.includes(`${scope}:registry=`),
|
|
2131
|
+
detail: `no '${scope ?? "@scope"}:registry=' line \u2014 a scoped package could publish to npmjs.org instead of the feed`,
|
|
2132
|
+
remedy: "run `mnci upgrade`"
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
function checkVersionActions(workspaceRoot) {
|
|
2136
|
+
const candidates = [
|
|
2137
|
+
...(0, import_node_fs7.globSync)("packages/*/pubspec.yaml", { cwd: workspaceRoot }),
|
|
2138
|
+
...(0, import_node_fs7.globSync)("python-packages/*/pyproject.toml", { cwd: workspaceRoot })
|
|
2139
|
+
];
|
|
2140
|
+
return candidates.flatMap((manifest) => {
|
|
2141
|
+
const projectRoot = manifest.slice(0, manifest.lastIndexOf("/"));
|
|
2142
|
+
const projectJsonPath = (0, import_node_path12.join)(workspaceRoot, projectRoot, "project.json");
|
|
2143
|
+
let hasOverride = false;
|
|
2144
|
+
try {
|
|
2145
|
+
const projectJson = readJson(projectJsonPath);
|
|
2146
|
+
hasOverride = Boolean(projectJson.release?.version?.versionActions);
|
|
2147
|
+
} catch {
|
|
2148
|
+
hasOverride = false;
|
|
2149
|
+
}
|
|
2150
|
+
return [
|
|
2151
|
+
{
|
|
2152
|
+
check: `${projectRoot} keeps its versionActions override`,
|
|
2153
|
+
ok: hasOverride,
|
|
2154
|
+
detail: "missing \u2014 nx release aborts for the ENTIRE workspace, not just this project",
|
|
2155
|
+
remedy: `add release.version.versionActions to ${projectRoot}/project.json`
|
|
2156
|
+
}
|
|
2157
|
+
];
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
function checkSync(workspaceRoot) {
|
|
2161
|
+
return {
|
|
2162
|
+
check: "TypeScript project references synced",
|
|
2163
|
+
ok: runShell("npx", ["nx", "sync:check"], workspaceRoot) === 0,
|
|
2164
|
+
detail: "nx sync:check failed \u2014 a stale project reference was never committed",
|
|
2165
|
+
remedy: "run `npx nx sync` and commit the result"
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
function collectFindings(workspaceRoot) {
|
|
2169
|
+
const nxJsonPath = (0, import_node_path12.join)(workspaceRoot, "nx.json");
|
|
2170
|
+
if (!fileExists(nxJsonPath)) {
|
|
2171
|
+
throw new Error(
|
|
2172
|
+
`No nx.json found in ${workspaceRoot} \u2014 run 'mnci doctor' from the workspace root.`
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
const nxJson = readJson(nxJsonPath);
|
|
2176
|
+
return [
|
|
2177
|
+
...checkEslintConfigs(workspaceRoot),
|
|
2178
|
+
checkPrettierConfig(workspaceRoot),
|
|
2179
|
+
checkEslintPlugin(nxJson),
|
|
2180
|
+
checkResolvedEslint(workspaceRoot),
|
|
2181
|
+
checkNpmrc(workspaceRoot, nxJson.mnci?.registry, nxJson.mnci?.scope),
|
|
2182
|
+
...checkVersionActions(workspaceRoot),
|
|
2183
|
+
checkSync(workspaceRoot)
|
|
2184
|
+
].filter((finding) => finding !== void 0);
|
|
2185
|
+
}
|
|
2186
|
+
function runDoctor(workspaceRoot) {
|
|
2187
|
+
const findings = collectFindings(workspaceRoot);
|
|
2188
|
+
const failed = findings.filter((finding) => !finding.ok);
|
|
2189
|
+
for (const finding of findings) {
|
|
2190
|
+
if (finding.ok) {
|
|
2191
|
+
logger.success(finding.check);
|
|
2192
|
+
} else {
|
|
2193
|
+
logger.error(`${finding.check} \u2014 ${finding.detail ?? "failed"}`);
|
|
2194
|
+
if (finding.remedy) {
|
|
2195
|
+
logger.info(` fix: ${finding.remedy}`);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
if (failed.length === 0) {
|
|
2200
|
+
logger.success(`All ${findings.length} checks passed.`);
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
logger.error(`${failed.length} of ${findings.length} checks failed.`);
|
|
2204
|
+
process.exitCode = 1;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// src/commands/interactive.ts
|
|
2208
|
+
var import_node_path15 = require("path");
|
|
2209
|
+
var import_prompts8 = require("@inquirer/prompts");
|
|
2210
|
+
|
|
2056
2211
|
// src/commands/new.ts
|
|
2212
|
+
var import_node_path13 = require("path");
|
|
2057
2213
|
function nxCloudProviderValue(ci) {
|
|
2058
2214
|
return ci === "azure" ? "azure" : "github";
|
|
2059
2215
|
}
|
|
@@ -2118,7 +2274,7 @@ async function runNew(name, options) {
|
|
|
2118
2274
|
],
|
|
2119
2275
|
process.cwd()
|
|
2120
2276
|
);
|
|
2121
|
-
const workspaceRoot = (0,
|
|
2277
|
+
const workspaceRoot = (0, import_node_path13.join)(process.cwd(), workspaceName);
|
|
2122
2278
|
logger.step(
|
|
2123
2279
|
"Applying MoNecromanCI overlay (VS Code workspace, release config, .npmrc, commitlint, pipeline, stack)"
|
|
2124
2280
|
);
|
|
@@ -2152,8 +2308,8 @@ async function runNew(name, options) {
|
|
|
2152
2308
|
}
|
|
2153
2309
|
|
|
2154
2310
|
// src/commands/upgrade.ts
|
|
2155
|
-
var
|
|
2156
|
-
var
|
|
2311
|
+
var import_node_fs8 = require("fs");
|
|
2312
|
+
var import_node_path14 = require("path");
|
|
2157
2313
|
function resolveRegistry2(options, persisted) {
|
|
2158
2314
|
if (options.registry === "azure-artifacts" || options.organization && options.artifactsFeed) {
|
|
2159
2315
|
const persistedAzure = persisted?.kind === "azure-artifacts" ? persisted : void 0;
|
|
@@ -2182,7 +2338,7 @@ function resolveWorkspaceName(workspaceRoot, persisted) {
|
|
|
2182
2338
|
return persisted.workspaceName;
|
|
2183
2339
|
}
|
|
2184
2340
|
try {
|
|
2185
|
-
const existing = (0,
|
|
2341
|
+
const existing = (0, import_node_fs8.readdirSync)(workspaceRoot).find(
|
|
2186
2342
|
(file) => file.endsWith(".code-workspace") && file !== "undefined.code-workspace"
|
|
2187
2343
|
);
|
|
2188
2344
|
if (existing) {
|
|
@@ -2190,7 +2346,7 @@ function resolveWorkspaceName(workspaceRoot, persisted) {
|
|
|
2190
2346
|
}
|
|
2191
2347
|
} catch {
|
|
2192
2348
|
}
|
|
2193
|
-
return (0,
|
|
2349
|
+
return (0, import_node_path14.basename)(workspaceRoot);
|
|
2194
2350
|
}
|
|
2195
2351
|
function resolveOverlayOptions(workspaceRoot, options, persisted) {
|
|
2196
2352
|
const scope = options.scope ?? persisted.scope;
|
|
@@ -2228,7 +2384,7 @@ function resolveOverlayOptions(workspaceRoot, options, persisted) {
|
|
|
2228
2384
|
};
|
|
2229
2385
|
}
|
|
2230
2386
|
function runUpgrade(workspaceRoot, options) {
|
|
2231
|
-
if (!fileExists((0,
|
|
2387
|
+
if (!fileExists((0, import_node_path14.join)(workspaceRoot, "nx.json"))) {
|
|
2232
2388
|
throw new Error(
|
|
2233
2389
|
`No nx.json found in ${workspaceRoot} \u2014 this does not look like an Nx workspace. Run 'mnci upgrade' from the workspace root.`
|
|
2234
2390
|
);
|
|
@@ -2245,7 +2401,7 @@ function runUpgrade(workspaceRoot, options) {
|
|
|
2245
2401
|
|
|
2246
2402
|
// src/commands/interactive.ts
|
|
2247
2403
|
async function runInteractive() {
|
|
2248
|
-
const inWorkspace = fileExists((0,
|
|
2404
|
+
const inWorkspace = fileExists((0, import_node_path15.join)(process.cwd(), "nx.json"));
|
|
2249
2405
|
const newChoice = { name: "Create a new monorepo", value: "new" };
|
|
2250
2406
|
const addChoice = { name: "Add a project to this workspace", value: "add" };
|
|
2251
2407
|
const upgradeChoice = {
|
|
@@ -2266,8 +2422,8 @@ async function runInteractive() {
|
|
|
2266
2422
|
}
|
|
2267
2423
|
|
|
2268
2424
|
// src/util/versionChecker.ts
|
|
2269
|
-
var
|
|
2270
|
-
var
|
|
2425
|
+
var import_node_fs9 = require("fs");
|
|
2426
|
+
var import_node_path16 = require("path");
|
|
2271
2427
|
var import_cross_spawn2 = __toESM(require("cross-spawn"));
|
|
2272
2428
|
var PACKAGE_NAME = "@mnci/cli";
|
|
2273
2429
|
var REGISTRY_TIMEOUT_MS = 2e3;
|
|
@@ -2323,8 +2479,8 @@ function releaseTriple(version) {
|
|
|
2323
2479
|
}
|
|
2324
2480
|
function readCliVersion(packageDirectory) {
|
|
2325
2481
|
try {
|
|
2326
|
-
const manifestPath = (0,
|
|
2327
|
-
const manifest = JSON.parse((0,
|
|
2482
|
+
const manifestPath = (0, import_node_path16.join)(packageDirectory, "..", "package.json");
|
|
2483
|
+
const manifest = JSON.parse((0, import_node_fs9.readFileSync)(manifestPath, "utf8"));
|
|
2328
2484
|
return manifest.version ?? "0.0.0";
|
|
2329
2485
|
} catch {
|
|
2330
2486
|
return "0.0.0";
|
|
@@ -2349,6 +2505,11 @@ function buildProgram(cliVersion) {
|
|
|
2349
2505
|
).action((options) => {
|
|
2350
2506
|
runUpgrade(process.cwd(), options);
|
|
2351
2507
|
});
|
|
2508
|
+
program.command("doctor").description(
|
|
2509
|
+
"Check this workspace against the invariants mnci maintains (one ESLint config, no stray .prettierrc, the eslint plugin registered, the resolved eslint major, .npmrc vs the recorded registry, versionActions overrides, nx sync) \u2014 read-only; exits non-zero if anything failed"
|
|
2510
|
+
).action(() => {
|
|
2511
|
+
runDoctor(process.cwd());
|
|
2512
|
+
});
|
|
2352
2513
|
program.command("add").addArgument(new import_commander.Argument("[kind]", "project kind").choices(PROJECT_KINDS)).argument("[name]", "project name").description("Add a project by delegating to the matching Nx plugin generator").option("--scope <scope>", "npm scope for a publishable lib (defaults to @<workspace name>)").option(
|
|
2353
2514
|
"--framework <framework>",
|
|
2354
2515
|
"node-app only: express | fastify | koa | nest | none (default: none)"
|