@leanmcp/cli 0.4.5 → 0.5.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 +594 -47
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3,11 +3,11 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
3
3
 
4
4
  // src/index.ts
5
5
  import { Command } from "commander";
6
- import fs8 from "fs-extra";
7
- import path8 from "path";
8
- import ora7 from "ora";
6
+ import fs10 from "fs-extra";
7
+ import path10 from "path";
8
+ import ora8 from "ora";
9
9
  import { createRequire } from "module";
10
- import { confirm as confirm3 } from "@inquirer/prompts";
10
+ import { confirm as confirm4 } from "@inquirer/prompts";
11
11
  import { spawn as spawn4 } from "child_process";
12
12
 
13
13
  // src/commands/dev.ts
@@ -1261,8 +1261,8 @@ __name(whoamiCommand, "whoamiCommand");
1261
1261
 
1262
1262
  // src/commands/deploy.ts
1263
1263
  import ora5 from "ora";
1264
- import path7 from "path";
1265
- import fs7 from "fs-extra";
1264
+ import path8 from "path";
1265
+ import fs8 from "fs-extra";
1266
1266
  import os3 from "os";
1267
1267
  import archiver from "archiver";
1268
1268
  import { input as input2, confirm as confirm2, select } from "@inquirer/prompts";
@@ -1444,6 +1444,128 @@ function generateProjectName() {
1444
1444
  }
1445
1445
  __name(generateProjectName, "generateProjectName");
1446
1446
 
1447
+ // src/utils/env-parser.ts
1448
+ import fs7 from "fs-extra";
1449
+ import path7 from "path";
1450
+ var RESERVED_ENV_KEYS = [
1451
+ "AWS_REGION",
1452
+ "AWS_ACCESS_KEY_ID",
1453
+ "AWS_SECRET_ACCESS_KEY",
1454
+ "AWS_SESSION_TOKEN",
1455
+ "AWS_LAMBDA_FUNCTION_NAME",
1456
+ "AWS_LAMBDA_FUNCTION_MEMORY_SIZE",
1457
+ "AWS_LAMBDA_FUNCTION_VERSION",
1458
+ "AWS_LAMBDA_LOG_GROUP_NAME",
1459
+ "AWS_LAMBDA_LOG_STREAM_NAME",
1460
+ "_HANDLER",
1461
+ "_X_AMZN_TRACE_ID"
1462
+ ];
1463
+ var SYSTEM_ENV_KEYS = [
1464
+ "PORT",
1465
+ "AWS_LWA_PORT",
1466
+ "AWS_LWA_INVOKE_MODE",
1467
+ "AWS_LWA_READINESS_CHECK_MIN_UNHEALTHY_STATUS"
1468
+ ];
1469
+ function parseEnvVar(input3) {
1470
+ if (!input3 || typeof input3 !== "string") {
1471
+ return null;
1472
+ }
1473
+ const trimmed = input3.trim();
1474
+ const equalIndex = trimmed.indexOf("=");
1475
+ if (equalIndex === -1) {
1476
+ return null;
1477
+ }
1478
+ const key = trimmed.substring(0, equalIndex).trim();
1479
+ let value = trimmed.substring(equalIndex + 1);
1480
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1481
+ value = value.slice(1, -1);
1482
+ }
1483
+ if (!isValidEnvKey(key)) {
1484
+ return null;
1485
+ }
1486
+ return {
1487
+ key,
1488
+ value
1489
+ };
1490
+ }
1491
+ __name(parseEnvVar, "parseEnvVar");
1492
+ function parseEnvFile(content) {
1493
+ const result = {};
1494
+ if (!content || typeof content !== "string") {
1495
+ return result;
1496
+ }
1497
+ const lines = content.split(/\r?\n/);
1498
+ for (const line of lines) {
1499
+ const trimmed = line.trim();
1500
+ if (!trimmed || trimmed.startsWith("#")) {
1501
+ continue;
1502
+ }
1503
+ const parsed = parseEnvVar(trimmed);
1504
+ if (parsed) {
1505
+ result[parsed.key] = parsed.value;
1506
+ }
1507
+ }
1508
+ return result;
1509
+ }
1510
+ __name(parseEnvFile, "parseEnvFile");
1511
+ async function loadEnvFile(filePath) {
1512
+ const absolutePath = path7.resolve(filePath);
1513
+ if (!await fs7.pathExists(absolutePath)) {
1514
+ throw new Error(`Env file not found: ${absolutePath}`);
1515
+ }
1516
+ const content = await fs7.readFile(absolutePath, "utf-8");
1517
+ return parseEnvFile(content);
1518
+ }
1519
+ __name(loadEnvFile, "loadEnvFile");
1520
+ async function writeEnvFile(filePath, vars) {
1521
+ const absolutePath = path7.resolve(filePath);
1522
+ const lines = [
1523
+ "# Environment variables",
1524
+ `# Generated by leanmcp CLI at ${(/* @__PURE__ */ new Date()).toISOString()}`,
1525
+ ""
1526
+ ];
1527
+ const sortedKeys = Object.keys(vars).sort();
1528
+ for (const key of sortedKeys) {
1529
+ const value = vars[key];
1530
+ if (value.includes(" ") || value.includes("#") || value.includes('"')) {
1531
+ lines.push(`${key}="${value.replace(/"/g, '\\"')}"`);
1532
+ } else {
1533
+ lines.push(`${key}=${value}`);
1534
+ }
1535
+ }
1536
+ lines.push("");
1537
+ await fs7.writeFile(absolutePath, lines.join("\n"));
1538
+ }
1539
+ __name(writeEnvFile, "writeEnvFile");
1540
+ function isValidEnvKey(key) {
1541
+ if (!key || typeof key !== "string") {
1542
+ return false;
1543
+ }
1544
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
1545
+ }
1546
+ __name(isValidEnvKey, "isValidEnvKey");
1547
+ function isReservedKey(key) {
1548
+ return RESERVED_ENV_KEYS.includes(key);
1549
+ }
1550
+ __name(isReservedKey, "isReservedKey");
1551
+ function isSystemKey(key) {
1552
+ return SYSTEM_ENV_KEYS.includes(key);
1553
+ }
1554
+ __name(isSystemKey, "isSystemKey");
1555
+ function formatEnvVarsForDisplay(vars, reveal = false) {
1556
+ const keys = Object.keys(vars).sort();
1557
+ if (keys.length === 0) {
1558
+ return " (no environment variables)";
1559
+ }
1560
+ const lines = keys.map((key) => {
1561
+ const value = reveal ? vars[key] : "***";
1562
+ const keyType = isSystemKey(key) ? " (system)" : "";
1563
+ return ` ${key}=${value}${keyType}`;
1564
+ });
1565
+ return lines.join("\n");
1566
+ }
1567
+ __name(formatEnvVarsForDisplay, "formatEnvVarsForDisplay");
1568
+
1447
1569
  // src/commands/deploy.ts
