@leanmcp/cli 0.2.13 → 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.
package/bin/leanmcp.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  // Always prefer ESM build
4
4
  import('../dist/index.js');
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";
@@ -218,6 +242,17 @@ module.exports = {
218
242
  plugins: [],
219
243
  }
220
244
  `);
245
+ const tsconfigPath = path2.join(tempDir, "tsconfig.json");
246
+ await fs2.writeFile(tsconfigPath, JSON.stringify({
247
+ compilerOptions: {
248
+ target: "ES2020",
249
+ jsx: "react-jsx",
250
+ module: "ESNext",
251
+ moduleResolution: "bundler",
252
+ skipLibCheck: true,
253
+ esModuleInterop: true
254
+ }
255
+ }, null, 2));
221
256
  const stylesCss = path2.join(tempDir, "styles.css");
222
257
  await fs2.writeFile(stylesCss, `
223
258
  @tailwind base;
@@ -307,12 +342,23 @@ createRoot(document.getElementById('root')!).render(
307
342
  );
308
343
  `);
309
344
  try {
345
+ const reactPath = resolveReactDependency(projectDir, "react");
346
+ const reactDomPath = resolveReactDependency(projectDir, "react-dom");
310
347
  await vite.build({
311
348
  root: tempDir,
312
349
  plugins: [
313
350
  react(),
314
351
  viteSingleFile()
315
352
  ],
353
+ resolve: {
354
+ alias: {
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")
360
+ }
361
+ },
316
362
  css: {
317
363
  postcss: {
318
364
  plugins: [
@@ -366,8 +412,49 @@ async function writeUIManifest(manifest, projectDir) {
366
412
  });
367
413
  }
368
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");
369
423
 
370
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");
371
458
  async function devCommand() {
372
459
  const cwd = process.cwd();
373
460
  if (!await fs3.pathExists(path3.join(cwd, "main.ts"))) {
@@ -375,16 +462,13 @@ async function devCommand() {
375
462
  console.error(chalk.gray("Run this command from your project root."));
376
463
  process.exit(1);
377
464
  }
378
- console.log(chalk.cyan("\n\u{1F680} LeanMCP Development Server\n"));
465
+ console.log(chalk.cyan("\nLeanMCP Development Server\n"));
379
466
  const scanSpinner = ora("Scanning for @UIApp components...").start();
380
467
  const uiApps = await scanUIApp(cwd);
381
468
  if (uiApps.length === 0) {
382
- scanSpinner.succeed("No @UIApp components found");
469
+ scanSpinner.info("No @UIApp components found");
383
470
  } else {
384
- scanSpinner.succeed(`Found ${uiApps.length} @UIApp component(s)`);
385
- for (const app of uiApps) {
386
- console.log(chalk.gray(` \u2022 ${app.componentName} \u2192 ${app.resourceUri}`));
387
- }
471
+ scanSpinner.info(`Found ${uiApps.length} @UIApp component(s)`);
388
472
  }
389
473
  const manifest = {};
390
474
  if (uiApps.length > 0) {
@@ -402,10 +486,10 @@ async function devCommand() {
402
486
  if (errors.length > 0) {
403
487
  buildSpinner.warn("Built with warnings");
404
488
  for (const error of errors) {
405
- console.error(chalk.yellow(` \u26A0 ${error}`));
489
+ console.error(chalk.yellow(` ${error}`));
406
490
  }
407
491
  } else {
408
- buildSpinner.succeed("UI components built");
492
+ buildSpinner.info("UI components built");
409
493
  }
410
494
  }
411
495
  console.log(chalk.cyan("\nStarting development server...\n"));
@@ -419,31 +503,66 @@ async function devCommand() {
419
503
  shell: true
420
504
  });
421
505
  let watcher = null;
422
- if (uiApps.length > 0) {
423
- const componentPaths = uiApps.map((app) => app.componentPath);
424
- watcher = chokidar.watch(componentPaths, {
425
- ignoreInitial: true
426
- });
427
- watcher.on("change", async (changedPath) => {
428
- const app = uiApps.find((a) => a.componentPath === changedPath);
429
- if (!app) return;
430
- console.log(chalk.cyan(`
431
- [UI] Rebuilding ${app.componentName}...`));
432
- const result = await buildUIComponent(app, cwd, true);
433
- if (result.success) {
434
- manifest[app.resourceUri] = result.htmlPath;
435
- await writeUIManifest(manifest, cwd);
436
- console.log(chalk.green(`[UI] ${app.componentName} rebuilt successfully`));
437
- } else {
438
- console.log(chalk.yellow(`[UI] ${app.componentName} build failed: ${result.error}`));
439
- }
440
- });
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
+ }
441
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
+ });
559
+ let isCleaningUp = false;
442
560
  const cleanup = /* @__PURE__ */ __name(() => {
561
+ if (isCleaningUp) return;
562
+ isCleaningUp = true;
443
563
  console.log(chalk.gray("\nShutting down..."));
444
564
  if (watcher) watcher.close();
445
- devServer.kill();
446
- process.exit(0);
565
+ devServer.kill("SIGTERM");
447
566
  }, "cleanup");
448
567
  process.on("SIGINT", cleanup);
449
568
  process.on("SIGTERM", cleanup);
@@ -531,10 +650,12 @@ async function startCommand() {
531
650
  stdio: "inherit",
532
651
  shell: true
533
652
  });
653
+ let isCleaningUp = false;
534
654
  const cleanup = /* @__PURE__ */ __name(() => {
655
+ if (isCleaningUp) return;
656
+ isCleaningUp = true;
535
657
  console.log(chalk2.gray("\nShutting down..."));
536
- server.kill();
537
- process.exit(0);
658
+ server.kill("SIGTERM");
538
659
  }, "cleanup");
539
660
  process.on("SIGINT", cleanup);
540
661
  process.on("SIGTERM", cleanup);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leanmcp/cli",
3
- "version": "0.2.13",
3
+ "version": "0.3.0",
4
4
  "description": "Command-line interface for scaffolding LeanMCP projects",
5
5
  "bin": {
6
6
  "leanmcp": "bin/leanmcp.js"