@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.
- package/.turbo/turbo-build.log +3 -3
- package/AGENTS.md +6 -1
- package/dist/agentic/guides/module-facts.json +113 -113
- package/dist/agentic/guides/modules/ai_assistant.md +1 -1
- package/dist/agentic/guides/modules/api_docs.md +1 -1
- package/dist/agentic/guides/modules/api_keys.md +1 -1
- package/dist/agentic/guides/modules/attachments.md +1 -1
- package/dist/agentic/guides/modules/audit_logs.md +1 -1
- package/dist/agentic/guides/modules/auth.md +1 -1
- package/dist/agentic/guides/modules/business_rules.md +1 -1
- package/dist/agentic/guides/modules/catalog.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail.md +1 -1
- package/dist/agentic/guides/modules/channel_imap.md +1 -1
- package/dist/agentic/guides/modules/checkout.md +1 -1
- package/dist/agentic/guides/modules/communication_channels.md +3 -3
- package/dist/agentic/guides/modules/configs.md +1 -1
- package/dist/agentic/guides/modules/content.md +1 -1
- package/dist/agentic/guides/modules/currencies.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts.md +1 -1
- package/dist/agentic/guides/modules/customers.md +1 -1
- package/dist/agentic/guides/modules/dashboards.md +1 -1
- package/dist/agentic/guides/modules/data_sync.md +1 -1
- package/dist/agentic/guides/modules/design_system.md +1 -1
- package/dist/agentic/guides/modules/dictionaries.md +1 -1
- package/dist/agentic/guides/modules/directory.md +1 -1
- package/dist/agentic/guides/modules/entities.md +1 -1
- package/dist/agentic/guides/modules/events.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
- package/dist/agentic/guides/modules/generators.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops.md +1 -1
- package/dist/agentic/guides/modules/integrations.md +1 -1
- package/dist/agentic/guides/modules/messages.md +1 -1
- package/dist/agentic/guides/modules/notifications.md +1 -1
- package/dist/agentic/guides/modules/onboarding.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways.md +1 -1
- package/dist/agentic/guides/modules/perspectives.md +1 -1
- package/dist/agentic/guides/modules/planner.md +1 -1
- package/dist/agentic/guides/modules/portal.md +1 -1
- package/dist/agentic/guides/modules/progress.md +1 -1
- package/dist/agentic/guides/modules/query_index.md +1 -1
- package/dist/agentic/guides/modules/record_locks.md +1 -1
- package/dist/agentic/guides/modules/resources.md +1 -1
- package/dist/agentic/guides/modules/sales.md +1 -1
- package/dist/agentic/guides/modules/scheduler.md +1 -1
- package/dist/agentic/guides/modules/search.md +1 -1
- package/dist/agentic/guides/modules/security.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
- package/dist/agentic/guides/modules/sso.md +1 -1
- package/dist/agentic/guides/modules/staff.md +1 -1
- package/dist/agentic/guides/modules/storage_s3.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
- package/dist/agentic/guides/modules/sync_excel.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
- package/dist/agentic/guides/modules/translations.md +1 -1
- package/dist/agentic/guides/modules/webhooks.md +1 -1
- package/dist/agentic/guides/modules/wms.md +1 -1
- package/dist/agentic/guides/modules/workflows.md +1 -1
- package/dist/agentic/guides/upstream/manifest.json +1 -1
- package/dist/bin.js +20 -34
- package/dist/bin.js.map +2 -2
- package/dist/lib/cli-bootstrap-mode.js +32 -0
- package/dist/lib/cli-bootstrap-mode.js.map +7 -0
- package/dist/lib/dev-supervisor-manifest.js +105 -0
- package/dist/lib/dev-supervisor-manifest.js.map +7 -0
- package/dist/lib/generators/entity-ids.js +11 -24
- package/dist/lib/generators/entity-ids.js.map +2 -2
- package/dist/lib/generators/module-di.js +10 -23
- package/dist/lib/generators/module-di.js.map +2 -2
- package/dist/lib/generators/module-entities.js +10 -23
- package/dist/lib/generators/module-entities.js.map +2 -2
- package/dist/lib/generators/module-registry.js +394 -31
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/generators/route-manifest-shards.js +170 -0
- package/dist/lib/generators/route-manifest-shards.js.map +7 -0
- package/dist/lib/queue-worker-supervisor.js.map +2 -2
- package/dist/lib/resolver.js +26 -3
- package/dist/lib/resolver.js.map +2 -2
- package/dist/lib/utils.js +15 -8
- package/dist/lib/utils.js.map +2 -2
- package/dist/mercato.js +9 -6
- package/dist/mercato.js.map +2 -2
- package/package.json +6 -6
- package/src/__tests__/mercato.test.ts +144 -0
- package/src/bin.ts +28 -42
- package/src/lib/__tests__/cli-bootstrap-mode.test.ts +22 -0
- package/src/lib/__tests__/content-stable-writes.test.ts +74 -0
- package/src/lib/__tests__/dev-supervisor-manifest.test.ts +131 -0
- package/src/lib/__tests__/queue-worker-supervisor.test.ts +33 -0
- package/src/lib/__tests__/resolver.enterprise.test.ts +50 -0
- package/src/lib/cli-bootstrap-mode.ts +31 -0
- package/src/lib/dev-supervisor-manifest.ts +142 -0
- package/src/lib/generators/__tests__/entity-ids.test.ts +32 -0
- package/src/lib/generators/__tests__/module-subset.test.ts +248 -0
- package/src/lib/generators/__tests__/output-snapshots.test.ts +3 -3
- package/src/lib/generators/__tests__/route-manifest-shards.test.ts +62 -0
- package/src/lib/generators/entity-ids.ts +10 -26
- package/src/lib/generators/module-di.ts +9 -25
- package/src/lib/generators/module-entities.ts +9 -25
- package/src/lib/generators/module-registry.ts +432 -29
- package/src/lib/generators/route-manifest-shards.ts +146 -0
- package/src/lib/queue-worker-supervisor.ts +7 -2
- package/src/lib/resolver.ts +31 -3
- package/src/lib/utils.ts +24 -16
- 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.
|
|
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.
|
|
64
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
65
|
-
"@open-mercato/telemetry": "0.6.8-develop.
|
|
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.
|
|
76
|
+
"@open-mercato/shared": "0.6.8-develop.6892.1.f179677788"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
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
|
|
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('
|
|
80
|
-
(
|
|
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
|
|
77
|
+
const bootstrapMode = resolveCliBootstrapMode(process.argv)
|
|
92
78
|
|
|
93
|
-
if (
|
|
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
|
+
}
|