@leanmcp/cli 0.2.14 → 0.3.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.
Files changed (2) hide show
  1. package/dist/index.js +129 -32
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import chalk from "chalk";
17
17
  import ora from "ora";
18
18
  import path3 from "path";
19
19
  import fs3 from "fs-extra";
20
+ import crypto from "crypto";
20
21
  import chokidar from "chokidar";
21
22
 
22
23
  // src/vite/scanUIApp.ts
@@ -131,6 +132,29 @@ import react from "@vitejs/plugin-react";
131
132
  import { viteSingleFile } from "vite-plugin-singlefile";
132
133
  import fs2 from "fs-extra";
133
134
  import path2 from "path";
135
+ function resolveReactDependency(startDir, packageName) {
136
+ const localPath = path2.join(startDir, "node_modules", packageName);
137
+ if (fs2.existsSync(localPath)) {
138
+ return localPath;
139
+ }
140
+ let currentDir = path2.dirname(startDir);
141
+ const maxDepth = 10;
142
+ let depth = 0;
143
+ while (depth < maxDepth) {
144
+ const nodeModulesPath = path2.join(currentDir, "node_modules", packageName);
145
+ if (fs2.existsSync(nodeModulesPath)) {
146
+ return nodeModulesPath;
147
+ }
148
+ const parentDir = path2.dirname(currentDir);
149
+ if (parentDir === currentDir) {
150
+ break;
151
+ }
152
+ currentDir = parentDir;
153
+ depth++;
154
+ }
155
+ return localPath;
156
+ }
157
+ __name(resolveReactDependency, "resolveReactDependency");
134
158
  async function buildUIComponent(uiApp, projectDir, isDev = false) {
135
159
  const { componentPath, componentName, resourceUri } = uiApp;
136
160
  const safeFileName = resourceUri.replace("ui://", "").replace(/\//g, "-") + ".html";
@@ -318,6 +342,8 @@ createRoot(document.getElementById('root')!).render(
318
342
  );
319
343
  `);
320
344
  try {
345
+ const reactPath = resolveReactDependency(projectDir, "react");
346
+ const reactDomPath = resolveReactDependency(projectDir, "react-dom");
321
347
  await vite.build({
322
348
  root: tempDir,
323
349
  plugins: [
@@ -326,11 +352,11 @@ createRoot(document.getElementById('root')!).render(
326
352
  ],
327
353
  resolve: {
328
354
  alias: {
329
- // Resolve React from user's project node_modules
330
- "react": path2.join(projectDir, "node_modules", "react"),
331
- "react-dom": path2.join(projectDir, "node_modules", "react-dom"),
332
- "react/jsx-runtime": path2.join(projectDir, "node_modules", "react", "jsx-runtime"),
333
- "react/jsx-dev-runtime": path2.join(projectDir, "node_modules", "react", "jsx-dev-runtime")
355
+ // Resolve React from project or workspace root node_modules
356
+ "react": reactPath,
357
+ "react-dom": reactDomPath,
358
+ "react/jsx-runtime": path2.join(reactPath, "jsx-runtime"),
359
+ "react/jsx-dev-runtime": path2.join(reactPath, "jsx-dev-runtime")
334
360
  }
335
361
  },
336
362
  css: {
@@ -386,8 +412,49 @@ async function writeUIManifest(manifest, projectDir) {
386
412
  });
387
413
  }
388
414
  __name(writeUIManifest, "writeUIManifest");
415
+ async function deleteUIComponent(uri, projectDir) {
416
+ const safeFileName = uri.replace("ui://", "").replace(/\//g, "-") + ".html";
417
+ const htmlPath = path2.join(projectDir, "dist", "ui", safeFileName);
418
+ if (await fs2.pathExists(htmlPath)) {
419
+ await fs2.remove(htmlPath);
420
+ }
421
+ }
422
+ __name(deleteUIComponent, "deleteUIComponent");
389
423
 
390
424
  // src/commands/dev.ts
425
+ function computeHash(filePath) {
426
+ const content = fs3.readFileSync(filePath, "utf-8");
427
+ return crypto.createHash("md5").update(content).digest("hex");
428
+ }
429
+ __name(computeHash, "computeHash");
430
+ function computeDiff(previousUIApps, currentUIApps, hashCache) {
431
+ const previousURIs = new Set(previousUIApps.map((app) => app.resourceUri));
432
+ const currentURIs = new Set(currentUIApps.map((app) => app.resourceUri));
433
+ const removed = Array.from(previousURIs).filter((uri) => !currentURIs.has(uri));
434
+ const added = new Set(Array.from(currentURIs).filter((uri) => !previousURIs.has(uri)));
435
+ const addedOrChanged = [];
436
+ for (const app of currentUIApps) {
437
+ const isNew = added.has(app.resourceUri);
438
+ if (isNew) {
439
+ addedOrChanged.push(app);
440
+ } else {
441
+ const oldHash = hashCache.get(app.resourceUri);
442
+ let newHash;
443
+ if (fs3.existsSync(app.componentPath)) {
444
+ newHash = computeHash(app.componentPath);
445
+ }
446
+ if (oldHash !== newHash) {
447
+ addedOrChanged.push(app);
448
+ }
449
+ }
450
+ }
451
+ return {
452
+ removed,
453
+ added,
454
+ addedOrChanged
455
+ };
456
+ }
457
+ __name(computeDiff, "computeDiff");
391
458
  async function devCommand() {
392
459
  const cwd = process.cwd();
393
460
  if (!await fs3.pathExists(path3.join(cwd, "main.ts"))) {
@@ -395,16 +462,13 @@ async function devCommand() {
395
462
  console.error(chalk.gray("Run this command from your project root."));
396
463
  process.exit(1);
397
464
  }
398
- console.log(chalk.cyan("\n\u{1F680} LeanMCP Development Server\n"));
465
+ console.log(chalk.cyan("\nLeanMCP Development Server\n"));
399
466
  const scanSpinner = ora("Scanning for @UIApp components...").start();
400
467
  const uiApps = await scanUIApp(cwd);
401
468
  if (uiApps.length === 0) {
402
- scanSpinner.succeed("No @UIApp components found");
469
+ scanSpinner.info("No @UIApp components found");
403
470
  } else {
404
- scanSpinner.succeed(`Found ${uiApps.length} @UIApp component(s)`);
405
- for (const app of uiApps) {
406
- console.log(chalk.gray(` \u2022 ${app.componentName} \u2192 ${app.resourceUri}`));
407
- }
471
+ scanSpinner.info(`Found ${uiApps.length} @UIApp component(s)`);
408
472
  }
409
473
  const manifest = {};
410
474
  if (uiApps.length > 0) {
@@ -422,10 +486,10 @@ async function devCommand() {
422
486
  if (errors.length > 0) {
423
487
  buildSpinner.warn("Built with warnings");
424
488
  for (const error of errors) {
425
- console.error(chalk.yellow(` \u26A0 ${error}`));
489
+ console.error(chalk.yellow(` ${error}`));
426
490
  }
427
491
  } else {
428
- buildSpinner.succeed("UI components built");
492
+ buildSpinner.info("UI components built");
429
493
  }
430
494
  }
431
495
  console.log(chalk.cyan("\nStarting development server...\n"));
@@ -439,26 +503,59 @@ async function devCommand() {
439
503
  shell: true
440
504
  });
441
505
  let watcher = null;
442
- if (uiApps.length > 0) {
443
- const componentPaths = uiApps.map((app) => app.componentPath);
444
- watcher = chokidar.watch(componentPaths, {
445
- ignoreInitial: true
446
- });
447
- watcher.on("change", async (changedPath) => {
448
- const app = uiApps.find((a) => a.componentPath === changedPath);
449
- if (!app) return;
450
- console.log(chalk.cyan(`
451
- [UI] Rebuilding ${app.componentName}...`));
452
- const result = await buildUIComponent(app, cwd, true);
453
- if (result.success) {
454
- manifest[app.resourceUri] = result.htmlPath;
455
- await writeUIManifest(manifest, cwd);
456
- console.log(chalk.green(`[UI] ${app.componentName} rebuilt successfully`));
457
- } else {
458
- console.log(chalk.yellow(`[UI] ${app.componentName} build failed: ${result.error}`));
459
- }
460
- });
506
+ const componentHashCache = /* @__PURE__ */ new Map();
507
+ for (const app of uiApps) {
508
+ if (await fs3.pathExists(app.componentPath)) {
509
+ componentHashCache.set(app.resourceUri, computeHash(app.componentPath));
510
+ }
461
511
  }
512
+ let previousUIApps = uiApps;
513
+ const mcpPath = path3.join(cwd, "mcp");
514
+ watcher = chokidar.watch(mcpPath, {
515
+ ignoreInitial: true,
516
+ ignored: [
517
+ "**/node_modules/**",
518
+ "**/*.d.ts"
519
+ ],
520
+ persistent: true,
521
+ awaitWriteFinish: {
522
+ stabilityThreshold: 100,
523
+ pollInterval: 50
524
+ }
525
+ });
526
+ let debounceTimer = null;
527
+ watcher.on("all", async (event, changedPath) => {
528
+ if (debounceTimer) clearTimeout(debounceTimer);
529
+ debounceTimer = setTimeout(async () => {
530
+ const currentUIApps = await scanUIApp(cwd);
531
+ const diff = computeDiff(previousUIApps, currentUIApps, componentHashCache);
532
+ if (diff.removed.length === 0 && diff.addedOrChanged.length === 0) {
533
+ return;
534
+ }
535
+ for (const uri of diff.removed) {
536
+ console.log(chalk.yellow(`Removing ${uri}...`));
537
+ await deleteUIComponent(uri, cwd);
538
+ delete manifest[uri];
539
+ componentHashCache.delete(uri);
540
+ }
541
+ for (const app of diff.addedOrChanged) {
542
+ const action = diff.added.has(app.resourceUri) ? "Building" : "Rebuilding";
543
+ console.log(chalk.cyan(`${action} ${app.componentName}...`));
544
+ const result = await buildUIComponent(app, cwd, true);
545
+ if (result.success) {
546
+ manifest[app.resourceUri] = result.htmlPath;
547
+ if (await fs3.pathExists(app.componentPath)) {
548
+ componentHashCache.set(app.resourceUri, computeHash(app.componentPath));
549
+ }
550
+ console.log(chalk.green(`${app.componentName} ${action.toLowerCase()} complete`));
551
+ } else {
552
+ console.log(chalk.yellow(`Build failed: ${result.error}`));
553
+ }
554
+ }
555
+ await writeUIManifest(manifest, cwd);
556
+ previousUIApps = currentUIApps;
557
+ }, 150);
558
+ });
462
559
  let isCleaningUp = false;
463
560
  const cleanup = /* @__PURE__ */ __name(() => {
464
561
  if (isCleaningUp) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leanmcp/cli",
3
- "version": "0.2.14",
3
+ "version": "0.3.0",
4
4
  "description": "Command-line interface for scaffolding LeanMCP projects",
5
5
  "bin": {
6
6
  "leanmcp": "bin/leanmcp.js"