@open-mercato/cli 0.6.8-develop.6891.1.4dca3f1ad3 → 0.6.8-develop.6892.1.f179677788

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 (105) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/AGENTS.md +6 -1
  3. package/dist/agentic/guides/module-facts.json +113 -113
  4. package/dist/agentic/guides/modules/ai_assistant.md +1 -1
  5. package/dist/agentic/guides/modules/api_docs.md +1 -1
  6. package/dist/agentic/guides/modules/api_keys.md +1 -1
  7. package/dist/agentic/guides/modules/attachments.md +1 -1
  8. package/dist/agentic/guides/modules/audit_logs.md +1 -1
  9. package/dist/agentic/guides/modules/auth.md +1 -1
  10. package/dist/agentic/guides/modules/business_rules.md +1 -1
  11. package/dist/agentic/guides/modules/catalog.md +1 -1
  12. package/dist/agentic/guides/modules/channel_gmail.md +1 -1
  13. package/dist/agentic/guides/modules/channel_imap.md +1 -1
  14. package/dist/agentic/guides/modules/checkout.md +1 -1
  15. package/dist/agentic/guides/modules/communication_channels.md +3 -3
  16. package/dist/agentic/guides/modules/configs.md +1 -1
  17. package/dist/agentic/guides/modules/content.md +1 -1
  18. package/dist/agentic/guides/modules/currencies.md +1 -1
  19. package/dist/agentic/guides/modules/customer_accounts.md +1 -1
  20. package/dist/agentic/guides/modules/customers.md +1 -1
  21. package/dist/agentic/guides/modules/dashboards.md +1 -1
  22. package/dist/agentic/guides/modules/data_sync.md +1 -1
  23. package/dist/agentic/guides/modules/design_system.md +1 -1
  24. package/dist/agentic/guides/modules/dictionaries.md +1 -1
  25. package/dist/agentic/guides/modules/directory.md +1 -1
  26. package/dist/agentic/guides/modules/entities.md +1 -1
  27. package/dist/agentic/guides/modules/events.md +1 -1
  28. package/dist/agentic/guides/modules/feature_toggles.md +1 -1
  29. package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
  30. package/dist/agentic/guides/modules/generators.md +1 -1
  31. package/dist/agentic/guides/modules/inbox_ops.md +1 -1
  32. package/dist/agentic/guides/modules/integrations.md +1 -1
  33. package/dist/agentic/guides/modules/messages.md +1 -1
  34. package/dist/agentic/guides/modules/notifications.md +1 -1
  35. package/dist/agentic/guides/modules/onboarding.md +1 -1
  36. package/dist/agentic/guides/modules/payment_gateways.md +1 -1
  37. package/dist/agentic/guides/modules/perspectives.md +1 -1
  38. package/dist/agentic/guides/modules/planner.md +1 -1
  39. package/dist/agentic/guides/modules/portal.md +1 -1
  40. package/dist/agentic/guides/modules/progress.md +1 -1
  41. package/dist/agentic/guides/modules/query_index.md +1 -1
  42. package/dist/agentic/guides/modules/record_locks.md +1 -1
  43. package/dist/agentic/guides/modules/resources.md +1 -1
  44. package/dist/agentic/guides/modules/sales.md +1 -1
  45. package/dist/agentic/guides/modules/scheduler.md +1 -1
  46. package/dist/agentic/guides/modules/search.md +1 -1
  47. package/dist/agentic/guides/modules/security.md +1 -1
  48. package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
  49. package/dist/agentic/guides/modules/sso.md +1 -1
  50. package/dist/agentic/guides/modules/staff.md +1 -1
  51. package/dist/agentic/guides/modules/storage_s3.md +1 -1
  52. package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
  53. package/dist/agentic/guides/modules/sync_excel.md +1 -1
  54. package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
  55. package/dist/agentic/guides/modules/translations.md +1 -1
  56. package/dist/agentic/guides/modules/webhooks.md +1 -1
  57. package/dist/agentic/guides/modules/wms.md +1 -1
  58. package/dist/agentic/guides/modules/workflows.md +1 -1
  59. package/dist/agentic/guides/upstream/manifest.json +1 -1
  60. package/dist/bin.js +20 -34
  61. package/dist/bin.js.map +2 -2
  62. package/dist/lib/cli-bootstrap-mode.js +32 -0
  63. package/dist/lib/cli-bootstrap-mode.js.map +7 -0
  64. package/dist/lib/dev-supervisor-manifest.js +105 -0
  65. package/dist/lib/dev-supervisor-manifest.js.map +7 -0
  66. package/dist/lib/generators/entity-ids.js +11 -24
  67. package/dist/lib/generators/entity-ids.js.map +2 -2
  68. package/dist/lib/generators/module-di.js +10 -23
  69. package/dist/lib/generators/module-di.js.map +2 -2
  70. package/dist/lib/generators/module-entities.js +10 -23
  71. package/dist/lib/generators/module-entities.js.map +2 -2
  72. package/dist/lib/generators/module-registry.js +394 -31
  73. package/dist/lib/generators/module-registry.js.map +2 -2
  74. package/dist/lib/generators/route-manifest-shards.js +170 -0
  75. package/dist/lib/generators/route-manifest-shards.js.map +7 -0
  76. package/dist/lib/queue-worker-supervisor.js.map +2 -2
  77. package/dist/lib/resolver.js +26 -3
  78. package/dist/lib/resolver.js.map +2 -2
  79. package/dist/lib/utils.js +15 -8
  80. package/dist/lib/utils.js.map +2 -2
  81. package/dist/mercato.js +9 -6
  82. package/dist/mercato.js.map +2 -2
  83. package/package.json +6 -6
  84. package/src/__tests__/mercato.test.ts +144 -0
  85. package/src/bin.ts +28 -42
  86. package/src/lib/__tests__/cli-bootstrap-mode.test.ts +22 -0
  87. package/src/lib/__tests__/content-stable-writes.test.ts +74 -0
  88. package/src/lib/__tests__/dev-supervisor-manifest.test.ts +131 -0
  89. package/src/lib/__tests__/queue-worker-supervisor.test.ts +33 -0
  90. package/src/lib/__tests__/resolver.enterprise.test.ts +50 -0
  91. package/src/lib/cli-bootstrap-mode.ts +31 -0
  92. package/src/lib/dev-supervisor-manifest.ts +142 -0
  93. package/src/lib/generators/__tests__/entity-ids.test.ts +32 -0
  94. package/src/lib/generators/__tests__/module-subset.test.ts +248 -0
  95. package/src/lib/generators/__tests__/output-snapshots.test.ts +3 -3
  96. package/src/lib/generators/__tests__/route-manifest-shards.test.ts +62 -0
  97. package/src/lib/generators/entity-ids.ts +10 -26
  98. package/src/lib/generators/module-di.ts +9 -25
  99. package/src/lib/generators/module-entities.ts +9 -25
  100. package/src/lib/generators/module-registry.ts +432 -29
  101. package/src/lib/generators/route-manifest-shards.ts +146 -0
  102. package/src/lib/queue-worker-supervisor.ts +7 -2
  103. package/src/lib/resolver.ts +31 -3
  104. package/src/lib/utils.ts +24 -16
  105. package/src/mercato.ts +15 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cli",