1448
1570
  var DEBUG_MODE3 = false;
1449
1571
  function setDeployDebugMode(enabled) {
@@ -1490,10 +1612,10 @@ var API_ENDPOINTS = {
1490
1612
  var LEANMCP_CONFIG_DIR = ".leanmcp";
1491
1613
  var LEANMCP_CONFIG_FILE = "config.json";
1492
1614
  async function readLeanMCPConfig(projectPath) {
1493
- const configPath = path7.join(projectPath, LEANMCP_CONFIG_DIR, LEANMCP_CONFIG_FILE);
1615
+ const configPath = path8.join(projectPath, LEANMCP_CONFIG_DIR, LEANMCP_CONFIG_FILE);
1494
1616
  try {
1495
- if (await fs7.pathExists(configPath)) {
1496
- const config = await fs7.readJSON(configPath);
1617
+ if (await fs8.pathExists(configPath)) {
1618
+ const config = await fs8.readJSON(configPath);
1497
1619
  debug3("Found existing .leanmcp config:", config);
1498
1620
  return config;
1499
1621
  }
@@ -1504,10 +1626,10 @@ async function readLeanMCPConfig(projectPath) {
1504
1626
  }
1505
1627
  __name(readLeanMCPConfig, "readLeanMCPConfig");
1506
1628
  async function writeLeanMCPConfig(projectPath, config) {
1507
- const configDir = path7.join(projectPath, LEANMCP_CONFIG_DIR);
1508
- const configPath = path7.join(configDir, LEANMCP_CONFIG_FILE);
1509
- await fs7.ensureDir(configDir);
1510
- await fs7.writeJSON(configPath, config, {
1629
+ const configDir = path8.join(projectPath, LEANMCP_CONFIG_DIR);
1630
+ const configPath = path8.join(configDir, LEANMCP_CONFIG_FILE);
1631
+ await fs8.ensureDir(configDir);
1632
+ await fs8.writeJSON(configPath, config, {
1511
1633
  spaces: 2
1512
1634
  });
1513
1635
  debug3("Saved .leanmcp config:", config);
@@ -1515,7 +1637,7 @@ async function writeLeanMCPConfig(projectPath, config) {
1515
1637
  __name(writeLeanMCPConfig, "writeLeanMCPConfig");
1516
1638
  async function createZipArchive(folderPath, outputPath) {
1517
1639
  return new Promise((resolve, reject) => {
1518
- const output = fs7.createWriteStream(outputPath);
1640
+ const output = fs8.createWriteStream(outputPath);
1519
1641
  const archive = archiver("zip", {
1520
1642
  zlib: {
1521
1643
  level: 9
@@ -1613,13 +1735,13 @@ async function deployCommand(folderPath, options = {}) {
1613
1735
  }
1614
1736
  const apiUrl = await getApiUrl();
1615
1737
  debug3("API URL:", apiUrl);
1616
- const absolutePath = path7.resolve(process.cwd(), folderPath);
1617
- if (!await fs7.pathExists(absolutePath)) {
1738
+ const absolutePath = path8.resolve(process.cwd(), folderPath);
1739
+ if (!await fs8.pathExists(absolutePath)) {
1618
1740
  logger.error(`Folder not found: ${absolutePath}`);
1619
1741
  process.exit(1);
1620
1742
  }
1621
- const hasMainTs = await fs7.pathExists(path7.join(absolutePath, "main.ts"));
1622
- const hasPackageJson = await fs7.pathExists(path7.join(absolutePath, "package.json"));
1743
+ const hasMainTs = await fs8.pathExists(path8.join(absolutePath, "main.ts"));
1744
+ const hasPackageJson = await fs8.pathExists(path8.join(absolutePath, "package.json"));
1623
1745
  if (!hasMainTs && !hasPackageJson) {
1624
1746
  logger.error("Not a valid project folder.");
1625
1747
  logger.gray("Expected main.ts or package.json in the folder.\n");
@@ -1689,10 +1811,10 @@ Generated project name: ${chalk.bold(projectName)}
1689
1811
  } catch (e) {
1690
1812
  debug3("Could not fetch existing projects");
1691
1813
  }
1692
- let folderName = path7.basename(absolutePath);
1814
+ let folderName = path8.basename(absolutePath);
1693
1815
  if (hasPackageJson) {
1694
1816
  try {
1695
- const pkg2 = await fs7.readJSON(path7.join(absolutePath, "package.json"));
1817
+ const pkg2 = await fs8.readJSON(path8.join(absolutePath, "package.json"));
1696
1818
  folderName = pkg2.name || folderName;
1697
1819
  } catch (e) {
1698
1820
  }
@@ -1843,7 +1965,7 @@ ${error instanceof Error ? error.message : String(error)}`);
1843
1965
  }
1844
1966
  const uploadSpinner = ora5("Packaging and uploading...").start();
1845
1967
  try {
1846
- const tempZip = path7.join(os3.tmpdir(), `leanmcp-${Date.now()}.zip`);
1968
+ const tempZip = path8.join(os3.tmpdir(), `leanmcp-${Date.now()}.zip`);
1847
1969
  const zipSize = await createZipArchive(absolutePath, tempZip);
1848
1970
  uploadSpinner.text = `Packaging... (${Math.round(zipSize / 1024)}KB)`;
1849
1971
  debug3("Step 2a: Getting upload URL for project:", projectId);
@@ -1870,7 +1992,7 @@ ${error instanceof Error ? error.message : String(error)}`);
1870
1992
  throw new Error("Backend did not return upload URL");
1871
1993
  }
1872
1994
  debug3("Step 2b: Uploading to S3...");
1873
- const zipBuffer = await fs7.readFile(tempZip);
1995
+ const zipBuffer = await fs8.readFile(tempZip);
1874
1996
  const s3Response = await fetch(uploadUrl, {
1875
1997
  method: "PUT",
1876
1998
  body: zipBuffer,
@@ -1893,7 +2015,7 @@ ${error instanceof Error ? error.message : String(error)}`);
1893
2015
  s3Location
1894
2016
  })
1895
2017
  });
1896
- await fs7.remove(tempZip);
2018
+ await fs8.remove(tempZip);
1897
2019
  uploadSpinner.succeed("Project uploaded");
1898
2020
  } catch (error) {
1899
2021
  uploadSpinner.fail("Failed to upload");
@@ -2123,8 +2245,8 @@ async function projectsDeleteCommand(projectId, options = {}) {
2123
2245
  process.exit(1);
2124
2246
  }
2125
2247
  if (!options.force) {
2126
- const { confirm: confirm4 } = await import("@inquirer/prompts");
2127
- const shouldDelete = await confirm4({
2248
+ const { confirm: confirm5 } = await import("@inquirer/prompts");
2249
+ const shouldDelete = await confirm5({
2128
2250
  message: `Are you sure you want to delete project ${projectId}?`,
2129
2251
  default: false
2130
2252
  });
@@ -2159,6 +2281,383 @@ ${error instanceof Error ? error.message : String(error)}`);
2159
2281
  }
2160
2282
  __name(projectsDeleteCommand, "projectsDeleteCommand");
2161
2283
 
2284
+ // src/commands/env.ts
2285
+ import ora7 from "ora";
2286
+ import path9 from "path";
2287
+ import fs9 from "fs-extra";
2288
+ import { confirm as confirm3 } from "@inquirer/prompts";
2289
+ var DEBUG_MODE4 = false;
2290
+ function setEnvDebugMode(enabled) {
2291
+ DEBUG_MODE4 = enabled;
2292
+ }
2293
+ __name(setEnvDebugMode, "setEnvDebugMode");
2294
+ function debug4(message, ...args) {
2295
+ if (DEBUG_MODE4) {
2296
+ console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
2297
+ }
2298
+ }
2299
+ __name(debug4, "debug");
2300
+ async function debugFetch2(url, options = {}) {
2301
+ debug4(`HTTP ${options.method || "GET"} ${url}`);
2302
+ if (options.body && typeof options.body === "string") {
2303
+ try {
2304
+ const body = JSON.parse(options.body);
2305
+ debug4("Request body:", JSON.stringify(body, null, 2));
2306
+ } catch {
2307
+ debug4("Request body:", options.body);
2308
+ }
2309
+ }
2310
+ const startTime = Date.now();
2311
+ const response = await fetch(url, options);
2312
+ const duration = Date.now() - startTime;
2313
+ debug4(`Response: ${response.status} ${response.statusText} (${duration}ms)`);
2314
+ return response;
2315
+ }
2316
+ __name(debugFetch2, "debugFetch");
2317
+ var LEANMCP_CONFIG_DIR2 = ".leanmcp";
2318
+ var LEANMCP_CONFIG_FILE2 = "config.json";
2319
+ async function readLeanMCPConfig2(projectPath) {
2320
+ const configPath = path9.join(projectPath, LEANMCP_CONFIG_DIR2, LEANMCP_CONFIG_FILE2);
2321
+ try {
2322
+ if (await fs9.pathExists(configPath)) {
2323
+ const config = await fs9.readJSON(configPath);
2324
+ debug4("Found existing .leanmcp config:", config);
2325
+ return config;
2326
+ }
2327
+ } catch (e) {
2328
+ debug4("Could not read .leanmcp config:", e);
2329
+ }
2330
+ return null;
2331
+ }
2332
+ __name(readLeanMCPConfig2, "readLeanMCPConfig");
2333
+ async function getDeploymentContext(folderPath) {
2334
+ const apiKey = await getApiKey();
2335
+ if (!apiKey) {
2336
+ logger.error("Not logged in.");
2337
+ logger.gray("Run `leanmcp login` first to authenticate.\n");
2338
+ return null;
2339
+ }
2340
+ const apiUrl = await getApiUrl();
2341
+ const absolutePath = path9.resolve(process.cwd(), folderPath);
2342
+ const config = await readLeanMCPConfig2(absolutePath);
2343
+ if (!config) {
2344
+ logger.error("No deployment found.");
2345
+ logger.gray(`No .leanmcp/config.json found in ${absolutePath}`);
2346
+ logger.gray("Deploy first with: leanmcp deploy .\n");
2347
+ return null;
2348
+ }
2349
+ if (!config.deploymentId) {
2350
+ logger.error("Deployment ID not found in config.");
2351
+ logger.gray("Please redeploy with: leanmcp deploy .\n");
2352
+ return null;
2353
+ }
2354
+ return {
2355
+ apiKey,
2356
+ apiUrl,
2357
+ config
2358
+ };
2359
+ }
2360
+ __name(getDeploymentContext, "getDeploymentContext");
2361
+ async function envListCommand(folderPath, options = {}) {
2362
+ logger.info("\nLeanMCP Environment Variables\n");
2363
+ const context = await getDeploymentContext(folderPath);
2364
+ if (!context) {
2365
+ process.exit(1);
2366
+ }
2367
+ const { apiKey, apiUrl, config } = context;
2368
+ const spinner = ora7("Fetching environment variables...").start();
2369
+ try {
2370
+ const reveal = options.reveal ? "?reveal=true" : "";
2371
+ const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env${reveal}`, {
2372
+ headers: {
2373
+ "Authorization": `Bearer ${apiKey}`
2374
+ }
2375
+ });
2376
+ if (!response.ok) {
2377
+ const error = await response.text();
2378
+ throw new Error(`Failed to fetch env vars: ${error}`);
2379
+ }
2380
+ const envVars = await response.json();
2381
+ spinner.succeed("Environment variables retrieved");
2382
+ logger.info(`
2383
+ Project: ${config.projectName}`);
2384
+ logger.gray(`Deployment: ${config.deploymentId.substring(0, 8)}...`);
2385
+ logger.gray(`URL: ${config.url}
2386
+ `);
2387
+ const formatted = formatEnvVarsForDisplay(envVars, options.reveal);
2388
+ logger.log(formatted);
2389
+ logger.log("");
2390
+ if (!options.reveal) {
2391
+ logger.gray("Use --reveal to show actual values\n");
2392
+ }
2393
+ } catch (error) {
2394
+ spinner.fail("Failed to fetch environment variables");
2395
+ logger.error(`
2396
+ ${error instanceof Error ? error.message : String(error)}`);
2397
+ process.exit(1);
2398
+ }
2399
+ }
2400
+ __name(envListCommand, "envListCommand");
2401
+ async function envSetCommand(keyValue, folderPath, options = {}) {
2402
+ const context = await getDeploymentContext(folderPath);
2403
+ if (!context) {
2404
+ process.exit(1);
2405
+ }
2406
+ const { apiKey, apiUrl, config } = context;
2407
+ let variables = {};
2408
+ if (options.file) {
2409
+ const spinner2 = ora7(`Loading from ${options.file}...`).start();
2410
+ try {
2411
+ variables = await loadEnvFile(options.file);
2412
+ spinner2.succeed(`Loaded ${Object.keys(variables).length} variable(s) from ${options.file}`);
2413
+ } catch (error) {
2414
+ spinner2.fail(`Failed to load ${options.file}`);
2415
+ logger.error(`
2416
+ ${error instanceof Error ? error.message : String(error)}`);
2417
+ process.exit(1);
2418
+ }
2419
+ } else {
2420
+ const parsed = parseEnvVar(keyValue);
2421
+ if (!parsed) {
2422
+ logger.error("Invalid format. Expected: KEY=VALUE");
2423
+ logger.gray("Example: leanmcp env set API_KEY=secret123\n");
2424
+ process.exit(1);
2425
+ }
2426
+ if (isReservedKey(parsed.key)) {
2427
+ logger.error(`Cannot set reserved key: ${parsed.key}`);
2428
+ logger.gray("This key is managed by AWS Lambda.\n");
2429
+ process.exit(1);
2430
+ }
2431
+ if (isSystemKey(parsed.key) && !options.force) {
2432
+ logger.warn(`Warning: ${parsed.key} is a system key.`);
2433
+ const shouldContinue = await confirm3({
2434
+ message: "Are you sure you want to modify it?",
2435
+ default: false
2436
+ });
2437
+ if (!shouldContinue) {
2438
+ logger.gray("\nCancelled.\n");
2439
+ return;
2440
+ }
2441
+ }
2442
+ variables = {
2443
+ [parsed.key]: parsed.value
2444
+ };
2445
+ }
2446
+ if (Object.keys(variables).length === 0) {
2447
+ logger.warn("No variables to set.\n");
2448
+ return;
2449
+ }
2450
+ const spinner = ora7("Updating environment variables...").start();
2451
+ try {
2452
+ const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env`, {
2453
+ method: "PUT",
2454
+ headers: {
2455
+ "Authorization": `Bearer ${apiKey}`,
2456
+ "Content-Type": "application/json"
2457
+ },
2458
+ body: JSON.stringify({
2459
+ variables
2460
+ })
2461
+ });
2462
+ if (!response.ok) {
2463
+ const error = await response.text();
2464
+ throw new Error(`Failed to update env vars: ${error}`);
2465
+ }
2466
+ const result = await response.json();
2467
+ spinner.succeed("Environment variables updated");
2468
+ logger.info(`
2469
+ ${result.message || "Variables updated successfully"}`);
2470
+ logger.gray("Note: Lambda will cold-start on next invocation.\n");
2471
+ } catch (error) {
2472
+ spinner.fail("Failed to update environment variables");
2473
+ logger.error(`
2474
+ ${error instanceof Error ? error.message : String(error)}`);
2475
+ process.exit(1);
2476
+ }
2477
+ }
2478
+ __name(envSetCommand, "envSetCommand");
2479
+ async function envGetCommand(key, folderPath, options = {}) {
2480
+ const context = await getDeploymentContext(folderPath);
2481
+ if (!context) {
2482
+ process.exit(1);
2483
+ }
2484
+ const { apiKey, apiUrl, config } = context;
2485
+ try {
2486
+ const reveal = options.reveal ? "?reveal=true" : "";
2487
+ const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env${reveal}`, {
2488
+ headers: {
2489
+ "Authorization": `Bearer ${apiKey}`
2490
+ }
2491
+ });
2492
+ if (!response.ok) {
2493
+ const error = await response.text();
2494
+ throw new Error(`Failed to fetch env vars: ${error}`);
2495
+ }
2496
+ const envVars = await response.json();
2497
+ if (key in envVars) {
2498
+ const value = envVars[key];
2499
+ logger.log(`${key}=${value}`);
2500
+ } else {
2501
+ logger.warn(`Variable '${key}' not found.`);
2502
+ process.exit(1);
2503
+ }
2504
+ } catch (error) {
2505
+ logger.error(`
2506
+ ${error instanceof Error ? error.message : String(error)}`);
2507
+ process.exit(1);
2508
+ }
2509
+ }
2510
+ __name(envGetCommand, "envGetCommand");
2511
+ async function envRemoveCommand(key, folderPath, options = {}) {
2512
+ const context = await getDeploymentContext(folderPath);
2513
+ if (!context) {
2514
+ process.exit(1);
2515
+ }
2516
+ const { apiKey, apiUrl, config } = context;
2517
+ if (isReservedKey(key)) {
2518
+ logger.error(`Cannot remove reserved key: ${key}`);
2519
+ process.exit(1);
2520
+ }
2521
+ if (isSystemKey(key)) {
2522
+ logger.error(`Cannot remove system key: ${key}`);
2523
+ process.exit(1);
2524
+ }
2525
+ if (!options.force) {
2526
+ const shouldDelete = await confirm3({
2527
+ message: `Remove environment variable '${key}'?`,
2528
+ default: false
2529
+ });
2530
+ if (!shouldDelete) {
2531
+ logger.gray("\nCancelled.\n");
2532
+ return;
2533
+ }
2534
+ }
2535
+ const spinner = ora7(`Removing ${key}...`).start();
2536
+ try {
2537
+ const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env/${key}`, {
2538
+ method: "DELETE",
2539
+ headers: {
2540
+ "Authorization": `Bearer ${apiKey}`
2541
+ }
2542
+ });
2543
+ if (!response.ok) {
2544
+ const error = await response.text();
2545
+ throw new Error(`Failed to remove env var: ${error}`);
2546
+ }
2547
+ spinner.succeed(`Removed ${key}`);
2548
+ logger.gray("Note: Lambda will cold-start on next invocation.\n");
2549
+ } catch (error) {
2550
+ spinner.fail(`Failed to remove ${key}`);
2551
+ logger.error(`
2552
+ ${error instanceof Error ? error.message : String(error)}`);
2553
+ process.exit(1);
2554
+ }
2555
+ }
2556
+ __name(envRemoveCommand, "envRemoveCommand");
2557
+ async function envPullCommand(folderPath, options = {}) {
2558
+ const context = await getDeploymentContext(folderPath);
2559
+ if (!context) {
2560
+ process.exit(1);
2561
+ }
2562
+ const { apiKey, apiUrl, config } = context;
2563
+ const outputFile = options.file || ".env.remote";
2564
+ const spinner = ora7("Fetching environment variables...").start();
2565
+ try {
2566
+ const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env?reveal=true`, {
2567
+ headers: {
2568
+ "Authorization": `Bearer ${apiKey}`
2569
+ }
2570
+ });
2571
+ if (!response.ok) {
2572
+ const error = await response.text();
2573
+ throw new Error(`Failed to fetch env vars: ${error}`);
2574
+ }
2575
+ const envVars = await response.json();
2576
+ const userVars = {};
2577
+ for (const [key, value] of Object.entries(envVars)) {
2578
+ if (!isSystemKey(key)) {
2579
+ userVars[key] = value;
2580
+ }
2581
+ }
2582
+ spinner.text = `Writing to ${outputFile}...`;
2583
+ await writeEnvFile(outputFile, userVars);
2584
+ spinner.succeed(`Saved ${Object.keys(userVars).length} variable(s) to ${outputFile}`);
2585
+ logger.gray(`
2586
+ System variables (PORT, AWS_LWA_*) are not included.
2587
+ `);
2588
+ } catch (error) {
2589
+ spinner.fail("Failed to pull environment variables");
2590
+ logger.error(`
2591
+ ${error instanceof Error ? error.message : String(error)}`);
2592
+ process.exit(1);
2593
+ }
2594
+ }
2595
+ __name(envPullCommand, "envPullCommand");
2596
+ async function envPushCommand(folderPath, options = {}) {
2597
+ const context = await getDeploymentContext(folderPath);
2598
+ if (!context) {
2599
+ process.exit(1);
2600
+ }
2601
+ const { apiKey, apiUrl, config } = context;
2602
+ const inputFile = options.file || ".env";
2603
+ const loadSpinner = ora7(`Loading from ${inputFile}...`).start();
2604
+ let variables;
2605
+ try {
2606
+ variables = await loadEnvFile(inputFile);
2607
+ loadSpinner.succeed(`Loaded ${Object.keys(variables).length} variable(s) from ${inputFile}`);
2608
+ } catch (error) {
2609
+ loadSpinner.fail(`Failed to load ${inputFile}`);
2610
+ logger.error(`
2611
+ ${error instanceof Error ? error.message : String(error)}`);
2612
+ process.exit(1);
2613
+ }
2614
+ if (Object.keys(variables).length === 0) {
2615
+ logger.warn("No variables found in file.\n");
2616
+ return;
2617
+ }
2618
+ logger.warn("\nThis will REPLACE ALL current environment variables.");
2619
+ logger.gray("System variables (PORT, AWS_LWA_*) will be preserved.\n");
2620
+ if (!options.force) {
2621
+ const shouldPush = await confirm3({
2622
+ message: `Replace all env vars with ${Object.keys(variables).length} variables from ${inputFile}?`,
2623
+ default: false
2624
+ });
2625
+ if (!shouldPush) {
2626
+ logger.gray("\nCancelled.\n");
2627
+ return;
2628
+ }
2629
+ }
2630
+ const pushSpinner = ora7("Pushing environment variables...").start();
2631
+ try {
2632
+ const response = await debugFetch2(`${apiUrl}/api/lambda-deploy/${config.deploymentId}/env`, {
2633
+ method: "PUT",
2634
+ headers: {
2635
+ "Authorization": `Bearer ${apiKey}`,
2636
+ "Content-Type": "application/json"
2637
+ },
2638
+ body: JSON.stringify({
2639
+ variables,
2640
+ replaceAll: true
2641
+ })
2642
+ });
2643
+ if (!response.ok) {
2644
+ const error = await response.text();
2645
+ throw new Error(`Failed to push env vars: ${error}`);
2646
+ }
2647
+ const result = await response.json();
2648
+ pushSpinner.succeed("Environment variables pushed");
2649
+ logger.info(`
2650
+ ${result.message || "Variables updated successfully"}`);
2651
+ logger.gray("Note: Lambda will cold-start on next invocation.\n");
2652
+ } catch (error) {
2653
+ pushSpinner.fail("Failed to push environment variables");
2654
+ logger.error(`
2655
+ ${error instanceof Error ? error.message : String(error)}`);
2656
+ process.exit(1);
2657
+ }
2658
+ }
2659
+ __name(envPushCommand, "envPushCommand");
2660
+
2162
2661
  // src/templates/readme_v1.ts
2163
2662
  var getReadmeTemplate = /* @__PURE__ */ __name((projectName) => `<p align="center">
2164
2663
  <img
@@ -2675,6 +3174,7 @@ function enableDebugIfNeeded() {
2675
3174
  setDebugMode(true);
2676
3175
  setDebugMode2(true);
2677
3176
  setDeployDebugMode(true);
3177
+ setEnvDebugMode(true);
2678
3178
  debug("Debug mode enabled globally");
2679
3179
  }
2680
3180
  }
@@ -2692,6 +3192,8 @@ Examples:
2692
3192
  $ leanmcp deploy ./my-app # Deploy to LeanMCP cloud
2693
3193
  $ leanmcp projects list # List your cloud projects
2694
3194
  $ leanmcp projects delete <id> # Delete a cloud project
3195
+ $ leanmcp env list # List environment variables
3196
+ $ leanmcp env set KEY=VALUE # Set an environment variable
2695
3197
 
2696
3198
  Global Options:
2697
3199
  -v, --version Output the current version
@@ -2703,14 +3205,14 @@ program.command("create <projectName>").description("Create a new LeanMCP projec
2703
3205
  projectName,
2704
3206
  ...options
2705
3207
  });
2706
- const spinner = ora7(`Creating project ${projectName}...`).start();
2707
- const targetDir = path8.join(process.cwd(), projectName);
2708
- if (fs8.existsSync(targetDir)) {
3208
+ const spinner = ora8(`Creating project ${projectName}...`).start();
3209
+ const targetDir = path10.join(process.cwd(), projectName);
3210
+ if (fs10.existsSync(targetDir)) {
2709
3211
  spinner.fail(`Folder ${projectName} already exists.`);
2710
3212
  process.exit(1);
2711
3213
  }
2712
- await fs8.mkdirp(targetDir);
2713
- await fs8.mkdirp(path8.join(targetDir, "mcp", "example"));
3214
+ await fs10.mkdirp(targetDir);
3215
+ await fs10.mkdirp(path10.join(targetDir, "mcp", "example"));
2714
3216
  const pkg2 = {
2715
3217
  name: projectName,
2716
3218
  version: "1.0.0",
@@ -2746,7 +3248,7 @@ program.command("create <projectName>").description("Create a new LeanMCP projec
2746
3248
  "typescript": "^5.6.3"
2747
3249
  }
2748
3250
  };
2749
- await fs8.writeJSON(path8.join(targetDir, "package.json"), pkg2, {
3251
+ await fs10.writeJSON(path10.join(targetDir, "package.json"), pkg2, {
2750
3252
  spaces: 2
2751
3253
  });
2752
3254
  const tsconfig = {
@@ -2769,15 +3271,15 @@ program.command("create <projectName>").description("Create a new LeanMCP projec
2769
3271
  "dist"
2770
3272
  ]
2771
3273
  };
2772
- await fs8.writeJSON(path8.join(targetDir, "tsconfig.json"), tsconfig, {
3274
+ await fs10.writeJSON(path10.join(targetDir, "tsconfig.json"), tsconfig, {
2773
3275
  spaces: 2
2774
3276
  });
2775
3277
  const dashboardLine = options.dashboard === false ? `
2776
3278
  dashboard: false, // Dashboard disabled via --no-dashboard` : "";
2777
3279
  const mainTs = getMainTsTemplate(projectName, dashboardLine);
2778
- await fs8.writeFile(path8.join(targetDir, "main.ts"), mainTs);
3280
+ await fs10.writeFile(path10.join(targetDir, "main.ts"), mainTs);
2779
3281
  const exampleServiceTs = getExampleServiceTemplate(projectName);
2780
- await fs8.writeFile(path8.join(targetDir, "mcp", "example", "index.ts"), exampleServiceTs);
3282
+ await fs10.writeFile(path10.join(targetDir, "mcp", "example", "index.ts"), exampleServiceTs);
2781
3283
  const gitignore = gitignoreTemplate;
2782
3284
  const env = `# Server Configuration
2783
3285
  PORT=3001
@@ -2785,10 +3287,10 @@ NODE_ENV=development
2785
3287
 
2786
3288
  # Add your environment variables here
2787
3289
  `;
2788
- await fs8.writeFile(path8.join(targetDir, ".gitignore"), gitignore);
2789
- await fs8.writeFile(path8.join(targetDir, ".env"), env);
3290
+ await fs10.writeFile(path10.join(targetDir, ".gitignore"), gitignore);
3291
+ await fs10.writeFile(path10.join(targetDir, ".env"), env);
2790
3292
  const readme = getReadmeTemplate(projectName);
2791
- await fs8.writeFile(path8.join(targetDir, "README.md"), readme);
3293
+ await fs10.writeFile(path10.join(targetDir, "README.md"), readme);
2792
3294
  spinner.succeed(`Project ${projectName} created!`);
2793
3295
  logger.log("\nSuccess! Your MCP server is ready.\n", chalk.green);
2794
3296
  logger.log("To deploy to LeanMCP cloud:", chalk.cyan);
@@ -2809,12 +3311,12 @@ NODE_ENV=development
2809
3311
  logger.log(` leanmcp deploy .`, chalk.gray);
2810
3312
  return;
2811
3313
  }
2812
- const shouldInstall = isNonInteractive ? true : await confirm3({
3314
+ const shouldInstall = isNonInteractive ? true : await confirm4({
2813
3315
  message: "Would you like to install dependencies now?",
2814
3316
  default: true
2815
3317
  });
2816
3318
  if (shouldInstall) {
2817
- const installSpinner = ora7("Installing dependencies...").start();
3319
+ const installSpinner = ora8("Installing dependencies...").start();
2818
3320
  try {
2819
3321
  await new Promise((resolve, reject) => {
2820
3322
  const npmInstall = spawn4("npm", [
@@ -2844,7 +3346,7 @@ NODE_ENV=development
2844
3346
  logger.log(` leanmcp deploy .`, chalk.gray);
2845
3347
  return;
2846
3348
  }
2847
- const shouldStartDev = options.allowAll ? true : await confirm3({
3349
+ const shouldStartDev = options.allowAll ? true : await confirm4({
2848
3350
  message: "Would you like to start the development server?",
2849
3351
  default: true
2850
3352
  });
@@ -2891,20 +3393,20 @@ NODE_ENV=development
2891
3393
  });
2892
3394
  program.command("add <serviceName>").description("Add a new MCP service to your project").action(async (serviceName) => {
2893
3395
  const cwd = process.cwd();
2894
- const mcpDir = path8.join(cwd, "mcp");
2895
- if (!fs8.existsSync(path8.join(cwd, "main.ts"))) {
3396
+ const mcpDir = path10.join(cwd, "mcp");
3397
+ if (!fs10.existsSync(path10.join(cwd, "main.ts"))) {
2896
3398
  logger.log("ERROR: Not a LeanMCP project (main.ts missing).", chalk.red);
2897
3399
  process.exit(1);
2898
3400
  }
2899
- const serviceDir = path8.join(mcpDir, serviceName);
2900
- const serviceFile = path8.join(serviceDir, "index.ts");
2901
- if (fs8.existsSync(serviceDir)) {
3401
+ const serviceDir = path10.join(mcpDir, serviceName);
3402
+ const serviceFile = path10.join(serviceDir, "index.ts");
3403
+ if (fs10.existsSync(serviceDir)) {
2902
3404
  logger.log(`ERROR: Service ${serviceName} already exists.`, chalk.red);
2903
3405
  process.exit(1);
2904
3406
  }
2905
- await fs8.mkdirp(serviceDir);
3407
+ await fs10.mkdirp(serviceDir);
2906
3408
  const indexTs = getServiceIndexTemplate(serviceName, capitalize(serviceName));
2907
- await fs8.writeFile(serviceFile, indexTs);
3409
+ await fs10.writeFile(serviceFile, indexTs);
2908
3410
  logger.log(`\\nCreated new service: ${chalk.bold(serviceName)}`, chalk.green);
2909
3411
  logger.log(` File: mcp/${serviceName}/index.ts`, chalk.gray);
2910
3412
  logger.log(` Tool: greet`, chalk.gray);
@@ -2966,4 +3468,49 @@ projectsCmd.command("delete <projectId>").alias("rm").description("Delete a proj
2966
3468
  });
2967
3469
  projectsDeleteCommand(projectId, options);
2968
3470
  });
3471
+ var envCmd = program.command("env").description("Manage environment variables for deployed projects");
3472
+ envCmd.command("list [folder]").alias("ls").description("List all environment variables").option("--reveal", "Show actual values instead of masked").option("--project-id <id>", "Specify project ID").action((folder, options) => {
3473
+ trackCommand("env_list", {
3474
+ folder,
3475
+ ...options
3476
+ });
3477
+ envListCommand(folder || ".", options);
3478
+ });
3479
+ envCmd.command("set <keyValue> [folder]").description("Set an environment variable (KEY=VALUE)").option("-f, --file <file>", "Load from env file").option("--force", "Skip confirmation for reserved keys").action((keyValue, folder, options) => {
3480
+ trackCommand("env_set", {
3481
+ folder,
3482
+ ...options
3483
+ });
3484
+ envSetCommand(keyValue, folder || ".", options);
3485
+ });
3486
+ envCmd.command("get <key> [folder]").description("Get an environment variable value").option("--reveal", "Show actual value").action((key, folder, options) => {
3487
+ trackCommand("env_get", {
3488
+ key,
3489
+ folder,
3490
+ ...options
3491
+ });
3492
+ envGetCommand(key, folder || ".", options);
3493
+ });
3494
+ envCmd.command("remove <key> [folder]").alias("rm").description("Remove an environment variable").option("--force", "Skip confirmation").action((key, folder, options) => {
3495
+ trackCommand("env_remove", {
3496
+ key,
3497
+ folder,
3498
+ ...options
3499
+ });
3500
+ envRemoveCommand(key, folder || ".", options);
3501
+ });
3502
+ envCmd.command("pull [folder]").description("Download environment variables to local .env file").option("-f, --file <file>", "Output file", ".env.remote").action((folder, options) => {
3503
+ trackCommand("env_pull", {
3504
+ folder,
3505
+ ...options
3506
+ });
3507
+ envPullCommand(folder || ".", options);
3508
+ });
3509
+ envCmd.command("push [folder]").description("Upload environment variables from local .env file (replaces all)").option("-f, --file <file>", "Input file", ".env").option("--force", "Skip confirmation").action((folder, options) => {
3510
+ trackCommand("env_push", {
3511
+ folder,
3512
+ ...options
3513
+ });
3514
+ envPushCommand(folder || ".", options);
3515
+ });
2969
3516
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leanmcp/cli",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "description": "Command-line interface for scaffolding LeanMCP projects",
5
5
  "bin": {
6
6
  "leanmcp": "bin/leanmcp.js"
@@ -70,4 +70,4 @@
70
70
  "publishConfig": {
71
71
  "access": "public"
72
72
  }
73
- }
73
+ }