@open-mercato/cli 0.6.7-develop.6726.1.983ae8a07e → 0.6.7-develop.6749.1.6b54c56dfe
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/dist/agentic/guides/module-facts.json +2247 -172
- package/dist/agentic/guides/modules/ai_assistant.md +47 -3
- package/dist/agentic/guides/modules/api_docs.md +23 -2
- package/dist/agentic/guides/modules/api_keys.md +25 -3
- package/dist/agentic/guides/modules/attachments.md +25 -3
- package/dist/agentic/guides/modules/audit_logs.md +24 -3
- package/dist/agentic/guides/modules/auth.md +47 -3
- package/dist/agentic/guides/modules/business_rules.md +31 -3
- package/dist/agentic/guides/modules/catalog.md +69 -4
- package/dist/agentic/guides/modules/channel_gmail.md +19 -2
- package/dist/agentic/guides/modules/channel_imap.md +19 -2
- package/dist/agentic/guides/modules/checkout.md +34 -2
- package/dist/agentic/guides/modules/communication_channels.md +23 -2
- package/dist/agentic/guides/modules/configs.md +28 -3
- package/dist/agentic/guides/modules/content.md +22 -2
- package/dist/agentic/guides/modules/currencies.md +32 -3
- package/dist/agentic/guides/modules/customer_accounts.md +27 -2
- package/dist/agentic/guides/modules/customers.md +70 -4
- package/dist/agentic/guides/modules/dashboards.md +25 -3
- package/dist/agentic/guides/modules/data_sync.md +22 -2
- package/dist/agentic/guides/modules/dictionaries.md +21 -2
- package/dist/agentic/guides/modules/directory.md +27 -2
- package/dist/agentic/guides/modules/entities.md +36 -3
- package/dist/agentic/guides/modules/events.md +19 -2
- package/dist/agentic/guides/modules/feature_toggles.md +32 -3
- package/dist/agentic/guides/modules/gateway_stripe.md +23 -3
- package/dist/agentic/guides/modules/generators.md +19 -2
- package/dist/agentic/guides/modules/inbox_ops.md +29 -2
- package/dist/agentic/guides/modules/integrations.md +23 -2
- package/dist/agentic/guides/modules/messages.md +25 -2
- package/dist/agentic/guides/modules/notifications.md +24 -3
- package/dist/agentic/guides/modules/onboarding.md +22 -2
- package/dist/agentic/guides/modules/payment_gateways.md +21 -2
- package/dist/agentic/guides/modules/perspectives.md +19 -2
- package/dist/agentic/guides/modules/planner.md +27 -3
- package/dist/agentic/guides/modules/portal.md +27 -2
- package/dist/agentic/guides/modules/progress.md +19 -2
- package/dist/agentic/guides/modules/query_index.md +27 -3
- package/dist/agentic/guides/modules/record_locks.md +21 -2
- package/dist/agentic/guides/modules/resources.md +32 -3
- package/dist/agentic/guides/modules/sales.md +44 -3
- package/dist/agentic/guides/modules/scheduler.md +30 -3
- package/dist/agentic/guides/modules/search.md +38 -3
- package/dist/agentic/guides/modules/security.md +32 -2
- package/dist/agentic/guides/modules/shipping_carriers.md +21 -2
- package/dist/agentic/guides/modules/sso.md +23 -2
- package/dist/agentic/guides/modules/staff.md +48 -3
- package/dist/agentic/guides/modules/storage_s3.md +23 -3
- package/dist/agentic/guides/modules/sync_akeneo.md +23 -3
- package/dist/agentic/guides/modules/sync_excel.md +19 -2
- package/dist/agentic/guides/modules/system_status_overlays.md +19 -2
- package/dist/agentic/guides/modules/translations.md +21 -2
- package/dist/agentic/guides/modules/webhooks.md +29 -4
- package/dist/agentic/guides/modules/wms.md +35 -3
- package/dist/agentic/guides/modules/workflows.md +42 -3
- package/dist/agentic/guides/upstream/manifest.json +1 -1
- package/dist/agentic/shared/ai/harness/README.md +4 -4
- package/dist/agentic/shared/ai/harness/RELEASE.md +2 -2
- package/dist/agentic/shared/ai/harness/cases.json +334 -467
- package/dist/agentic/shared/ai/harness/cases.schema.json +1 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +4 -4
- package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +53 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +62 -0
- package/dist/agentic/shared/ai/harness/release-matrix.json +24 -0
- package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +192 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +1 -1
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +4 -4
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +1 -1
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +34 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +4 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +11 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +31 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +39 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +43 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +11 -0
- package/dist/agentic/shared/ai/skills/tiers.json +1 -0
- package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +19 -6
- package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +163 -31
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +26 -13
- package/dist/lib/generators/module-facts.js +197 -9
- package/dist/lib/generators/module-facts.js.map +2 -2
- package/dist/lib/generators/module-registry.js +5 -1
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/mercato.js +8 -0
- package/dist/mercato.js.map +2 -2
- package/package.json +5 -5
- package/src/lib/generators/__tests__/module-facts.source-links.test.ts +119 -0
- package/src/lib/generators/module-facts.ts +271 -7
- package/src/lib/generators/module-registry.ts +7 -1
- package/src/mercato.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6749.1.6b54c56dfe",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"@mikro-orm/decorators": "^7.1.5",
|
|
61
61
|
"@mikro-orm/migrations": "^7.1.5",
|
|
62
62
|
"@mikro-orm/postgresql": "^7.1.5",
|
|
63
|
-
"@open-mercato/queue": "0.6.7-develop.
|
|
64
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
63
|
+
"@open-mercato/queue": "0.6.7-develop.6749.1.6b54c56dfe",
|
|
64
|
+
"@open-mercato/shared": "0.6.7-develop.6749.1.6b54c56dfe",
|
|
65
65
|
"cross-spawn": "^7.0.6",
|
|
66
66
|
"pg": "8.22.0",
|
|
67
67
|
"semver": "^7.8.5",
|
|
@@ -72,10 +72,10 @@
|
|
|
72
72
|
"typescript-js": "npm:typescript@6.0.3"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
75
|
+
"@open-mercato/shared": "0.6.7-develop.6749.1.6b54c56dfe"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
78
|
+
"@open-mercato/shared": "0.6.7-develop.6749.1.6b54c56dfe",
|
|
79
79
|
"@types/jest": "^30.0.0",
|
|
80
80
|
"jest": "^30.4.2",
|
|
81
81
|
"ts-jest": "^29.4.11"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { extractModuleFacts, renderModuleFactsMarkdown } from '../module-facts'
|
|
5
|
+
|
|
6
|
+
function writeFixture(root: string, relativePath: string, source: string): void {
|
|
7
|
+
const filePath = path.join(root, relativePath)
|
|
8
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
9
|
+
fs.writeFileSync(filePath, source)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('module-facts source-linked extension surfaces', () => {
|
|
13
|
+
let temporaryRoot: string
|
|
14
|
+
let moduleRoot: string
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'module-facts-source-links-'))
|
|
18
|
+
moduleRoot = path.join(temporaryRoot, 'facts')
|
|
19
|
+
writeFixture(moduleRoot, 'index.ts', "export const metadata = { title: 'Facts' }\n")
|
|
20
|
+
writeFixture(
|
|
21
|
+
moduleRoot,
|
|
22
|
+
'api/records/route.ts',
|
|
23
|
+
"export const metadata = { path: '/facts/records-custom' }\nexport async function GET() {}\n",
|
|
24
|
+
)
|
|
25
|
+
writeFixture(moduleRoot, 'backend/page.tsx', 'export default function FactsPage() { return null }\n')
|
|
26
|
+
writeFixture(
|
|
27
|
+
moduleRoot,
|
|
28
|
+
'backend/facts/items/[id]/page.tsx',
|
|
29
|
+
'export default function FactItemPage() { return null }\n',
|
|
30
|
+
)
|
|
31
|
+
writeFixture(
|
|
32
|
+
moduleRoot,
|
|
33
|
+
'frontend/facts/portal/[id]/page.tsx',
|
|
34
|
+
'export default function FactPortalPage() { return null }\n',
|
|
35
|
+
)
|
|
36
|
+
writeFixture(
|
|
37
|
+
moduleRoot,
|
|
38
|
+
'cli.ts',
|
|
39
|
+
"const commands = [{ command: 'facts:sync' }, { command: 'facts:check' }]\nexport default commands\n",
|
|
40
|
+
)
|
|
41
|
+
writeFixture(
|
|
42
|
+
moduleRoot,
|
|
43
|
+
'ai-tools/tools-pack.ts',
|
|
44
|
+
"type FactsAiToolDefinition = { name: string; description: string }\nconst inspectTool: FactsAiToolDefinition = { name: 'facts.inspect', description: 'Inspect facts' }\nexport default [inspectTool]\n",
|
|
45
|
+
)
|
|
46
|
+
writeFixture(
|
|
47
|
+
moduleRoot,
|
|
48
|
+
'ai-tools.ts',
|
|
49
|
+
"type FactsAiToolDefinition = { name: string; description: string }\nconst listTool: FactsAiToolDefinition = { name: 'facts.list', description: 'List facts' }\nexport const aiTools = [listTool]\nexport default aiTools\n",
|
|
50
|
+
)
|
|
51
|
+
writeFixture(
|
|
52
|
+
moduleRoot,
|
|
53
|
+
'ai-agents.ts',
|
|
54
|
+
"type AiAgentDefinition = { id: string; label: string }\nconst AGENT_ID = 'facts.assistant'\nconst agent: AiAgentDefinition = { id: AGENT_ID, label: 'Facts assistant' }\nexport const aiAgents = [agent]\nexport default aiAgents\n",
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
afterAll(() => {
|
|
59
|
+
fs.rmSync(temporaryRoot, { recursive: true, force: true })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('emits portable source links for routes, pages, commands, tools, and agents', () => {
|
|
63
|
+
const registrySource = `export const modules = [{
|
|
64
|
+
id: 'facts',
|
|
65
|
+
apis: [{
|
|
66
|
+
path: '/facts/records-custom',
|
|
67
|
+
handlers: { GET: async () => undefined },
|
|
68
|
+
metadata: { GET: { requireFeatures: ['facts.view'] } },
|
|
69
|
+
}],
|
|
70
|
+
}]`
|
|
71
|
+
const facts = extractModuleFacts({
|
|
72
|
+
moduleId: 'facts',
|
|
73
|
+
moduleRoot,
|
|
74
|
+
sourcePackage: '@open-mercato/example',
|
|
75
|
+
sourceVersion: '1.2.3',
|
|
76
|
+
registrySource,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
expect(facts.sourceRoot).toBe('node_modules/@open-mercato/example/src/modules/facts')
|
|
80
|
+
expect(facts.apiRoutes).toEqual([
|
|
81
|
+
{
|
|
82
|
+
path: '/facts/records-custom',
|
|
83
|
+
methods: ['GET'],
|
|
84
|
+
auth: { GET: { requireFeatures: ['facts.view'] } },
|
|
85
|
+
sourcePath: 'node_modules/@open-mercato/example/src/modules/facts/api/records/route.ts',
|
|
86
|
+
},
|
|
87
|
+
])
|
|
88
|
+
expect(facts.backendPages).toEqual([
|
|
89
|
+
{
|
|
90
|
+
path: '/backend/facts',
|
|
91
|
+
sourcePath: 'node_modules/@open-mercato/example/src/modules/facts/backend/page.tsx',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
path: '/backend/facts/items/[id]',
|
|
95
|
+
sourcePath: 'node_modules/@open-mercato/example/src/modules/facts/backend/facts/items/[id]/page.tsx',
|
|
96
|
+
},
|
|
97
|
+
])
|
|
98
|
+
expect(facts.frontendPages).toEqual([
|
|
99
|
+
{
|
|
100
|
+
path: '/facts/portal/[id]',
|
|
101
|
+
sourcePath: 'node_modules/@open-mercato/example/src/modules/facts/frontend/facts/portal/[id]/page.tsx',
|
|
102
|
+
},
|
|
103
|
+
])
|
|
104
|
+
expect(facts.cli).toEqual(['facts:sync', 'facts:check'])
|
|
105
|
+
expect(facts.cliCommands.map((command) => command.command)).toEqual(['facts:sync', 'facts:check'])
|
|
106
|
+
expect(facts.aiTools.map((tool) => tool.name)).toEqual(['facts.inspect', 'facts.list'])
|
|
107
|
+
expect(facts.aiAgents.map((agentFact) => agentFact.id)).toEqual(['facts.assistant'])
|
|
108
|
+
|
|
109
|
+
const markdown = renderModuleFactsMarkdown(facts)
|
|
110
|
+
expect(markdown).toContain('## Backend pages')
|
|
111
|
+
expect(markdown).toContain('## Frontend pages')
|
|
112
|
+
expect(markdown).toContain('## CLI commands')
|
|
113
|
+
expect(markdown).toContain('## AI tools / MCP capabilities')
|
|
114
|
+
expect(markdown).toContain('## AI agents')
|
|
115
|
+
expect(markdown).toContain(
|
|
116
|
+
'(../../../node_modules/@open-mercato/example/src/modules/facts/api/records/route.ts)',
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -21,6 +21,27 @@ export interface ModuleApiRouteFact {
|
|
|
21
21
|
path: string
|
|
22
22
|
methods: string[]
|
|
23
23
|
auth: Record<string, ApiRouteAuthRule>
|
|
24
|
+
sourcePath: string | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ModulePageFact {
|
|
28
|
+
path: string
|
|
29
|
+
sourcePath: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ModuleCliCommandFact {
|
|
33
|
+
command: string
|
|
34
|
+
sourcePath: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ModuleAiToolFact {
|
|
38
|
+
name: string
|
|
39
|
+
sourcePath: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ModuleAiAgentFact {
|
|
43
|
+
id: string
|
|
44
|
+
sourcePath: string
|
|
24
45
|
}
|
|
25
46
|
|
|
26
47
|
export interface ModuleEventFact {
|
|
@@ -42,6 +63,7 @@ export interface ModuleFacts {
|
|
|
42
63
|
coreVersion: string | null
|
|
43
64
|
sourcePackage: string | null
|
|
44
65
|
sourceVersion: string | null
|
|
66
|
+
sourceRoot: string
|
|
45
67
|
entities: ModuleEntityFact[]
|
|
46
68
|
events: ModuleEventFact[]
|
|
47
69
|
aclFeatures: string[]
|
|
@@ -51,6 +73,11 @@ export interface ModuleFacts {
|
|
|
51
73
|
hostTokens: ModuleHostTokens
|
|
52
74
|
notifications: string[]
|
|
53
75
|
cli: string[]
|
|
76
|
+
backendPages: ModulePageFact[]
|
|
77
|
+
frontendPages: ModulePageFact[]
|
|
78
|
+
cliCommands: ModuleCliCommandFact[]
|
|
79
|
+
aiTools: ModuleAiToolFact[]
|
|
80
|
+
aiAgents: ModuleAiAgentFact[]
|
|
54
81
|
warnings: string[]
|
|
55
82
|
}
|
|
56
83
|
|
|
@@ -495,13 +522,14 @@ function parseApiRouteEntry(entryLiteral: ts.ObjectLiteralExpression): ModuleApi
|
|
|
495
522
|
}
|
|
496
523
|
}
|
|
497
524
|
|
|
498
|
-
return { path: routePath, methods, auth }
|
|
525
|
+
return { path: routePath, methods, auth, sourcePath: null }
|
|
499
526
|
}
|
|
500
527
|
|
|
501
528
|
function extractApiRoutes(
|
|
502
529
|
moduleId: string,
|
|
503
530
|
registrySource: string | null,
|
|
504
531
|
registryDescription: string,
|
|
532
|
+
routeSourcePaths: ReadonlyMap<string, string>,
|
|
505
533
|
warnings: string[],
|
|
506
534
|
): ModuleApiRouteFact[] {
|
|
507
535
|
if (registrySource == null) {
|
|
@@ -529,7 +557,7 @@ function extractApiRoutes(
|
|
|
529
557
|
const route = parseApiRouteEntry(element)
|
|
530
558
|
if (route && !seenPaths.has(route.path)) {
|
|
531
559
|
seenPaths.add(route.path)
|
|
532
|
-
routes.push(route)
|
|
560
|
+
routes.push({ ...route, sourcePath: routeSourcePaths.get(route.path) ?? null })
|
|
533
561
|
}
|
|
534
562
|
}
|
|
535
563
|
}
|
|
@@ -717,6 +745,190 @@ function listSourceFilesRecursive(directory: string): string[] {
|
|
|
717
745
|
return files
|
|
718
746
|
}
|
|
719
747
|
|
|
748
|
+
function toPortableSourceRoot(moduleId: string, sourcePackage: string | null): string {
|
|
749
|
+
return path.posix.join('node_modules', sourcePackage ?? '@open-mercato/core', 'src', 'modules', moduleId)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function toPortableSourcePath(moduleRoot: string, sourceRoot: string, filePath: string): string {
|
|
753
|
+
const relativePath = path.relative(moduleRoot, filePath).split(path.sep).join('/')
|
|
754
|
+
return path.posix.join(sourceRoot, relativePath)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function sourceHasDefaultExport(sourceFile: ts.SourceFile): boolean {
|
|
758
|
+
return sourceFile.statements.some((statement) => {
|
|
759
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals) return true
|
|
760
|
+
if (!ts.isFunctionDeclaration(statement) && !ts.isClassDeclaration(statement)) return false
|
|
761
|
+
return statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
|
|
762
|
+
})
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function readRouteMetadataPath(sourceFile: ts.SourceFile): string | null {
|
|
766
|
+
const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
|
|
767
|
+
return metadata ? readStringPropertyInitializer(metadata, 'path') ?? null : null
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function extractApiRouteSourcePaths(
|
|
771
|
+
moduleId: string,
|
|
772
|
+
moduleRoot: string,
|
|
773
|
+
sourceRoot: string,
|
|
774
|
+
): Map<string, string> {
|
|
775
|
+
const apiRoot = path.join(moduleRoot, 'api')
|
|
776
|
+
const sources = new Map<string, string>()
|
|
777
|
+
if (!fs.existsSync(apiRoot)) return sources
|
|
778
|
+
const methodDirectories = new Set(['get', 'post', 'put', 'patch', 'delete'])
|
|
779
|
+
|
|
780
|
+
for (const filePath of listSourceFilesRecursive(apiRoot)) {
|
|
781
|
+
const relativePath = path.relative(apiRoot, filePath).split(path.sep).join('/')
|
|
782
|
+
const segments = relativePath.split('/')
|
|
783
|
+
const fileName = segments.pop() as string
|
|
784
|
+
const fileStem = fileName.replace(/\.tsx?$/, '')
|
|
785
|
+
let routeSegments: string[]
|
|
786
|
+
if (fileStem === 'route') {
|
|
787
|
+
routeSegments = segments
|
|
788
|
+
} else if (segments[0] && methodDirectories.has(segments[0].toLowerCase())) {
|
|
789
|
+
routeSegments = [...segments.slice(1), fileStem]
|
|
790
|
+
} else {
|
|
791
|
+
routeSegments = [...segments, fileStem]
|
|
792
|
+
}
|
|
793
|
+
const defaultPath = `/${[moduleId, ...routeSegments].filter(Boolean).join('/')}`
|
|
794
|
+
const sourceFile = readSourceFile(filePath)
|
|
795
|
+
const routePath = sourceFile ? readRouteMetadataPath(sourceFile) ?? defaultPath : defaultPath
|
|
796
|
+
sources.set(routePath, toPortableSourcePath(moduleRoot, sourceRoot, filePath))
|
|
797
|
+
}
|
|
798
|
+
return sources
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function extractModulePages(
|
|
802
|
+
moduleId: string,
|
|
803
|
+
moduleRoot: string,
|
|
804
|
+
sourceRoot: string,
|
|
805
|
+
surface: 'backend' | 'frontend',
|
|
806
|
+
): ModulePageFact[] {
|
|
807
|
+
const pageRoot = path.join(moduleRoot, surface)
|
|
808
|
+
if (!fs.existsSync(pageRoot)) return []
|
|
809
|
+
const pages: ModulePageFact[] = []
|
|
810
|
+
const seen = new Set<string>()
|
|
811
|
+
|
|
812
|
+
for (const filePath of listSourceFilesRecursive(pageRoot)) {
|
|
813
|
+
if (!filePath.endsWith('.tsx')) continue
|
|
814
|
+
const relativePath = path.relative(pageRoot, filePath).split(path.sep).join('/')
|
|
815
|
+
const segments = relativePath.split('/')
|
|
816
|
+
const fileName = segments.pop() as string
|
|
817
|
+
const fileStem = fileName.replace(/\.tsx$/, '')
|
|
818
|
+
const isModernPage = fileStem === 'page'
|
|
819
|
+
if (!isModernPage && (fileStem.endsWith('.meta') || /^[A-Z]/.test(fileStem))) continue
|
|
820
|
+
const sourceFile = readSourceFile(filePath)
|
|
821
|
+
if (!sourceFile || !sourceHasDefaultExport(sourceFile)) continue
|
|
822
|
+
const routeSegments = isModernPage ? segments : [...segments, fileStem]
|
|
823
|
+
const routePath = surface === 'frontend'
|
|
824
|
+
? `/${routeSegments.filter(Boolean).join('/')}`
|
|
825
|
+
: isModernPage
|
|
826
|
+
? `/backend/${routeSegments.join('/') || moduleId}`
|
|
827
|
+
: `/backend/${routeSegments[0] === moduleId
|
|
828
|
+
? routeSegments.filter(Boolean).join('/')
|
|
829
|
+
: [moduleId, ...routeSegments].filter(Boolean).join('/')}`
|
|
830
|
+
if (seen.has(routePath)) continue
|
|
831
|
+
seen.add(routePath)
|
|
832
|
+
pages.push({
|
|
833
|
+
path: routePath,
|
|
834
|
+
sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, filePath),
|
|
835
|
+
})
|
|
836
|
+
}
|
|
837
|
+
return pages.sort((left, right) => left.path.localeCompare(right.path))
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function findAncestorVariableDeclaration(node: ts.Node): ts.VariableDeclaration | null {
|
|
841
|
+
let current: ts.Node | undefined = node.parent
|
|
842
|
+
while (current) {
|
|
843
|
+
if (ts.isVariableDeclaration(current)) return current
|
|
844
|
+
current = current.parent
|
|
845
|
+
}
|
|
846
|
+
return null
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function findAncestorCallExpression(node: ts.Node): ts.CallExpression | null {
|
|
850
|
+
let current: ts.Node | undefined = node.parent
|
|
851
|
+
while (current) {
|
|
852
|
+
if (ts.isCallExpression(current)) return current
|
|
853
|
+
if (ts.isVariableDeclaration(current) || ts.isStatement(current)) return null
|
|
854
|
+
current = current.parent
|
|
855
|
+
}
|
|
856
|
+
return null
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function extractNamedAiFacts(
|
|
860
|
+
filePaths: readonly string[],
|
|
861
|
+
moduleRoot: string,
|
|
862
|
+
sourceRoot: string,
|
|
863
|
+
propertyName: 'name' | 'id',
|
|
864
|
+
): Array<{ value: string; sourcePath: string }> {
|
|
865
|
+
const facts: Array<{ value: string; sourcePath: string }> = []
|
|
866
|
+
const seen = new Set<string>()
|
|
867
|
+
for (const filePath of filePaths) {
|
|
868
|
+
const sourceFile = readSourceFile(filePath)
|
|
869
|
+
if (!sourceFile) continue
|
|
870
|
+
const initializers = buildVariableInitializerMap(sourceFile)
|
|
871
|
+
const readDefinitionValue = (objectLiteral: ts.ObjectLiteralExpression): string | undefined => {
|
|
872
|
+
const initializer = getObjectPropertyInitializer(objectLiteral, propertyName)
|
|
873
|
+
if (!initializer) return undefined
|
|
874
|
+
const resolved = unwrapExpression(initializer)
|
|
875
|
+
if (ts.isStringLiteralLike(resolved)) return resolved.text
|
|
876
|
+
if (ts.isIdentifier(resolved)) {
|
|
877
|
+
const declarationInitializer = initializers.get(resolved.text)
|
|
878
|
+
if (declarationInitializer) {
|
|
879
|
+
const declarationValue = unwrapExpression(declarationInitializer)
|
|
880
|
+
if (ts.isStringLiteralLike(declarationValue)) return declarationValue.text
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return undefined
|
|
884
|
+
}
|
|
885
|
+
const visit = (node: ts.Node): void => {
|
|
886
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
887
|
+
const value = readDefinitionValue(node)
|
|
888
|
+
if (value && !seen.has(value)) {
|
|
889
|
+
const declaration = findAncestorVariableDeclaration(node)
|
|
890
|
+
const declarationName = declaration && ts.isIdentifier(declaration.name) ? declaration.name.text : ''
|
|
891
|
+
const declarationType = declaration?.type?.getText(sourceFile) ?? ''
|
|
892
|
+
const call = findAncestorCallExpression(node)
|
|
893
|
+
const callee = call?.expression.getText(sourceFile) ?? ''
|
|
894
|
+
const isDefinition = propertyName === 'name'
|
|
895
|
+
? /AiTool/.test(declarationType) || /tool$/i.test(declarationName) || /AiTool/.test(callee)
|
|
896
|
+
: /AiAgentDefinition/.test(declarationType) || /agent$/i.test(declarationName) || /defineAiAgent/.test(callee)
|
|
897
|
+
if (isDefinition) {
|
|
898
|
+
seen.add(value)
|
|
899
|
+
facts.push({
|
|
900
|
+
value,
|
|
901
|
+
sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, filePath),
|
|
902
|
+
})
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
node.forEachChild(visit)
|
|
907
|
+
}
|
|
908
|
+
sourceFile.forEachChild(visit)
|
|
909
|
+
}
|
|
910
|
+
return facts.sort((left, right) => left.value.localeCompare(right.value))
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function extractAiTools(moduleRoot: string, sourceRoot: string): ModuleAiToolFact[] {
|
|
914
|
+
const files = new Set<string>()
|
|
915
|
+
const rootFile = resolveConventionFile(moduleRoot, 'ai-tools')
|
|
916
|
+
if (rootFile) files.add(rootFile)
|
|
917
|
+
const toolsDirectory = path.join(moduleRoot, 'ai-tools')
|
|
918
|
+
if (fs.existsSync(toolsDirectory)) {
|
|
919
|
+
for (const filePath of listSourceFilesRecursive(toolsDirectory)) files.add(filePath)
|
|
920
|
+
}
|
|
921
|
+
return extractNamedAiFacts([...files], moduleRoot, sourceRoot, 'name')
|
|
922
|
+
.map((fact) => ({ name: fact.value, sourcePath: fact.sourcePath }))
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function extractAiAgents(moduleRoot: string, sourceRoot: string): ModuleAiAgentFact[] {
|
|
926
|
+
const agentsFile = resolveConventionFile(moduleRoot, 'ai-agents')
|
|
927
|
+
if (!agentsFile) return []
|
|
928
|
+
return extractNamedAiFacts([agentsFile], moduleRoot, sourceRoot, 'id')
|
|
929
|
+
.map((fact) => ({ id: fact.value, sourcePath: fact.sourcePath }))
|
|
930
|
+
}
|
|
931
|
+
|
|
720
932
|
function extractTableIds(moduleRoot: string): string[] {
|
|
721
933
|
if (!fs.existsSync(moduleRoot)) return []
|
|
722
934
|
|
|
@@ -780,6 +992,7 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
780
992
|
if (!moduleRoot) {
|
|
781
993
|
throw new Error(`[internal] extractModuleFacts requires moduleRoot or coreSrcRoot for module "${moduleId}"`)
|
|
782
994
|
}
|
|
995
|
+
const sourceRoot = toPortableSourceRoot(moduleId, sourcePackage)
|
|
783
996
|
|
|
784
997
|
const entitiesFilePath =
|
|
785
998
|
resolveConventionFile(path.join(moduleRoot, 'data'), 'entities') ??
|
|
@@ -802,11 +1015,22 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
802
1015
|
const aclFeatures = extractAclFeatures(aclFilePath)
|
|
803
1016
|
|
|
804
1017
|
const { source: registrySource, description: registryDescription } = resolveRegistrySource(options)
|
|
805
|
-
const
|
|
1018
|
+
const apiRouteSourcePaths = extractApiRouteSourcePaths(moduleId, moduleRoot, sourceRoot)
|
|
1019
|
+
const apiRoutes = extractApiRoutes(moduleId, registrySource, registryDescription, apiRouteSourcePaths, warnings)
|
|
806
1020
|
const diTokens = extractDiTokens(diFilePath)
|
|
807
1021
|
const searchEntities = extractSearchEntities(searchFilePath, warnings)
|
|
808
1022
|
const notifications = extractNotifications(notificationsFilePath, warnings)
|
|
809
1023
|
const cli = extractCli(cliFilePath, warnings)
|
|
1024
|
+
const cliCommands = cliFilePath
|
|
1025
|
+
? cli.map((command) => ({
|
|
1026
|
+
command,
|
|
1027
|
+
sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, cliFilePath),
|
|
1028
|
+
}))
|
|
1029
|
+
: []
|
|
1030
|
+
const backendPages = extractModulePages(moduleId, moduleRoot, sourceRoot, 'backend')
|
|
1031
|
+
const frontendPages = extractModulePages(moduleId, moduleRoot, sourceRoot, 'frontend')
|
|
1032
|
+
const aiTools = extractAiTools(moduleRoot, sourceRoot)
|
|
1033
|
+
const aiAgents = extractAiAgents(moduleRoot, sourceRoot)
|
|
810
1034
|
const hostTokens: ModuleHostTokens = {
|
|
811
1035
|
entityIds: extractHostEntityIds(entities),
|
|
812
1036
|
tableIds: extractTableIds(moduleRoot),
|
|
@@ -819,6 +1043,7 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
819
1043
|
coreVersion,
|
|
820
1044
|
sourcePackage,
|
|
821
1045
|
sourceVersion,
|
|
1046
|
+
sourceRoot,
|
|
822
1047
|
entities,
|
|
823
1048
|
events,
|
|
824
1049
|
aclFeatures,
|
|
@@ -828,6 +1053,11 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
828
1053
|
hostTokens,
|
|
829
1054
|
notifications,
|
|
830
1055
|
cli,
|
|
1056
|
+
backendPages,
|
|
1057
|
+
frontendPages,
|
|
1058
|
+
cliCommands,
|
|
1059
|
+
aiTools,
|
|
1060
|
+
aiAgents,
|
|
831
1061
|
warnings,
|
|
832
1062
|
}
|
|
833
1063
|
}
|
|
@@ -844,6 +1074,7 @@ export interface ModuleFactsJsonEntry {
|
|
|
844
1074
|
coreVersion: string | null
|
|
845
1075
|
sourcePackage: string | null
|
|
846
1076
|
sourceVersion: string | null
|
|
1077
|
+
sourceRoot: string
|
|
847
1078
|
entities: ModuleEntityFact[]
|
|
848
1079
|
events: ModuleFactsJsonEvent[]
|
|
849
1080
|
aclFeatures: string[]
|
|
@@ -853,6 +1084,11 @@ export interface ModuleFactsJsonEntry {
|
|
|
853
1084
|
hostTokens: ModuleHostTokens
|
|
854
1085
|
notifications: string[]
|
|
855
1086
|
cli: string[]
|
|
1087
|
+
backendPages: ModulePageFact[]
|
|
1088
|
+
frontendPages: ModulePageFact[]
|
|
1089
|
+
cliCommands: ModuleCliCommandFact[]
|
|
1090
|
+
aiTools: ModuleAiToolFact[]
|
|
1091
|
+
aiAgents: ModuleAiAgentFact[]
|
|
856
1092
|
}
|
|
857
1093
|
|
|
858
1094
|
const EMPTY_SECTION_MARKER = '_none_'
|
|
@@ -894,6 +1130,10 @@ function renderInlineListSection(heading: string, values: string[]): string {
|
|
|
894
1130
|
return `${heading}\n\n${values.join(' · ')}`
|
|
895
1131
|
}
|
|
896
1132
|
|
|
1133
|
+
function renderSourceLink(sourcePath: string): string {
|
|
1134
|
+
return `[${sourcePath}](../../../${sourcePath})`
|
|
1135
|
+
}
|
|
1136
|
+
|
|
897
1137
|
function describeAuthRule(rule: ApiRouteAuthRule | undefined): string {
|
|
898
1138
|
if (!rule) return 'public'
|
|
899
1139
|
if (rule.requireFeatures && rule.requireFeatures.length > 0) return rule.requireFeatures.join(', ')
|
|
@@ -916,14 +1156,23 @@ function renderApiRouteAuthCell(route: ModuleApiRouteFact): string {
|
|
|
916
1156
|
|
|
917
1157
|
function renderApiRoutesSection(routes: ModuleApiRouteFact[]): string {
|
|
918
1158
|
if (routes.length === 0) return `## API routes\n\n${EMPTY_SECTION_MARKER}`
|
|
919
|
-
const header = '| Path | Methods | Auth (per-method requireFeatures) |'
|
|
920
|
-
const divider = '
|
|
1159
|
+
const header = '| Path | Methods | Auth (per-method requireFeatures) | Source |'
|
|
1160
|
+
const divider = '|---|---|---|---|'
|
|
921
1161
|
const rows = routes.map(
|
|
922
|
-
(route) => `| ${route.path} | ${route.methods.join(' ')} | ${renderApiRouteAuthCell(route)} |`,
|
|
1162
|
+
(route) => `| ${route.path} | ${route.methods.join(' ')} | ${renderApiRouteAuthCell(route)} | ${route.sourcePath ? renderSourceLink(route.sourcePath) : '—'} |`,
|
|
923
1163
|
)
|
|
924
1164
|
return ['## API routes', '', header, divider, ...rows].join('\n')
|
|
925
1165
|
}
|
|
926
1166
|
|
|
1167
|
+
function renderLinkedFactsSection(
|
|
1168
|
+
heading: string,
|
|
1169
|
+
facts: ReadonlyArray<{ label: string; sourcePath: string }>,
|
|
1170
|
+
): string {
|
|
1171
|
+
if (facts.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
|
|
1172
|
+
const rows = facts.map((fact) => `| ${fact.label} | ${renderSourceLink(fact.sourcePath)} |`)
|
|
1173
|
+
return [heading, '', '| ID / path | Source |', '|---|---|', ...rows].join('\n')
|
|
1174
|
+
}
|
|
1175
|
+
|
|
927
1176
|
function renderHostTokensSection(hostTokens: ModuleHostTokens): string {
|
|
928
1177
|
const entityIdsLine = hostTokens.entityIds.length > 0 ? hostTokens.entityIds.join(' · ') : EMPTY_SECTION_MARKER
|
|
929
1178
|
const tableIdsLine = hostTokens.tableIds.length > 0 ? hostTokens.tableIds.join(' · ') : EMPTY_SECTION_MARKER
|
|
@@ -934,6 +1183,7 @@ export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
|
|
|
934
1183
|
const sections = [
|
|
935
1184
|
`# ${facts.module} — module facts (generated, do not edit)`,
|
|
936
1185
|
renderVersionStamp(facts.coreVersion, facts.sourcePackage, facts.sourceVersion),
|
|
1186
|
+
`Source root: ${renderSourceLink(facts.sourceRoot)}`,
|
|
937
1187
|
'',
|
|
938
1188
|
renderEntitiesSection(facts.entities),
|
|
939
1189
|
'',
|
|
@@ -943,6 +1193,10 @@ export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
|
|
|
943
1193
|
'',
|
|
944
1194
|
renderApiRoutesSection(facts.apiRoutes),
|
|
945
1195
|
'',
|
|
1196
|
+
renderLinkedFactsSection('## Backend pages', facts.backendPages.map((page) => ({ label: page.path, sourcePath: page.sourcePath }))),
|
|
1197
|
+
'',
|
|
1198
|
+
renderLinkedFactsSection('## Frontend pages', facts.frontendPages.map((page) => ({ label: page.path, sourcePath: page.sourcePath }))),
|
|
1199
|
+
'',
|
|
946
1200
|
renderInlineListSection('## DI service tokens', facts.diTokens),
|
|
947
1201
|
'',
|
|
948
1202
|
renderInlineListSection('## Search entities', facts.searchEntities),
|
|
@@ -951,7 +1205,11 @@ export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
|
|
|
951
1205
|
'',
|
|
952
1206
|
renderInlineListSection('## Notifications', facts.notifications),
|
|
953
1207
|
'',
|
|
954
|
-
|
|
1208
|
+
renderLinkedFactsSection('## CLI commands', facts.cliCommands.map((command) => ({ label: command.command, sourcePath: command.sourcePath }))),
|
|
1209
|
+
'',
|
|
1210
|
+
renderLinkedFactsSection('## AI tools / MCP capabilities', facts.aiTools.map((tool) => ({ label: tool.name, sourcePath: tool.sourcePath }))),
|
|
1211
|
+
'',
|
|
1212
|
+
renderLinkedFactsSection('## AI agents', facts.aiAgents.map((agent) => ({ label: agent.id, sourcePath: agent.sourcePath }))),
|
|
955
1213
|
'',
|
|
956
1214
|
]
|
|
957
1215
|
return sections.join('\n')
|
|
@@ -964,6 +1222,7 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
|
|
|
964
1222
|
coreVersion: facts.coreVersion,
|
|
965
1223
|
sourcePackage: facts.sourcePackage,
|
|
966
1224
|
sourceVersion: facts.sourceVersion,
|
|
1225
|
+
sourceRoot: facts.sourceRoot,
|
|
967
1226
|
entities: facts.entities,
|
|
968
1227
|
events: facts.events.map((event) => ({ id: event.id, category: event.category, entity: event.entity })),
|
|
969
1228
|
aclFeatures: facts.aclFeatures,
|
|
@@ -973,6 +1232,11 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
|
|
|
973
1232
|
hostTokens: facts.hostTokens,
|
|
974
1233
|
notifications: facts.notifications,
|
|
975
1234
|
cli: facts.cli,
|
|
1235
|
+
backendPages: facts.backendPages,
|
|
1236
|
+
frontendPages: facts.frontendPages,
|
|
1237
|
+
cliCommands: facts.cliCommands,
|
|
1238
|
+
aiTools: facts.aiTools,
|
|
1239
|
+
aiAgents: facts.aiAgents,
|
|
976
1240
|
}
|
|
977
1241
|
}
|
|
978
1242
|
|
|
@@ -224,6 +224,8 @@ type SerializableWorkerMetadata = {
|
|
|
224
224
|
id?: string
|
|
225
225
|
queue?: string
|
|
226
226
|
concurrency?: number
|
|
227
|
+
lockDuration?: number
|
|
228
|
+
maxStalledCount?: number
|
|
227
229
|
}
|
|
228
230
|
|
|
229
231
|
type PageMetadataManifestLoadResult = {
|
|
@@ -1553,6 +1555,8 @@ function normalizeWorkerMetadata(raw: unknown): SerializableWorkerMetadata | nul
|
|
|
1553
1555
|
if (typeof source.id === 'string' && source.id.length > 0) normalized.id = source.id
|
|
1554
1556
|
if (typeof source.queue === 'string' && source.queue.length > 0) normalized.queue = source.queue
|
|
1555
1557
|
if (typeof source.concurrency === 'number') normalized.concurrency = source.concurrency
|
|
1558
|
+
if (typeof source.lockDuration === 'number') normalized.lockDuration = source.lockDuration
|
|
1559
|
+
if (typeof source.maxStalledCount === 'number') normalized.maxStalledCount = source.maxStalledCount
|
|
1556
1560
|
return Object.keys(normalized).length > 0 ? normalized : null
|
|
1557
1561
|
}
|
|
1558
1562
|
|
|
@@ -2055,7 +2059,7 @@ function processWorkers(discovered: DiscoveredWorker[]): string[] {
|
|
|
2055
2059
|
for (const { id, importPath, metadata } of discovered) {
|
|
2056
2060
|
const workerId = metadata.id ?? id
|
|
2057
2061
|
workers.push(
|
|
2058
|
-
`{ id: ${toLiteral(workerId)}, queue: ${toLiteral(metadata.queue)}, concurrency: ${toLiteral(metadata.concurrency ?? 1)}, handler: createLazyModuleWorker(() => ${buildDynamicImportExpression(importPath)}, ${toLiteral(workerId)}) }`
|
|
2062
|
+
`{ id: ${toLiteral(workerId)}, queue: ${toLiteral(metadata.queue)}, concurrency: ${toLiteral(metadata.concurrency ?? 1)}${metadata.lockDuration === undefined ? '' : `, lockDuration: ${toLiteral(metadata.lockDuration)}`}${metadata.maxStalledCount === undefined ? '' : `, maxStalledCount: ${toLiteral(metadata.maxStalledCount)}`}, handler: createLazyModuleWorker(() => ${buildDynamicImportExpression(importPath)}, ${toLiteral(workerId)}) }`
|
|
2059
2063
|
)
|
|
2060
2064
|
}
|
|
2061
2065
|
return workers
|
|
@@ -2730,6 +2734,8 @@ function processWorkersAst(discovered: DiscoveredWorker[]): WriterFunction[] {
|
|
|
2730
2734
|
{ name: 'id', value: workerId },
|
|
2731
2735
|
{ name: 'queue', value: metadata.queue },
|
|
2732
2736
|
{ name: 'concurrency', value: metadata.concurrency ?? 1 },
|
|
2737
|
+
...(metadata.lockDuration === undefined ? [] : [{ name: 'lockDuration', value: metadata.lockDuration }]),
|
|
2738
|
+
...(metadata.maxStalledCount === undefined ? [] : [{ name: 'maxStalledCount', value: metadata.maxStalledCount }]),
|
|
2733
2739
|
{
|
|
2734
2740
|
name: 'handler',
|
|
2735
2741
|
value: callExpression(identifier('createLazyModuleWorker'), [
|
package/src/mercato.ts
CHANGED
|
@@ -1654,6 +1654,8 @@ export async function run(argv = process.argv) {
|
|
|
1654
1654
|
effectiveByQueue.get(queue) ??
|
|
1655
1655
|
concurrencyOverride ??
|
|
1656
1656
|
Math.max(...queueWorkers.map((w) => w.concurrency), 1)
|
|
1657
|
+
const maxStalledCount = Math.max(...queueWorkers.map((w) => w.maxStalledCount ?? 1), 1)
|
|
1658
|
+
const lockDuration = Math.max(...queueWorkers.map((w) => w.lockDuration ?? 0), 0) || undefined
|
|
1657
1659
|
|
|
1658
1660
|
console.log(`[worker] Starting "${queue}" with ${queueWorkers.length} handler(s), concurrency: ${concurrency}`)
|
|
1659
1661
|
|
|
@@ -1662,6 +1664,8 @@ export async function run(argv = process.argv) {
|
|
|
1662
1664
|
queueName: queue,
|
|
1663
1665
|
connection: queueRedisUrl ? { url: queueRedisUrl } : undefined,
|
|
1664
1666
|
concurrency,
|
|
1667
|
+
lockDuration,
|
|
1668
|
+
maxStalledCount,
|
|
1665
1669
|
background: true,
|
|
1666
1670
|
handler: createPerJobWorkerHandler(queueWorkers, createRequestContainer),
|
|
1667
1671
|
})
|
|
@@ -1705,6 +1709,8 @@ export async function run(argv = process.argv) {
|
|
|
1705
1709
|
// never check out more pooled connections than the worker pool holds.
|
|
1706
1710
|
const budgetPlan = await resolveWorkerBudgetPlan([{ queue: queueName!, concurrency: requested }])
|
|
1707
1711
|
const concurrency = budgetPlan.entries[0]?.effective ?? requested
|
|
1712
|
+
const maxStalledCount = Math.max(...queueWorkers.map((w) => w.maxStalledCount ?? 1), 1)
|
|
1713
|
+
const lockDuration = Math.max(...queueWorkers.map((w) => w.lockDuration ?? 0), 0) || undefined
|
|
1708
1714
|
|
|
1709
1715
|
console.log(`[worker] Found ${queueWorkers.length} worker(s) for queue "${queueName}"`)
|
|
1710
1716
|
|
|
@@ -1713,6 +1719,8 @@ export async function run(argv = process.argv) {
|
|
|
1713
1719
|
queueName: queueName!,
|
|
1714
1720
|
connection: queueRedisUrl ? { url: queueRedisUrl } : undefined,
|
|
1715
1721
|
concurrency,
|
|
1722
|
+
lockDuration,
|
|
1723
|
+
maxStalledCount,
|
|
1716
1724
|
handler: createPerJobWorkerHandler(queueWorkers, createRequestContainer),
|
|
1717
1725
|
})
|
|
1718
1726
|
} else {
|