@open-mercato/cli 0.5.1-develop.3032.01699048cb → 0.5.1-develop.3043.1a796c3920

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 (31) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/agentic/shared/AGENTS.md.template +1 -1
  3. package/dist/lib/__integration__/TC-INT-007.spec.js +201 -0
  4. package/dist/lib/__integration__/TC-INT-007.spec.js.map +7 -0
  5. package/dist/lib/dev-env-reload.js +89 -0
  6. package/dist/lib/dev-env-reload.js.map +7 -0
  7. package/dist/lib/generators/extensions/ai-agents.js +218 -0
  8. package/dist/lib/generators/extensions/ai-agents.js.map +7 -0
  9. package/dist/lib/generators/extensions/ai-tools.js +56 -1
  10. package/dist/lib/generators/extensions/ai-tools.js.map +2 -2
  11. package/dist/lib/generators/extensions/index.js +2 -0
  12. package/dist/lib/generators/extensions/index.js.map +2 -2
  13. package/dist/lib/testing/integration-discovery.js +102 -5
  14. package/dist/lib/testing/integration-discovery.js.map +2 -2
  15. package/dist/mercato.js +153 -79
  16. package/dist/mercato.js.map +2 -2
  17. package/package.json +5 -5
  18. package/src/__tests__/mercato.test.ts +301 -25
  19. package/src/lib/__integration__/TC-INT-007.spec.ts +228 -0
  20. package/src/lib/__tests__/dev-env-reload.test.ts +62 -0
  21. package/src/lib/dev-env-reload.ts +110 -0
  22. package/src/lib/generators/__tests__/module-subset.test.ts +14 -0
  23. package/src/lib/generators/__tests__/output-snapshots.test.ts +17 -0
  24. package/src/lib/generators/__tests__/scanner.test.ts +1 -1
  25. package/src/lib/generators/__tests__/structural-contracts.test.ts +26 -0
  26. package/src/lib/generators/extensions/ai-agents.ts +240 -0
  27. package/src/lib/generators/extensions/ai-tools.ts +72 -1
  28. package/src/lib/generators/extensions/index.ts +2 -0
  29. package/src/lib/testing/__tests__/integration-discovery.test.ts +68 -0
  30. package/src/lib/testing/integration-discovery.ts +127 -3
  31. package/src/mercato.ts +190 -83
package/dist/mercato.js CHANGED
@@ -7,10 +7,12 @@ import { resolveInitDerivedSecrets } from "./lib/init-secrets.js";
7
7
  import { parseModuleInstallArgs } from "./lib/module-install-args.js";
8
8
  import { resolveNextBuildIdCandidate } from "./lib/next-build-id.js";
9
9
  import { acquireServerStartLock } from "./lib/server-start-lock.js";
10
+ import { createDevEnvReloader, watchDevEnvFiles } from "./lib/dev-env-reload.js";
10
11
  const lazyIntegration = () => import("./lib/testing/integration.js");
11
12
  import path from "node:path";
12
13
  import fs from "node:fs";
13
14
  let envLoaded = false;