3
- "version": "0.6.8-develop.6891.1.4dca3f1ad3",
3
+ "version": "0.6.8-develop.6892.1.f179677788",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,9 +60,9 @@
60
60
  "@mikro-orm/decorators": "^7.1.8",
61
61
  "@mikro-orm/migrations": "^7.1.8",
62
62
  "@mikro-orm/postgresql": "^7.1.8",
63
- "@open-mercato/queue": "0.6.8-develop.6891.1.4dca3f1ad3",
64
- "@open-mercato/shared": "0.6.8-develop.6891.1.4dca3f1ad3",
65
- "@open-mercato/telemetry": "0.6.8-develop.6891.1.4dca3f1ad3",
63
+ "@open-mercato/queue": "0.6.8-develop.6892.1.f179677788",
64
+ "@open-mercato/shared": "0.6.8-develop.6892.1.f179677788",
65
+ "@open-mercato/telemetry": "0.6.8-develop.6892.1.f179677788",
66
66
  "cross-spawn": "^7.0.6",
67
67
  "pg": "8.22.0",
68
68
  "semver": "^7.8.5",
@@ -73,10 +73,10 @@
73
73
  "typescript-js": "npm:typescript@6.0.3"
74
74
  },
75
75
  "peerDependencies": {
76
- "@open-mercato/shared": "0.6.8-develop.6891.1.4dca3f1ad3"
76
+ "@open-mercato/shared": "0.6.8-develop.6892.1.f179677788"
77
77
  },
78
78
  "devDependencies": {
79
- "@open-mercato/shared": "0.6.8-develop.6891.1.4dca3f1ad3",
79
+ "@open-mercato/shared": "0.6.8-develop.6892.1.f179677788",
80
80
  "@types/jest": "^30.0.0",
81
81
  "jest": "^30.4.2",
82
82
  "ts-jest": "^29.4.12"
@@ -716,6 +716,7 @@ describe('generate post-step structural invalidation', () => {
716
716
  consoleErrorSpy.mockRestore()
717
717
  consoleLogSpy.mockRestore()
718
718
  })
719
+
719
720
  })
