@mandujs/core 0.53.1 → 0.53.3
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/package.json +1 -1
- package/src/bundler/build.ts +54 -26
- package/src/bundler/dev.ts +21 -17
- package/src/bundler/types.ts +8 -3
- package/src/config/validate.ts +3 -3
- package/src/db/index.ts +138 -51
- package/src/db/migrations/lock.ts +67 -12
- package/src/db/migrations/runner.ts +118 -101
- package/src/diagnose/__tests__/checks.test.ts +74 -3
- package/src/diagnose/checks.ts +112 -0
- package/src/diagnose/index.ts +1 -0
- package/src/diagnose/run.ts +2 -0
- package/src/kitchen/api/agent-devtools-api.ts +544 -0
- package/src/kitchen/kitchen-handler.ts +33 -16
- package/src/kitchen/kitchen-ui.ts +346 -62
- package/src/resource/__tests__/generator.test.ts +32 -15
- package/src/resource/ddl/__tests__/emit.test.ts +24 -0
- package/src/resource/ddl/emit.ts +12 -1
- package/src/resource/generator-repo.ts +40 -20
- package/src/runtime/ssr.ts +24 -31
- package/src/runtime/streaming-ssr.ts +32 -37
|
@@ -69,7 +69,7 @@ import type {
|
|
|
69
69
|
PendingMigration,
|
|
70
70
|
SqlProvider,
|
|
71
71
|
} from "../../resource/ddl/types";
|
|
72
|
-
import type
|
|
72
|
+
import { withPinnedDbHandle, type Db } from "../index";
|
|
73
73
|
import {
|
|
74
74
|
DEFAULT_HISTORY_TABLE,
|
|
75
75
|
SAFE_HISTORY_TABLE_RE,
|
|
@@ -128,6 +128,24 @@ export class MigrationTimeoutError extends Error {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
function assertNoTamperedHistory(
|
|
132
|
+
history: HistoryRow[],
|
|
133
|
+
diskByVersion: Map<string, PendingMigration>,
|
|
134
|
+
): void {
|
|
135
|
+
for (const row of history) {
|
|
136
|
+
if (row.success !== 1) continue;
|
|
137
|
+
const disk = diskByVersion.get(row.version);
|
|
138
|
+
if (!disk) continue; // orphan on the history side — surfaced via status(), not apply()
|
|
139
|
+
if (disk.checksum !== row.checksum) {
|
|
140
|
+
throw new MigrationTamperedError(
|
|
141
|
+
disk.filename,
|
|
142
|
+
row.checksum,
|
|
143
|
+
disk.checksum,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
132
150
|
|
|
133
151
|
/** Options for {@link createMigrationRunner}. */
|
|
@@ -246,34 +264,18 @@ export function createMigrationRunner(
|
|
|
246
264
|
async function apply(
|
|
247
265
|
opts: { dryRun?: boolean } = {},
|
|
248
266
|
): Promise<AppliedMigration[]> {
|
|
249
|
-
await ensureReady();
|
|
250
|
-
|
|
251
|
-
// Tamper check BEFORE acquiring the lock so the fast-fail path
|
|
252
|
-
// doesn't hold the advisory lock longer than necessary.
|
|
253
|
-
const history = await readAllHistory(db, historyTable);
|
|
254
267
|
const diskFiles = await readMigrationsFromDisk(migrationsDir);
|
|
255
268
|
const diskByVersion = new Map(diskFiles.map((f) => [f.version, f]));
|
|
256
|
-
for (const row of history) {
|
|
257
|
-
if (row.success !== 1) continue;
|
|
258
|
-
const disk = diskByVersion.get(row.version);
|
|
259
|
-
if (!disk) continue; // orphan on the history side — surfaced via status(), not apply()
|
|
260
|
-
if (disk.checksum !== row.checksum) {
|
|
261
|
-
throw new MigrationTamperedError(
|
|
262
|
-
disk.filename,
|
|
263
|
-
row.checksum,
|
|
264
|
-
disk.checksum,
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
269
|
|
|
269
|
-
const appliedVersions = new Set(
|
|
270
|
-
history.filter((h) => h.success === 1).map((h) => h.version),
|
|
271
|
-
);
|
|
272
|
-
const pending = diskFiles.filter((f) => !appliedVersions.has(f.version));
|
|
273
|
-
if (pending.length === 0) return [];
|
|
274
|
-
|
|
275
|
-
// Dry-run: report what we WOULD apply, no IO, no history.
|
|
276
270
|
if (opts.dryRun === true) {
|
|
271
|
+
await ensureReady();
|
|
272
|
+
const history = await readAllHistory(db, historyTable);
|
|
273
|
+
assertNoTamperedHistory(history, diskByVersion);
|
|
274
|
+
|
|
275
|
+
const appliedVersions = new Set(
|
|
276
|
+
history.filter((h) => h.success === 1).map((h) => h.version),
|
|
277
|
+
);
|
|
278
|
+
const pending = diskFiles.filter((f) => !appliedVersions.has(f.version));
|
|
277
279
|
return pending.map<AppliedMigration>((p) => ({
|
|
278
280
|
version: p.version,
|
|
279
281
|
filename: p.filename,
|
|
@@ -290,100 +292,115 @@ export function createMigrationRunner(
|
|
|
290
292
|
|
|
291
293
|
const applied: AppliedMigration[] = [];
|
|
292
294
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
295
|
+
const runLockedApply = async (): Promise<AppliedMigration[]> => {
|
|
296
|
+
heldLock = await acquireMigrationLock(db, lockStrategy);
|
|
297
|
+
try {
|
|
298
|
+
await ensureReady();
|
|
299
|
+
const lockedHistory = await readAllHistory(db, historyTable);
|
|
300
|
+
assertNoTamperedHistory(lockedHistory, diskByVersion);
|
|
301
|
+
const lockedAppliedVersions = new Set(
|
|
302
|
+
lockedHistory.filter((h) => h.success === 1).map((h) => h.version),
|
|
303
|
+
);
|
|
304
|
+
const lockedPending = diskFiles.filter((f) => !lockedAppliedVersions.has(f.version));
|
|
305
|
+
|
|
306
|
+
for (const migration of lockedPending) {
|
|
307
|
+
const start = Date.now();
|
|
308
|
+
|
|
309
|
+
const statements = splitStatements(migration.sql);
|
|
310
|
+
if (statements.length === 0) {
|
|
311
|
+
// Empty migration — still record a history row so we don't
|
|
312
|
+
// re-run it. execution_ms = 0 reflects reality.
|
|
313
|
+
await insertHistory(db, historyTable, {
|
|
314
|
+
version: migration.version,
|
|
315
|
+
filename: migration.filename,
|
|
316
|
+
checksum: migration.checksum,
|
|
317
|
+
applied_at: new Date(),
|
|
318
|
+
execution_ms: 0,
|
|
319
|
+
success: 1,
|
|
320
|
+
installed_by: installedBy,
|
|
321
|
+
});
|
|
322
|
+
applied.push({
|
|
323
|
+
version: migration.version,
|
|
324
|
+
filename: migration.filename,
|
|
325
|
+
checksum: migration.checksum,
|
|
326
|
+
appliedAt: new Date(),
|
|
327
|
+
executionMs: 0,
|
|
328
|
+
success: true,
|
|
329
|
+
});
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
await db.transaction(async (tx) => {
|
|
335
|
+
for (const stmt of statements) {
|
|
336
|
+
await execRaw(tx, stmt);
|
|
337
|
+
const elapsed = Date.now() - start;
|
|
338
|
+
if (elapsed > applyTimeoutMs) {
|
|
339
|
+
throw new MigrationTimeoutError(
|
|
340
|
+
migration.filename,
|
|
341
|
+
elapsed,
|
|
342
|
+
applyTimeoutMs,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
} catch (err) {
|
|
348
|
+
if (err instanceof MigrationTimeoutError) throw err;
|
|
349
|
+
// Wrap with migration context so downstream callers know
|
|
350
|
+
// which file blew up. Preserve the original stack where
|
|
351
|
+
// possible via `cause`.
|
|
352
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
353
|
+
const wrapped = new Error(
|
|
354
|
+
`[@mandujs/core/db/migrations] Failed to apply ${migration.filename}: ${msg}`,
|
|
355
|
+
);
|
|
356
|
+
// Preserve the original as a `cause` chain for diagnostics.
|
|
357
|
+
(wrapped as { cause?: unknown }).cause = err;
|
|
358
|
+
throw wrapped;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const executionMs = Date.now() - start;
|
|
362
|
+
const appliedAt = new Date();
|
|
363
|
+
|
|
364
|
+
// History row is written AFTER the SQL transaction commits.
|
|
365
|
+
// If this INSERT itself fails, the migration has run but we
|
|
366
|
+
// have no record — the user will see it as pending again.
|
|
367
|
+
// Mitigation: the insert is a single tiny statement; in
|
|
368
|
+
// practice it either succeeds or the whole connection is
|
|
369
|
+
// dead (in which case subsequent apply() calls will also fail
|
|
370
|
+
// and the user will debug from the DB side).
|
|
302
371
|
await insertHistory(db, historyTable, {
|
|
303
372
|
version: migration.version,
|
|
304
373
|
filename: migration.filename,
|
|
305
374
|
checksum: migration.checksum,
|
|
306
|
-
applied_at:
|
|
307
|
-
execution_ms:
|
|
375
|
+
applied_at: appliedAt,
|
|
376
|
+
execution_ms: executionMs,
|
|
308
377
|
success: 1,
|
|
309
378
|
installed_by: installedBy,
|
|
310
379
|
});
|
|
380
|
+
|
|
311
381
|
applied.push({
|
|
312
382
|
version: migration.version,
|
|
313
383
|
filename: migration.filename,
|
|
314
384
|
checksum: migration.checksum,
|
|
315
|
-
appliedAt
|
|
316
|
-
executionMs
|
|
385
|
+
appliedAt,
|
|
386
|
+
executionMs,
|
|
317
387
|
success: true,
|
|
318
388
|
});
|
|
319
|
-
continue;
|
|
320
389
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
await
|
|
324
|
-
|
|
325
|
-
await execRaw(tx, stmt);
|
|
326
|
-
const elapsed = Date.now() - start;
|
|
327
|
-
if (elapsed > applyTimeoutMs) {
|
|
328
|
-
throw new MigrationTimeoutError(
|
|
329
|
-
migration.filename,
|
|
330
|
-
elapsed,
|
|
331
|
-
applyTimeoutMs,
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
} catch (err) {
|
|
337
|
-
if (err instanceof MigrationTimeoutError) throw err;
|
|
338
|
-
// Wrap with migration context so downstream callers know
|
|
339
|
-
// which file blew up. Preserve the original stack where
|
|
340
|
-
// possible via `cause`.
|
|
341
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
342
|
-
const wrapped = new Error(
|
|
343
|
-
`[@mandujs/core/db/migrations] Failed to apply ${migration.filename}: ${msg}`,
|
|
344
|
-
);
|
|
345
|
-
// Preserve the original as a `cause` chain for diagnostics.
|
|
346
|
-
(wrapped as { cause?: unknown }).cause = err;
|
|
347
|
-
throw wrapped;
|
|
390
|
+
} finally {
|
|
391
|
+
if (heldLock) {
|
|
392
|
+
await heldLock.release();
|
|
393
|
+
heldLock = null;
|
|
348
394
|
}
|
|
395
|
+
}
|
|
349
396
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
// History row is written AFTER the SQL transaction commits.
|
|
354
|
-
// If this INSERT itself fails, the migration has run but we
|
|
355
|
-
// have no record — the user will see it as pending again.
|
|
356
|
-
// Mitigation: the insert is a single tiny statement; in
|
|
357
|
-
// practice it either succeeds or the whole connection is
|
|
358
|
-
// dead (in which case subsequent apply() calls will also fail
|
|
359
|
-
// and the user will debug from the DB side).
|
|
360
|
-
await insertHistory(db, historyTable, {
|
|
361
|
-
version: migration.version,
|
|
362
|
-
filename: migration.filename,
|
|
363
|
-
checksum: migration.checksum,
|
|
364
|
-
applied_at: appliedAt,
|
|
365
|
-
execution_ms: executionMs,
|
|
366
|
-
success: 1,
|
|
367
|
-
installed_by: installedBy,
|
|
368
|
-
});
|
|
397
|
+
return applied;
|
|
398
|
+
};
|
|
369
399
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
filename: migration.filename,
|
|
373
|
-
checksum: migration.checksum,
|
|
374
|
-
appliedAt,
|
|
375
|
-
executionMs,
|
|
376
|
-
success: true,
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
} finally {
|
|
380
|
-
if (heldLock) {
|
|
381
|
-
await heldLock.release();
|
|
382
|
-
heldLock = null;
|
|
383
|
-
}
|
|
400
|
+
if (lockStrategy === "mysql_get_lock") {
|
|
401
|
+
return await withPinnedDbHandle(db, runLockedApply);
|
|
384
402
|
}
|
|
385
|
-
|
|
386
|
-
return applied;
|
|
403
|
+
return await runLockedApply();
|
|
387
404
|
}
|
|
388
405
|
|
|
389
406
|
async function status(): Promise<MigrationStatus> {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
checkCloneElementWarnings,
|
|
18
18
|
checkDevArtifactsInProd,
|
|
19
19
|
checkPackageExportGaps,
|
|
20
|
+
checkNestedInternalCore,
|
|
20
21
|
} from "../checks";
|
|
21
22
|
import { runExtendedDiagnose, buildReport } from "../run";
|
|
22
23
|
|
|
@@ -319,6 +320,75 @@ describe("checkPackageExportGaps", () => {
|
|
|
319
320
|
});
|
|
320
321
|
});
|
|
321
322
|
|
|
323
|
+
// ──────────────────────────────────────────────────────────────────
|
|
324
|
+
// nested_internal_core (#261)
|
|
325
|
+
// ──────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
describe("checkNestedInternalCore", () => {
|
|
328
|
+
let rootDir: string;
|
|
329
|
+
beforeEach(async () => { rootDir = await mkTmpRoot(); });
|
|
330
|
+
afterEach(async () => { await fs.rm(rootDir, { recursive: true, force: true }); });
|
|
331
|
+
|
|
332
|
+
async function seedHoistedCore(version: string): Promise<void> {
|
|
333
|
+
await writeFile(rootDir, "node_modules/@mandujs/core/package.json", JSON.stringify({
|
|
334
|
+
name: "@mandujs/core", version,
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function seedNestedCore(parent: string, version: string): Promise<void> {
|
|
339
|
+
await writeFile(
|
|
340
|
+
rootDir,
|
|
341
|
+
`node_modules/@mandujs/${parent}/node_modules/@mandujs/core/package.json`,
|
|
342
|
+
JSON.stringify({ name: "@mandujs/core", version }),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
it("skips when hoisted @mandujs/core is not installed", async () => {
|
|
347
|
+
const result = await checkNestedInternalCore(rootDir);
|
|
348
|
+
expect(result.ok).toBe(true);
|
|
349
|
+
expect(result.details?.skipped).toBe(true);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("passes when no nested core exists", async () => {
|
|
353
|
+
await seedHoistedCore("0.53.2");
|
|
354
|
+
// Seed a sibling without nested core
|
|
355
|
+
await writeFile(rootDir, "node_modules/@mandujs/mcp/package.json", JSON.stringify({
|
|
356
|
+
name: "@mandujs/mcp", version: "0.36.2",
|
|
357
|
+
}));
|
|
358
|
+
const result = await checkNestedInternalCore(rootDir);
|
|
359
|
+
expect(result.ok).toBe(true);
|
|
360
|
+
expect(result.details?.scannedSiblings).toBe(1);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("passes when nested core matches hoisted version exactly", async () => {
|
|
364
|
+
await seedHoistedCore("0.53.2");
|
|
365
|
+
await seedNestedCore("mcp", "0.53.2");
|
|
366
|
+
const result = await checkNestedInternalCore(rootDir);
|
|
367
|
+
expect(result.ok).toBe(true);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("flags a stale nested core that shadows hoisted (#261 repro)", async () => {
|
|
371
|
+
await seedHoistedCore("0.53.2");
|
|
372
|
+
await seedNestedCore("mcp", "0.47.0");
|
|
373
|
+
const result = await checkNestedInternalCore(rootDir);
|
|
374
|
+
expect(result.ok).toBe(false);
|
|
375
|
+
expect(result.severity).toBe("error");
|
|
376
|
+
expect(result.message).toMatch(/@mandujs\/mcp/);
|
|
377
|
+
expect(result.message).toMatch(/0\.47\.0/);
|
|
378
|
+
expect(result.suggestion).toMatch(/rm -rf node_modules\/@mandujs\/mcp\/node_modules/);
|
|
379
|
+
expect(result.details?.mismatchCount).toBe(1);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("reports every mismatched sibling, not just one", async () => {
|
|
383
|
+
await seedHoistedCore("0.53.2");
|
|
384
|
+
await seedNestedCore("mcp", "0.47.0");
|
|
385
|
+
await seedNestedCore("ate", "0.50.0");
|
|
386
|
+
const result = await checkNestedInternalCore(rootDir);
|
|
387
|
+
expect(result.ok).toBe(false);
|
|
388
|
+
expect(result.details?.mismatchCount).toBe(2);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
322
392
|
// ──────────────────────────────────────────────────────────────────
|
|
323
393
|
// aggregator
|
|
324
394
|
// ──────────────────────────────────────────────────────────────────
|
|
@@ -328,10 +398,10 @@ describe("runExtendedDiagnose", () => {
|
|
|
328
398
|
beforeEach(async () => { rootDir = await mkTmpRoot(); });
|
|
329
399
|
afterEach(async () => { await fs.rm(rootDir, { recursive: true, force: true }); });
|
|
330
400
|
|
|
331
|
-
it("runs all
|
|
401
|
+
it("runs all 7 extended checks and returns a structured report", async () => {
|
|
332
402
|
const report = await runExtendedDiagnose(rootDir);
|
|
333
|
-
//
|
|
334
|
-
expect(report.summary.total).toBe(
|
|
403
|
+
// #261 added `nested_internal_core` — total is now 7.
|
|
404
|
+
expect(report.summary.total).toBe(7);
|
|
335
405
|
// manifest is missing → at least one error
|
|
336
406
|
expect(report.healthy).toBe(false);
|
|
337
407
|
expect(report.errorCount).toBeGreaterThanOrEqual(1);
|
|
@@ -341,6 +411,7 @@ describe("runExtendedDiagnose", () => {
|
|
|
341
411
|
expect(rules).toContain("cloneelement_warnings");
|
|
342
412
|
expect(rules).toContain("dev_artifacts_in_prod");
|
|
343
413
|
expect(rules).toContain("package_export_gaps");
|
|
414
|
+
expect(rules).toContain("nested_internal_core");
|
|
344
415
|
expect(rules).toContain("a11y_hints");
|
|
345
416
|
});
|
|
346
417
|
|
package/src/diagnose/checks.ts
CHANGED
|
@@ -718,3 +718,115 @@ export async function checkA11yHints(rootDir: string): Promise<DiagnoseCheckResu
|
|
|
718
718
|
},
|
|
719
719
|
};
|
|
720
720
|
}
|
|
721
|
+
|
|
722
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
723
|
+
// 7. nested_internal_core (#261)
|
|
724
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
725
|
+
|
|
726
|
+
interface NestedCoreSite {
|
|
727
|
+
parentPackage: string;
|
|
728
|
+
nestedVersion: string;
|
|
729
|
+
relativePath: string;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function readPackageVersion(pkgJsonPath: string): Promise<string | null> {
|
|
733
|
+
try {
|
|
734
|
+
const raw = await fs.readFile(pkgJsonPath, "utf-8");
|
|
735
|
+
const parsed = JSON.parse(raw) as { version?: string };
|
|
736
|
+
return typeof parsed.version === "string" ? parsed.version : null;
|
|
737
|
+
} catch {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async function listMandujsSiblings(rootDir: string): Promise<string[]> {
|
|
743
|
+
const dir = path.join(rootDir, "node_modules", "@mandujs");
|
|
744
|
+
try {
|
|
745
|
+
const entries = (await fs.readdir(dir, { withFileTypes: true })) as Dirent[];
|
|
746
|
+
return entries
|
|
747
|
+
.filter((e) => e.isDirectory() && e.name !== "core")
|
|
748
|
+
.map((e) => e.name);
|
|
749
|
+
} catch {
|
|
750
|
+
return [];
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* #261 check 7: detect a stale `@mandujs/core` nested inside another
|
|
756
|
+
* `@mandujs/*` package's `node_modules`.
|
|
757
|
+
*
|
|
758
|
+
* Symptom: `bunx @mandujs/mcp` fails with `Cannot find module
|
|
759
|
+
* @mandujs/core/<subpath>` even though the project has the latest
|
|
760
|
+
* `@mandujs/core` hoisted at the top level. Cause: a previous install
|
|
761
|
+
* left an older core nested under `@mandujs/<sibling>/node_modules`,
|
|
762
|
+
* which wins module resolution from inside that sibling's code, and the
|
|
763
|
+
* older core's `exports` map lacks the subpath the sibling now imports.
|
|
764
|
+
*
|
|
765
|
+
* This check walks `node_modules/@mandujs/*` siblings, looks for a
|
|
766
|
+
* nested `node_modules/@mandujs/core/package.json`, and compares the
|
|
767
|
+
* version to the hoisted core. A mismatch is reported as `error`
|
|
768
|
+
* (boot-breaking on the user's machine) with a copy-pastable fix.
|
|
769
|
+
*/
|
|
770
|
+
export async function checkNestedInternalCore(rootDir: string): Promise<DiagnoseCheckResult> {
|
|
771
|
+
const hoistedPath = path.join(rootDir, "node_modules", "@mandujs", "core", "package.json");
|
|
772
|
+
const hoistedVersion = await readPackageVersion(hoistedPath);
|
|
773
|
+
|
|
774
|
+
if (!hoistedVersion) {
|
|
775
|
+
return {
|
|
776
|
+
ok: true,
|
|
777
|
+
rule: "nested_internal_core",
|
|
778
|
+
message: "@mandujs/core not installed at the project root — skipping nested-version check.",
|
|
779
|
+
details: { skipped: true },
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const siblings = await listMandujsSiblings(rootDir);
|
|
784
|
+
const mismatches: NestedCoreSite[] = [];
|
|
785
|
+
|
|
786
|
+
for (const sibling of siblings) {
|
|
787
|
+
const nestedPkgJson = path.join(
|
|
788
|
+
rootDir,
|
|
789
|
+
"node_modules",
|
|
790
|
+
"@mandujs",
|
|
791
|
+
sibling,
|
|
792
|
+
"node_modules",
|
|
793
|
+
"@mandujs",
|
|
794
|
+
"core",
|
|
795
|
+
"package.json",
|
|
796
|
+
);
|
|
797
|
+
const nestedVersion = await readPackageVersion(nestedPkgJson);
|
|
798
|
+
if (!nestedVersion) continue;
|
|
799
|
+
if (nestedVersion === hoistedVersion) continue;
|
|
800
|
+
mismatches.push({
|
|
801
|
+
parentPackage: `@mandujs/${sibling}`,
|
|
802
|
+
nestedVersion,
|
|
803
|
+
relativePath: path.relative(rootDir, nestedPkgJson),
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (mismatches.length === 0) {
|
|
808
|
+
return {
|
|
809
|
+
ok: true,
|
|
810
|
+
rule: "nested_internal_core",
|
|
811
|
+
message: `No stale nested @mandujs/core found (hoisted: ${hoistedVersion}, scanned ${siblings.length} sibling(s)).`,
|
|
812
|
+
details: { hoistedVersion, scannedSiblings: siblings.length },
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const first = mismatches[0];
|
|
817
|
+
const fixCommand = `rm -rf node_modules/${first.parentPackage}/node_modules`;
|
|
818
|
+
return {
|
|
819
|
+
ok: false,
|
|
820
|
+
rule: "nested_internal_core",
|
|
821
|
+
severity: "error",
|
|
822
|
+
message:
|
|
823
|
+
`${mismatches.length} stale nested @mandujs/core install(s) shadow the hoisted ${hoistedVersion}. ` +
|
|
824
|
+
`First: ${first.parentPackage} pinned to ${first.nestedVersion} (${first.relativePath}).`,
|
|
825
|
+
suggestion: `Run \`${fixCommand}\` and re-test \`bunx @mandujs/mcp\`, or remove node_modules and bun.lock entirely and \`bun install\`.`,
|
|
826
|
+
details: {
|
|
827
|
+
hoistedVersion,
|
|
828
|
+
mismatchCount: mismatches.length,
|
|
829
|
+
mismatches,
|
|
830
|
+
},
|
|
831
|
+
};
|
|
832
|
+
}
|
package/src/diagnose/index.ts
CHANGED
package/src/diagnose/run.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
checkCloneElementWarnings,
|
|
13
13
|
checkDevArtifactsInProd,
|
|
14
14
|
checkPackageExportGaps,
|
|
15
|
+
checkNestedInternalCore,
|
|
15
16
|
checkA11yHints,
|
|
16
17
|
} from "./checks";
|
|
17
18
|
|
|
@@ -30,6 +31,7 @@ export const EXTENDED_CHECKS = [
|
|
|
30
31
|
{ name: "cloneelement_warnings", run: checkCloneElementWarnings },
|
|
31
32
|
{ name: "dev_artifacts_in_prod", run: checkDevArtifactsInProd },
|
|
32
33
|
{ name: "package_export_gaps", run: checkPackageExportGaps },
|
|
34
|
+
{ name: "nested_internal_core", run: checkNestedInternalCore },
|
|
33
35
|
{ name: "a11y_hints", run: checkA11yHints },
|
|
34
36
|
] as const;
|
|
35
37
|
|