15
+ const initialProcessEnvironmentEntries = Object.entries(process.env);
14
16
  async function runWithCapturedExitCode(action) {
15
17
  const previousExitCode = process.exitCode;
16
18
  process.exitCode = void 0;
@@ -248,6 +250,43 @@ function formatManagedProcessExitStatus(result) {
248
250
  function createManagedProcessExitError(result) {
249
251
  return new Error(`[server] ${result.label} exited unexpectedly with ${formatManagedProcessExitStatus(result)}.`);
250
252
  }
253
+ function isDevServerRestartResult(result) {
254
+ return "restart" in result && result.restart === true;
255
+ }
256
+ function formatQueueWorkerLabel(queueNames) {
257
+ if (queueNames.length === 0) return "Queue worker";
258
+ const sorted = [...queueNames].sort((a, b) => a.localeCompare(b));
259
+ const preview = sorted.length > 4 ? `${sorted.slice(0, 4).join(", ")}, +${sorted.length - 4} more` : sorted.join(", ");
260
+ return `Queue worker (${preview})`;
261
+ }
262
+ function lookupModuleCommand(allModules, moduleName, commandName) {
263
+ const mod = allModules.find((entry) => entry.id === moduleName);
264
+ if (!mod) {
265
+ return { status: "missing-module" };
266
+ }
267
+ if (!mod.cli || mod.cli.length === 0) {
268
+ return { status: "missing-cli" };
269
+ }
270
+ const command = mod.cli.find((entry) => entry.command === commandName);
271
+ if (!command) {
272
+ return { status: "missing-command" };
273
+ }
274
+ return {
275
+ status: "ok",
276
+ module: mod,
277
+ command
278
+ };
279
+ }
280
+ function describeMissingModuleCommand(result) {
281
+ switch (result.status) {
282
+ case "missing-module":
283
+ return "module not enabled";
284
+ case "missing-cli":
285
+ return "module has no CLI commands";
286
+ case "missing-command":
287
+ return "command not found";
288
+ }
289
+ }
251
290
  function ensureNextBuildIdInConfiguredDistDir(appDir) {
252
291
  const configuredDistDir = path.join(appDir, ".mercato", "next");
253
292
  const configuredBuildIdPath = path.join(configuredDistDir, "BUILD_ID");
@@ -304,36 +343,24 @@ async function handleDirectEjectCommand(args) {
304
343
  return 0;
305
344
  }
306
345
  async function runModuleCommand(allModules, moduleName, commandName, args = [], options = {}) {
307
- const mod = allModules.find((m) => m.id === moduleName);
308
- if (!mod) {
309
- if (options.optional) {
310
- if (!options.silentOptional) {
311
- console.log(`\u23ED\uFE0F Skipping "${moduleName}:${commandName}" \u2014 module not enabled`);
312
- }
313
- return false;
314
- }
315
- throw new Error(`Module not found: "${moduleName}"`);
316
- }
317
- if (!mod.cli || mod.cli.length === 0) {
346
+ const resolved = lookupModuleCommand(allModules, moduleName, commandName);
347
+ if (resolved.status !== "ok") {
318
348
  if (options.optional) {
319
349
  if (!options.silentOptional) {
320
- console.log(`\u23ED\uFE0F Skipping "${moduleName}:${commandName}" \u2014 module has no CLI commands`);
350
+ console.log(`\u23ED\uFE0F Skipping "${moduleName}:${commandName}" \u2014 ${describeMissingModuleCommand(resolved)}`);
321
351
  }
322
352
  return false;
323
353
  }
324
- throw new Error(`Module "${moduleName}" has no CLI commands`);
325
- }
326
- const cmd = mod.cli.find((c) => c.command === commandName);
327
- if (!cmd) {
328
- if (options.optional) {
329
- if (!options.silentOptional) {
330
- console.log(`\u23ED\uFE0F Skipping "${moduleName}:${commandName}" \u2014 command not found`);
331
- }
332
- return false;
354
+ switch (resolved.status) {
355
+ case "missing-module":
356
+ throw new Error(`Module not found: "${moduleName}"`);
357
+ case "missing-cli":
358
+ throw new Error(`Module "${moduleName}" has no CLI commands`);
359
+ case "missing-command":
360
+ throw new Error(`Command "${commandName}" not found in module "${moduleName}"`);
333
361
  }
334
- throw new Error(`Command "${commandName}" not found in module "${moduleName}"`);
335
362
  }
336
- await cmd.run(args);
363
+ await resolved.command.run(args);
337
364
  return true;
338
365
  }
339
366
  async function runPostGenerateStructuralCachePurge(quiet) {
@@ -1351,12 +1378,11 @@ async function run(argv = process.argv) {
1351
1378
  const env = resolveEnvironment();
1352
1379
  const appDir = env.appDir;
1353
1380
  const nodeModulesBases = Array.from(/* @__PURE__ */ new Set([env.rootDir, appDir]));
1354
- const processes = [];
1355
- const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== "false";
1356
- const autoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER !== "false";
1357
- const queueStrategy = process.env.QUEUE_STRATEGY || "local";
1358
- const runtimeEnv = buildServerProcessEnvironment(process.env);
1381
+ let processes = [];
1359
1382
  let didRetryCorruptedTurbopackCache = false;
1383
+ let stopping = false;
1384
+ let envChangePromiseResolve = null;
1385
+ const envReloader = createDevEnvReloader(appDir, process.env, initialProcessEnvironmentEntries);
1360
1386
  function cleanup() {
1361
1387
  console.log("[server] Shutting down...");
1362
1388
  for (const proc of processes) {
@@ -1370,7 +1396,7 @@ async function run(argv = process.argv) {
1370
1396
  await Promise.all(
1371
1397
  processes.map(
1372
1398
  (proc) => new Promise((resolve) => {
1373
- if (proc.exitCode !== null) return resolve();
1399
+ if (proc.exitCode !== null || proc.signalCode !== null) return resolve();
1374
1400
  proc.on("exit", () => resolve());
1375
1401
  })
1376
1402
  )
@@ -1380,16 +1406,33 @@ async function run(argv = process.argv) {
1380
1406
  fs.unlinkSync(lockFile);
1381
1407
  } catch {
1382
1408
  }
1409
+ processes = [];
1383
1410
  }
1384
- process.on("SIGTERM", cleanup);
1385
- process.on("SIGINT", cleanup);
1411
+ process.on("SIGTERM", () => {
1412
+ stopping = true;
1413
+ cleanup();
1414
+ });
1415
+ process.on("SIGINT", () => {
1416
+ stopping = true;
1417
+ cleanup();
1418
+ });
1386
1419
  console.log("[server] Starting Open Mercato in dev mode...");
1387
1420
  const { createResolver: createResolverForSources } = await import("./lib/resolver.js");
1388
1421
  const { generateModulePackageSources } = await import("./lib/generators/index.js");
1389
1422
  await generateModulePackageSources({ resolver: createResolverForSources(), quiet: true });
1390
1423
  const nextBin = resolveInstalledBinary(nodeModulesBases, "next/dist/bin/next");
1391
1424
  const mercatoBin = resolveInstalledBinary(nodeModulesBases, "@open-mercato/cli/bin/mercato");
1392
- const startNextDev = () => new Promise((resolve) => {
1425
+ const stopEnvWatcher = watchDevEnvFiles(appDir, (filePath) => {
1426
+ envChangePromiseResolve?.({
1427
+ label: "Environment file change",
1428
+ restart: true,
1429
+ filePath
1430
+ });
1431
+ });
1432
+ const waitForEnvChange = () => new Promise((resolve) => {
1433
+ envChangePromiseResolve = resolve;
1434
+ });
1435
+ const startNextDev = (runtimeEnv) => new Promise((resolve) => {
1393
1436
  const nextProcess = spawn("node", [nextBin, "dev", "--turbopack"], {
1394
1437
  stdio: ["inherit", "pipe", "pipe"],
1395
1438
  env: runtimeEnv,
@@ -1418,7 +1461,7 @@ async function run(argv = process.argv) {
1418
1461
  didRetryCorruptedTurbopackCache = true;
1419
1462
  console.log("[server] Detected corrupted Turbopack dev cache. Clearing .mercato/next/dev and restarting Next.js once...");
1420
1463
  removeTurbopackDevCache(appDir);
1421
- return resolve(await startNextDev());
1464
+ return resolve(await startNextDev(runtimeEnv));
1422
1465
  }
1423
1466
  resolve({
1424
1467
  label: "Next.js dev server",
@@ -1427,37 +1470,61 @@ async function run(argv = process.argv) {
1427
1470
  });
1428
1471
  });
1429
1472
  });
1430
- const nextExitPromise = startNextDev();
1431
- const managedExitPromises = [nextExitPromise];
1432
- if (autoSpawnWorkers) {
1433
- const discoveredWorkerQueues = [...new Set(getRegisteredCliWorkers().map((worker) => worker.queue))];
1434
- if (discoveredWorkerQueues.length === 0) {
1435
- console.error("[server] AUTO_SPAWN_WORKERS is enabled, but no queues were discovered from CLI modules. Run `yarn generate` and verify `.mercato/generated/modules.cli.generated.ts` contains worker entries. Continuing without auto-spawned workers.");
1436
- } else {
1437
- console.log("[server] Starting workers for all queues...");
1438
- const workerProcess = spawn("node", [mercatoBin, "queue", "worker", "--all"], {
1439
- stdio: "inherit",
1440
- env: runtimeEnv,
1441
- cwd: appDir
1442
- });
1443
- processes.push(workerProcess);
1444
- managedExitPromises.push(waitForManagedProcessExit(workerProcess, "Queue worker"));
1473
+ try {
1474
+ while (!stopping) {
1475
+ envReloader.reload();
1476
+ const runtimeEnv = buildServerProcessEnvironment(process.env);
1477
+ const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== "false";
1478
+ const autoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER !== "false";
1479
+ const queueStrategy = process.env.QUEUE_STRATEGY || "local";
1480
+ const schedulerCommand = lookupModuleCommand(getCliModules(), "scheduler", "start");
1481
+ const managedExitPromises = [
1482
+ startNextDev(runtimeEnv),
1483
+ waitForEnvChange()
1484
+ ];
1485
+ if (autoSpawnWorkers) {
1486
+ const discoveredWorkerQueues = [...new Set(getRegisteredCliWorkers().map((worker) => worker.queue))];
1487
+ if (discoveredWorkerQueues.length === 0) {
1488
+ console.error("[server] AUTO_SPAWN_WORKERS is enabled, but no queues were discovered from CLI modules. Run `yarn generate` and verify `.mercato/generated/modules.cli.generated.ts` contains worker entries. Continuing without auto-spawned workers.");
1489
+ } else {
1490
+ console.log("[server] Starting workers for all queues...");
1491
+ const workerProcess = spawn("node", [mercatoBin, "queue", "worker", "--all"], {
1492
+ stdio: "inherit",
1493
+ env: runtimeEnv,
1494
+ cwd: appDir
1495
+ });
1496
+ processes.push(workerProcess);
1497
+ managedExitPromises.push(waitForManagedProcessExit(workerProcess, formatQueueWorkerLabel(discoveredWorkerQueues)));
1498
+ }
1499
+ }
1500
+ if (autoSpawnScheduler && queueStrategy === "local") {
1501
+ if (schedulerCommand.status !== "ok") {
1502
+ console.log(`[server] Skipping scheduler auto-start \u2014 ${describeMissingModuleCommand(schedulerCommand)}`);
1503
+ } else {
1504
+ console.log("[server] Starting scheduler polling engine...");
1505
+ const schedulerProcess = spawn("node", [mercatoBin, "scheduler", "start"], {
1506
+ stdio: "inherit",
1507
+ env: runtimeEnv,
1508
+ cwd: appDir
1509
+ });
1510
+ processes.push(schedulerProcess);
1511
+ managedExitPromises.push(waitForManagedProcessExit(schedulerProcess, "Scheduler polling engine"));
1512
+ }
1513
+ }
1514
+ const firstExit = await Promise.race(managedExitPromises);
1515
+ await cleanupAndWait();
1516
+ envChangePromiseResolve = null;
1517
+ if (isDevServerRestartResult(firstExit)) {
1518
+ console.log(`[server] Detected environment file change (${path.basename(firstExit.filePath)}). Restarting app runtime...`);
1519
+ continue;
1520
+ }
1521
+ if (!isExpectedManagedExitSignal(firstExit.signal)) {
1522
+ throw createManagedProcessExitError(firstExit);
1523
+ }
1524
+ stopping = true;
1445
1525
  }
1446
- }
1447
- if (autoSpawnScheduler && queueStrategy === "local") {
1448
- console.log("[server] Starting scheduler polling engine...");
1449
- const schedulerProcess = spawn("node", [mercatoBin, "scheduler", "start"], {
1450
- stdio: "inherit",
1451
- env: runtimeEnv,
1452
- cwd: appDir
1453
- });
1454
- processes.push(schedulerProcess);
1455
- managedExitPromises.push(waitForManagedProcessExit(schedulerProcess, "Scheduler polling engine"));
1456
- }
1457
- const firstExit = await Promise.race(managedExitPromises);
1458
- await cleanupAndWait();
1459
- if (!isExpectedManagedExitSignal(firstExit.signal)) {
1460
- throw createManagedProcessExitError(firstExit);
1526
+ } finally {
1527
+ stopEnvWatcher();
1461
1528
  }
1462
1529
  }
1463
1530
  },
@@ -1474,13 +1541,14 @@ async function run(argv = process.argv) {
1474
1541
  const autoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER !== "false";
1475
1542
  const queueStrategy = process.env.QUEUE_STRATEGY || "local";
1476
1543
  const runtimeEnv = buildServerProcessEnvironment(process.env);
1544
+ const schedulerCommand = lookupModuleCommand(getCliModules(), "scheduler", "start");
1477
1545
  const serverStartLock = acquireServerStartLock(appDir, {
1478
1546
  port: runtimeEnv.PORT ?? process.env.PORT ?? null
1479
1547
  });
1480
1548
  function cleanup() {
1481
1549
  console.log("[server] Shutting down...");
1482
1550
  for (const proc of processes) {
1483
- if (!proc.killed) {
1551
+ if (!proc.killed && proc.exitCode === null && proc.signalCode === null) {
1484
1552
  proc.kill("SIGTERM");
1485
1553
  }
1486
1554
  }
@@ -1490,7 +1558,7 @@ async function run(argv = process.argv) {
1490
1558
  await Promise.all(
1491
1559
  processes.map(
1492
1560
  (proc) => new Promise((resolve) => {
1493
- if (proc.exitCode !== null) return resolve();
1561
+ if (proc.exitCode !== null || proc.signalCode !== null) return resolve();
1494
1562
  proc.on("exit", () => resolve());
1495
1563
  })
1496
1564
  )
@@ -1509,6 +1577,9 @@ async function run(argv = process.argv) {
1509
1577
  cwd: appDir
1510
1578
  });
1511
1579
  processes.push(nextProcess);
1580
+ const managedExitPromises = [
1581
+ waitForManagedProcessExit(nextProcess, "Next.js production server")
1582
+ ];
1512
1583
  if (autoSpawnWorkers) {
1513
1584
  const discoveredWorkerQueues = [...new Set(getRegisteredCliWorkers().map((worker) => worker.queue))];
1514
1585
  if (discoveredWorkerQueues.length === 0) {
@@ -1521,25 +1592,28 @@ async function run(argv = process.argv) {
1521
1592
  cwd: appDir
1522
1593
  });
1523
1594
  processes.push(workerProcess);
1595
+ managedExitPromises.push(waitForManagedProcessExit(workerProcess, formatQueueWorkerLabel(discoveredWorkerQueues)));
1524
1596
  }
1525
1597
  }
1526
1598
  if (autoSpawnScheduler && queueStrategy === "local") {
1527
- console.log("[server] Starting scheduler polling engine...");
1528
- const schedulerProcess = spawn("node", [mercatoBin, "scheduler", "start"], {
1529
- stdio: "inherit",
1530
- env: runtimeEnv,
1531
- cwd: appDir
1532
- });
1533
- processes.push(schedulerProcess);
1599
+ if (schedulerCommand.status !== "ok") {
1600
+ console.log(`[server] Skipping scheduler auto-start \u2014 ${describeMissingModuleCommand(schedulerCommand)}`);
1601
+ } else {
1602
+ console.log("[server] Starting scheduler polling engine...");
1603
+ const schedulerProcess = spawn("node", [mercatoBin, "scheduler", "start"], {
1604
+ stdio: "inherit",
1605
+ env: runtimeEnv,
1606
+ cwd: appDir
1607
+ });
1608
+ processes.push(schedulerProcess);
1609
+ managedExitPromises.push(waitForManagedProcessExit(schedulerProcess, "Scheduler polling engine"));
1610
+ }
1534
1611
  }
1535
- await Promise.race(
1536
- processes.map(
1537
- (proc) => new Promise((resolve) => {
1538
- proc.on("exit", () => resolve());
1539
- })
1540
- )
1541
- );
1612
+ const firstExit = await Promise.race(managedExitPromises);
1542
1613
  await cleanupAndWait();
1614
+ if (!isExpectedManagedExitSignal(firstExit.signal)) {
1615
+ throw createManagedProcessExitError(firstExit);
1616
+ }
1543
1617
  } finally {
1544
1618
  serverStartLock.release();
1545
1619
  }