720
721
 
721
722
  describe('server dev managed process exits', () => {
@@ -1263,6 +1264,86 @@ describe('server dev managed process exits', () => {
1263
1264
  consoleLogSpy.mockRestore()
1264
1265
  })
1265
1266
 
1267
+ it('uses lightweight manifest workers instead of handler-bearing CLI modules', async () => {
1268
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
1269
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
1270
+ process.env.OM_AUTO_SPAWN_WORKERS_LAZY = 'true'
1271
+ process.env.OM_AUTO_SPAWN_WORKERS_LAZY_MODE = 'shared'
1272
+
1273
+ jest.doMock('node:fs', () => {
1274
+ const actual = jest.requireActual('node:fs')
1275
+ return {
1276
+ ...actual,
1277
+ existsSync: jest.fn((candidate: string) =>
1278
+ pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
1279
+ ),
1280
+ unlinkSync: jest.fn(),
1281
+ }
1282
+ })
1283
+ jest.doMock('../lib/generators', () => ({
1284
+ generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
1285
+ }))
1286
+ jest.doMock('../lib/resolver', () => ({
1287
+ resolveEnvironment: () => ({
1288
+ appDir: '/tmp/test-app',
1289
+ rootDir: '/tmp/test-root',
1290
+ }),
1291
+ createResolver: () => ({}),
1292
+ }))
1293
+
1294
+ const supervisorClose = jest.fn().mockResolvedValue(undefined)
1295
+ const startLazyWorkerSupervisor = jest.fn(() => ({
1296
+ startedQueues: new Set<string>(),
1297
+ getActiveChild: () => undefined,
1298
+ close: supervisorClose,
1299
+ done: Promise.resolve(),
1300
+ }))
1301
+ jest.doMock('../lib/queue-worker-supervisor', () => ({
1302
+ startLazyWorkerSupervisor,
1303
+ }))
1304
+ jest.doMock('child_process', () =>
1305
+ buildMockChildProcessModule((args) =>
1306
+ pathIncludes(args[0] ?? '', 'next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
1307
+ ),
1308
+ )
1309
+
1310
+ const manifestRegistry = await import('../lib/dev-supervisor-manifest')
1311
+ manifestRegistry.registerDevSupervisorManifest({
1312
+ version: 1,
1313
+ workers: [
1314
+ {
1315
+ id: 'manifest:worker',
1316
+ moduleId: 'manifest',
1317
+ queue: 'manifest-events',
1318
+ concurrency: 1,
1319
+ },
1320
+ ],
1321
+ schedulerStartStatus: 'missing-module',
1322
+ requiresFullBootstrap: false,
1323
+ })
1324
+ const mercato = await import('../mercato')
1325
+ mercato.registerCliModules([eventsWorkerFixture as Module])
1326
+
1327
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
1328
+
1329
+ expect(exitCode).toBe(0)
1330
+ expect(startLazyWorkerSupervisor).toHaveBeenCalledTimes(1)
1331
+ const workers = startLazyWorkerSupervisor.mock.calls[0][0].workers
1332
+ expect(workers).toEqual([
1333
+ {
1334
+ id: 'manifest:worker',
1335
+ moduleId: 'manifest',
1336
+ queue: 'manifest-events',
1337
+ concurrency: 1,
1338
+ },
1339
+ ])
1340
+ expect(workers[0]).not.toHaveProperty('handler')
1341
+ expect(supervisorClose).toHaveBeenCalled()
1342
+
1343
+ consoleErrorSpy.mockRestore()
1344
+ consoleLogSpy.mockRestore()
1345
+ })
1346
+
1266
1347
  it('starts the lazy scheduler supervisor instead of the scheduler process when OM_AUTO_SPAWN_SCHEDULER_LAZY=true', async () => {
1267
1348
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
1268
1349
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
@@ -1328,6 +1409,69 @@ describe('server dev managed process exits', () => {
1328
1409
  consoleErrorSpy.mockRestore()
1329
1410
  consoleLogSpy.mockRestore()
1330
1411
  })
1412
+
1413
+ it('uses the lightweight manifest scheduler status without loading scheduler CLI modules', async () => {
1414
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
1415
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
1416
+ process.env.AUTO_SPAWN_SCHEDULER = 'true'
1417
+ process.env.AUTO_SPAWN_WORKERS = 'false'
1418
+ process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY = 'true'
1419
+
1420
+ jest.doMock('node:fs', () => {
1421
+ const actual = jest.requireActual('node:fs')
1422
+ return {
1423
+ ...actual,
1424
+ existsSync: jest.fn((candidate: string) =>
1425
+ pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
1426
+ ),
1427
+ unlinkSync: jest.fn(),
1428
+ }
1429
+ })
1430
+ jest.doMock('../lib/generators', () => ({
1431
+ generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
1432
+ }))
1433
+ jest.doMock('../lib/resolver', () => ({
1434
+ resolveEnvironment: () => ({
1435
+ appDir: '/tmp/test-app',
1436
+ rootDir: '/tmp/test-root',
1437
+ }),
1438
+ createResolver: () => ({}),
1439
+ }))
1440
+
1441
+ const schedulerClose = jest.fn().mockResolvedValue(undefined)
1442
+ const startLazySchedulerSupervisor = jest.fn(() => ({
1443
+ started: false,
1444
+ getActiveChild: () => undefined,
1445
+ close: schedulerClose,
1446
+ done: Promise.resolve(),
1447
+ }))
1448
+ jest.doMock('../lib/scheduler-supervisor', () => ({
1449
+ startLazySchedulerSupervisor,
1450
+ }))
1451
+ jest.doMock('child_process', () =>
1452
+ buildMockChildProcessModule((args) =>
1453
+ pathIncludes(args[0] ?? '', 'next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
1454
+ ),
1455
+ )
1456
+
1457
+ const manifestRegistry = await import('../lib/dev-supervisor-manifest')
1458
+ manifestRegistry.registerDevSupervisorManifest({
1459
+ version: 1,
1460
+ workers: [],
1461
+ schedulerStartStatus: 'ok',
1462
+ requiresFullBootstrap: false,
1463
+ })
1464
+ const mercato = await import('../mercato')
1465
+
1466
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
1467
+
1468
+ expect(exitCode).toBe(0)
1469
+ expect(startLazySchedulerSupervisor).toHaveBeenCalledTimes(1)
1470
+ expect(schedulerClose).toHaveBeenCalled()
1471
+
1472
+ consoleErrorSpy.mockRestore()
1473
+ consoleLogSpy.mockRestore()
1474
+ })
1331
1475
  })
1332
1476
 
1333
1477
  describe('server start managed process exits', () => {
package/src/bin.ts CHANGED
@@ -8,37 +8,10 @@ import {
8
8
  getTelemetryRuntime,
9
9
  isTelemetryBackendEnabled,
10
10
  } from '@open-mercato/shared/lib/telemetry/runtime'
11
+ import { resolveCliBootstrapMode, type CliBootstrapMode } from './lib/cli-bootstrap-mode.js'
11
12
  // `run` is imported dynamically inside `main()` so telemetry can initialize
12
13
  // before the mercato entry (and its Postgres driver) loads — see main().
13
14
 
14
- // Commands that can run without bootstrap (without generated files)
15
- // - generate: creates the generated files
16
- // - db: uses resolver directly to find modules and migrations
17
- // - init: runs yarn commands to set up the app
18
- // - help: just shows help text
19
- const BOOTSTRAP_FREE_COMMANDS = [
20
- 'generate',
21
- 'module',
22
- 'deploy',
23
- 'db',
24
- 'init',
25
- 'agentic:init',
26
- 'telemetry',
27
- 'eject',
28
- 'test',
29
- 'test:integration',
30
- 'test:integration:coverage',
31
- 'test:integration:spec-coverage',
32
- 'test:ephemeral',
33
- 'test:integration:interactive',
34
- 'umes:list',
35
- 'umes:inspect',
36
- 'umes:check',
37
- 'help',
38
- '--help',
39
- '-h',
40
- ]
41
-
42
15
  function assertNode24Runtime(): void {
43
16
  const detectedNodeVersion = process.versions.node
44
17
  const majorVersion = Number.parseInt(detectedNodeVersion.split('.')[0] ?? '0', 10)
@@ -54,20 +27,30 @@ function assertNode24Runtime(): void {
54
27
  )
55
28
  }
56
29
 
57
- function needsBootstrap(argv: string[]): boolean {
58
- const [, , first] = argv
59
- if (!first) return false // help screen
60
- return !BOOTSTRAP_FREE_COMMANDS.includes(first)
61
- }
62
-
63
- async function tryBootstrap(): Promise<boolean> {
30
+ async function tryBootstrap(mode: Exclude<CliBootstrapMode, 'none'>): Promise<boolean> {
64
31
  try {
65
- const { bootstrapFromAppRoot } = await import('@open-mercato/shared/lib/bootstrap/dynamicLoader')
66
- const { registerCliModules } = await import('./mercato.js')
67
32
  // Use the CLI resolver to find the app directory (handles monorepo detection)
68
33
  const { createResolver } = await import('./lib/resolver.js')
69
34
  const resolver = createResolver()
70
35
  const appDir = resolver.getAppDir()
36
+
37
+ if (mode === 'dev-supervisor') {
38
+ const {
39
+ canUseLightweightDevSupervisor,
40
+ loadDevSupervisorManifest,
41
+ registerDevSupervisorManifest,
42
+ } = await import(
43
+ './lib/dev-supervisor-manifest.js'
44
+ )
45
+ const manifest = loadDevSupervisorManifest(appDir)
46
+ if (canUseLightweightDevSupervisor(manifest)) {
47
+ registerDevSupervisorManifest(manifest)
48
+ return true
49
+ }
50
+ }
51
+
52
+ const { bootstrapFromAppRoot } = await import('@open-mercato/shared/lib/bootstrap/dynamicLoader')
53
+ const { registerCliModules } = await import('./mercato.js')
71
54
  const data = await bootstrapFromAppRoot(appDir)
72
55
  // Register CLI modules directly to avoid module resolution issues
73
56
  registerCliModules(data.modules)
@@ -76,8 +59,11 @@ async function tryBootstrap(): Promise<boolean> {
76
59
  const message = err instanceof Error ? err.message : String(err)
77
60
  // Check if the error is about missing generated files
78
61
  if (
79
- message.includes('Cannot find module') &&
80
- (message.includes('/generated/') || message.includes('.generated') || message.includes('.mercato'))
62
+ message.includes('dev-supervisor.generated.json') ||
63
+ (
64
+ message.includes('Cannot find module') &&
65
+ (message.includes('/generated/') || message.includes('.generated') || message.includes('.mercato'))
66
+ )
81
67
  ) {
82
68
  return false
83
69
  }
@@ -88,9 +74,9 @@ async function tryBootstrap(): Promise<boolean> {
88
74
 
89
75
  async function main(): Promise<void> {
90
76
  assertNode24Runtime()
91
- const requiresBootstrap = needsBootstrap(process.argv)
77
+ const bootstrapMode = resolveCliBootstrapMode(process.argv)
92
78
 
93
- if (requiresBootstrap) {
79
+ if (bootstrapMode !== 'none') {
94
80
  // Load the app's `.env` BEFORE initTelemetry(): `run()` only dotenv-loads it
95
81
  // later (ensureEnvLoaded), which is too late — TELEMETRY_BACKEND set only in
96
82
  // `.env` would silently resolve to `noop` for worker/scheduler processes.
@@ -111,7 +97,7 @@ async function main(): Promise<void> {
111
97
  await initTelemetry()
112
98
  }
113
99
 
114
- const bootstrapSucceeded = await tryBootstrap()
100
+ const bootstrapSucceeded = await tryBootstrap(bootstrapMode)
115
101
  if (!bootstrapSucceeded) {
116
102
  console.error('╔═══════════════════════════════════════════════════════════════════╗')
117
103
  console.error('║ Generated files not found! ║')
@@ -0,0 +1,22 @@
1
+ import { resolveCliBootstrapMode } from '../cli-bootstrap-mode'
2
+
3
+ describe('resolveCliBootstrapMode', () => {
4
+ it('uses the lightweight manifest only for the exact server dev command', () => {
5
+ expect(resolveCliBootstrapMode(['node', 'mercato', 'server', 'dev'])).toBe('dev-supervisor')
6
+ expect(resolveCliBootstrapMode(['node', 'mercato', 'server', 'dev', '--port', '4000'])).toBe('dev-supervisor')
7
+ })
8
+
9
+ it('keeps production, workers, scheduler, and module commands on the full bootstrap', () => {
10
+ expect(resolveCliBootstrapMode(['node', 'mercato', 'server', 'start'])).toBe('full')
11
+ expect(resolveCliBootstrapMode(['node', 'mercato', 'queue', 'worker', '--all'])).toBe('full')
12
+ expect(resolveCliBootstrapMode(['node', 'mercato', 'scheduler', 'start'])).toBe('full')
13
+ expect(resolveCliBootstrapMode(['node', 'mercato', 'customers', 'seed'])).toBe('full')
14
+ })
15
+
16
+ it('preserves existing bootstrap-free commands', () => {
17
+ expect(resolveCliBootstrapMode(['node', 'mercato', 'generate'])).toBe('none')
18
+ expect(resolveCliBootstrapMode(['node', 'mercato', 'db', 'generate'])).toBe('none')
19
+ expect(resolveCliBootstrapMode(['node', 'mercato', 'help'])).toBe('none')
20
+ expect(resolveCliBootstrapMode(['node', 'mercato'])).toBe('none')
21
+ })
22
+ })
@@ -0,0 +1,74 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { createGeneratorResult, readChecksumRecord, writeGeneratedFile } from '../utils'
5
+
6
+ describe('content-stable generated writes', () => {
7
+ let tmpDir: string
8
+
9
+ beforeEach(() => {
10
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'generated-write-test-'))
11
+ })
12
+
13
+ afterEach(() => {
14
+ fs.rmSync(tmpDir, { recursive: true, force: true })
15
+ })
16
+
17
+ it('updates structure checksum state without touching byte-identical generated output', () => {
18
+ const outFile = path.join(tmpDir, 'modules.generated.ts')
19
+ const checksumFile = path.join(tmpDir, 'modules.generated.checksum')
20
+ const content = '// generated\nexport const modules = []\n'
21
+
22
+ writeGeneratedFile({
23
+ outFile,
24
+ checksumFile,
25
+ content,
26
+ structureChecksum: 'structure-a',
27
+ result: createGeneratorResult(),
28
+ quiet: true,
29
+ })
30
+
31
+ const oldTime = new Date(Date.now() - 60_000)
32
+ fs.utimesSync(outFile, oldTime, oldTime)
33
+ fs.utimesSync(checksumFile, oldTime, oldTime)
34
+ const outputMtimeBefore = fs.statSync(outFile).mtimeMs
35
+
36
+ const result = createGeneratorResult()
37
+ writeGeneratedFile({
38
+ outFile,
39
+ checksumFile,
40
+ content,
41
+ structureChecksum: 'structure-b',
42
+ result,
43
+ quiet: true,
44
+ })
45
+
46
+ expect(result.filesWritten).toEqual([])
47
+ expect(result.filesUnchanged).toEqual([outFile])
48
+ expect(fs.statSync(outFile).mtimeMs).toBe(outputMtimeBefore)
49
+ expect(fs.statSync(checksumFile).mtimeMs).toBeGreaterThan(oldTime.getTime())
50
+ expect(readChecksumRecord(checksumFile)?.structure).toBe('structure-b')
51
+ })
52
+
53
+ it('rewrites generated output when its actual bytes change', () => {
54
+ const outFile = path.join(tmpDir, 'modules.generated.ts')
55
+ const checksumFile = path.join(tmpDir, 'modules.generated.checksum')
56
+ fs.writeFileSync(outFile, 'stale output\n')
57
+
58
+ const result = createGeneratorResult()
59
+ const content = '// generated\nexport const modules = ["customers"]\n'
60
+ writeGeneratedFile({
61
+ outFile,
62
+ checksumFile,
63
+ content,
64
+ structureChecksum: 'structure-c',
65
+ result,
66
+ quiet: true,
67
+ })
68
+
69
+ expect(result.filesWritten).toEqual([outFile])
70
+ expect(result.filesUnchanged).toEqual([])
71
+ expect(fs.readFileSync(outFile, 'utf8')).toBe(content)
72
+ expect(readChecksumRecord(checksumFile)?.structure).toBe('structure-c')
73
+ })
74
+ })
@@ -0,0 +1,131 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import {
5
+ canUseLightweightDevSupervisor,
6
+ DEV_SUPERVISOR_MANIFEST_FILE,
7
+ getRegisteredDevSupervisorManifest,
8
+ loadDevSupervisorManifest,
9
+ registerDevSupervisorManifest,
10
+ resetDevSupervisorManifestForTests,
11
+ } from '../dev-supervisor-manifest'
12
+
13
+ function writeManifest(appDir: string, value: unknown): string {
14
+ const filePath = path.join(appDir, '.mercato', 'generated', DEV_SUPERVISOR_MANIFEST_FILE)
15
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
16
+ fs.writeFileSync(filePath, JSON.stringify(value), 'utf8')
17
+ return filePath
18
+ }
19
+
20
+ describe('dev supervisor manifest', () => {
21
+ let appDir: string
22
+
23
+ beforeEach(() => {
24
+ appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-dev-supervisor-'))
25
+ resetDevSupervisorManifestForTests()
26
+ })
27
+
28
+ afterEach(() => {
29
+ resetDevSupervisorManifestForTests()
30
+ fs.rmSync(appDir, { recursive: true, force: true })
31
+ })
32
+
33
+ it('loads and registers primitive worker and scheduler metadata', () => {
34
+ writeManifest(appDir, {
35
+ version: 1,
36
+ workers: [
37
+ {
38
+ id: 'events:workers:dispatch',
39
+ moduleId: 'events',
40
+ queue: 'events',
41
+ concurrency: 2,
42
+ },
43
+ ],
44
+ schedulerStartStatus: 'ok',
45
+ requiresFullBootstrap: false,
46
+ })
47
+
48
+ const manifest = loadDevSupervisorManifest(appDir)
49
+ expect(manifest).toEqual({
50
+ version: 1,
51
+ workers: [
52
+ {
53
+ id: 'events:workers:dispatch',
54
+ moduleId: 'events',
55
+ queue: 'events',
56
+ concurrency: 2,
57
+ },
58
+ ],
59
+ schedulerStartStatus: 'ok',
60
+ requiresFullBootstrap: false,
61
+ })
62
+
63
+ expect(canUseLightweightDevSupervisor(manifest)).toBe(true)
64
+ registerDevSupervisorManifest(manifest)
65
+ expect(getRegisteredDevSupervisorManifest()).toBe(manifest)
66
+ })
67
+
68
+ it('reports a missing generated manifest with regeneration guidance', () => {
69
+ expect(() => loadDevSupervisorManifest(appDir)).toThrow(
70
+ /dev-supervisor\.generated\.json.*Run `yarn generate`/,
71
+ )
72
+ })
73
+
74
+ it('rejects malformed JSON', () => {
75
+ const filePath = path.join(appDir, '.mercato', 'generated', DEV_SUPERVISOR_MANIFEST_FILE)
76
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
77
+ fs.writeFileSync(filePath, '{', 'utf8')
78
+
79
+ expect(() => loadDevSupervisorManifest(appDir)).toThrow(/JSON could not be parsed/)
80
+ })
81
+
82
+ it('rejects unsupported versions and invalid worker fields', () => {
83
+ writeManifest(appDir, {
84
+ version: 2,
85
+ workers: [],
86
+ schedulerStartStatus: 'missing-module',
87
+ })
88
+ expect(() => loadDevSupervisorManifest(appDir)).toThrow(/unsupported version 2/)
89
+
90
+ writeManifest(appDir, {
91
+ version: 1,
92
+ workers: [{ id: 'worker', queue: '', concurrency: 1 }],
93
+ schedulerStartStatus: 'missing-module',
94
+ })
95
+ expect(() => loadDevSupervisorManifest(appDir)).toThrow(/workers\[0\]\.queue/)
96
+ })
97
+
98
+ it('rejects unknown scheduler command states', () => {
99
+ writeManifest(appDir, {
100
+ version: 1,
101
+ workers: [],
102
+ schedulerStartStatus: 'maybe',
103
+ })
104
+ expect(() => loadDevSupervisorManifest(appDir)).toThrow(/schedulerStartStatus is invalid/)
105
+ })
106
+
107
+ it('preserves the full-bootstrap fallback marker', () => {
108
+ writeManifest(appDir, {
109
+ version: 1,
110
+ workers: [],
111
+ schedulerStartStatus: 'ok',
112
+ requiresFullBootstrap: true,
113
+ })
114
+
115
+ const manifest = loadDevSupervisorManifest(appDir)
116
+ expect(manifest.requiresFullBootstrap).toBe(true)
117
+ expect(canUseLightweightDevSupervisor(manifest)).toBe(false)
118
+ })
119
+
120
+ it('treats a legacy version-1 manifest without the safety marker as full-bootstrap only', () => {
121
+ writeManifest(appDir, {
122
+ version: 1,
123
+ workers: [],
124
+ schedulerStartStatus: 'ok',
125
+ })
126
+
127
+ const manifest = loadDevSupervisorManifest(appDir)
128
+ expect(manifest.requiresFullBootstrap).toBe(true)
129
+ expect(canUseLightweightDevSupervisor(manifest)).toBe(false)
130
+ })
131
+ })
@@ -65,6 +65,39 @@ async function flushAsync(times = 5): Promise<void> {
65
65
  }
66
66
 
67
67
  describe('startLazyWorkerSupervisor', () => {
68
+ it('accepts lightweight worker descriptors without importing handlers', async () => {
69
+ const spawnFn = jest.fn() as unknown as jest.MockedFunction<LazySupervisorSpawnFn>
70
+ const probeFn = jest.fn(async (queueName, strategy) =>
71
+ emptyProbe(queueName, strategy),
72
+ ) as jest.MockedFunction<LazySupervisorProbeFn>
73
+
74
+ const handle = startLazyWorkerSupervisor({
75
+ mercatoBin: '/tmp/mercato',
76
+ appDir: '/tmp/app',
77
+ runtimeEnv: { ...process.env },
78
+ workers: [
79
+ {
80
+ id: 'events:workers:dispatch',
81
+ moduleId: 'events',
82
+ queue: 'events',
83
+ concurrency: 1,
84
+ },
85
+ ],
86
+ pollMs: 250,
87
+ restartOnUnexpectedExit: true,
88
+ strategy: 'local',
89
+ spawnFn,
90
+ probeFn,
91
+ logger: silentLogger,
92
+ })
93
+
94
+ await flushAsync(20)
95
+ expect(probeFn).toHaveBeenCalledWith('events', 'local')
96
+ expect(spawnFn).not.toHaveBeenCalled()
97
+
98
+ await handle.close()
99
+ })
100
+
68
101
  it('does not spawn any worker while every queue is idle', async () => {
69
102
  const spawnFn = jest.fn() as unknown as jest.MockedFunction<LazySupervisorSpawnFn>
70
103
  const probeFn = jest.fn(async (queueName, strategy) =>
@@ -67,6 +67,33 @@ if (enabledModules.some((entry) => entry.id === 'example')) {
67
67
  )
68
68
  }
69
69
 
70
+ function writeModulesConfigWithSupervisorOverrides(rootDir: string) {
71
+ const srcDir = path.join(rootDir, 'src')
72
+ fs.mkdirSync(srcDir, { recursive: true })
73
+ fs.writeFileSync(
74
+ path.join(srcDir, 'modules.ts'),
75
+ `
76
+ const supervisorPolicy = { workers: { 'events:dispatch': null } }
77
+ export const enabledModules = [
78
+ {
79
+ id: 'events',
80
+ from: '@open-mercato/events',
81
+ overrides: {
82
+ workers: {
83
+ 'events:dispatch': null,
84
+ 'events:replacement': { id: 'events:replacement', queue: 'events-v2', concurrency: 2, handler() {} },
85
+ },
86
+ cli: { start: null },
87
+ },
88
+ },
89
+ { id: 'scheduler', overrides: supervisorPolicy },
90
+ { id: 'queue', overrides: { ...supervisorPolicy } },
91
+ ]
92
+ `,
93
+ 'utf8',
94
+ )
95
+ }
96
+
70
97
  describe('resolver enterprise module toggle', () => {
71
98
  const originalEnv = process.env.OM_ENABLE_ENTERPRISE_MODULES
72
99
  const originalResolverMarker = (globalThis as Record<string, unknown>).__resolver_evaluated__
@@ -140,4 +167,27 @@ describe('resolver enterprise module toggle', () => {
140
167
  ]),
141
168
  )
142
169
  })
170
+
171
+ it('marks inline worker and CLI overrides for the full dev bootstrap', () => {
172
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'resolver-supervisor-overrides-'))
173
+ writeModulesConfigWithSupervisorOverrides(tempDir)
174
+
175
+ expect(createResolver(tempDir).loadEnabledModules()).toEqual([
176
+ {
177
+ id: 'events',
178
+ from: '@open-mercato/events',
179
+ devSupervisorRequiresFullBootstrap: true,
180
+ },
181
+ {
182
+ id: 'scheduler',
183
+ from: '@open-mercato/core',
184
+ devSupervisorRequiresFullBootstrap: true,
185
+ },
186
+ {
187
+ id: 'queue',
188
+ from: '@open-mercato/core',
189
+ devSupervisorRequiresFullBootstrap: true,
190
+ },
191
+ ])
192
+ })
143
193
  })
@@ -0,0 +1,31 @@
1
+ export type CliBootstrapMode = 'none' | 'dev-supervisor' | 'full'
2
+
3
+ const BOOTSTRAP_FREE_COMMANDS = new Set([
4
+ 'generate',
5
+ 'module',
6
+ 'deploy',
7
+ 'db',
8
+ 'init',
9
+ 'agentic:init',
10
+ 'telemetry',
11
+ 'eject',
12
+ 'test',
13
+ 'test:integration',
14
+ 'test:integration:coverage',
15
+ 'test:integration:spec-coverage',
16
+ 'test:ephemeral',
17
+ 'test:integration:interactive',
18
+ 'umes:list',
19
+ 'umes:inspect',
20
+ 'umes:check',
21
+ 'help',
22
+ '--help',
23
+ '-h',
24
+ ])
25
+
26
+ export function resolveCliBootstrapMode(argv: readonly string[]): CliBootstrapMode {
27
+ const [, , first, second] = argv
28
+ if (!first || BOOTSTRAP_FREE_COMMANDS.has(first)) return 'none'
29
+ if (first === 'server' && second === 'dev') return 'dev-supervisor'
30
+ return 'full'
31
+ }