@open-mercato/cli 0.6.5-develop.5337.1.534b781eac → 0.6.5

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.
@@ -1,4 +1,4 @@
1
- [build:cli] found 87 entry points
1
+ [build:cli] found 89 entry points
2
2
  Copied create-app/agentic/ → dist/agentic/
3
3
  Discovered 16 standalone guides → dist/agentic/guides/
4
4
  [build:cli] built successfully
@@ -39,7 +39,7 @@ When the developer reports a problem, follow this order:
39
39
 
40
40
  ### Step 2: Check Generated Files
41
41
 
42
- Run these commands first — they fix 60%+ of issues:
42
+ These commands fix 60%+ of issues, so they are usually the first fix to propose (they are mutating — propose, then run after confirmation per [Step 4](#step-4-propose-before-fixing)):
43
43
 
44
44
  ```bash
45
45
  yarn generate # Regenerate module discovery files
@@ -61,6 +61,28 @@ ls .mercato/generated/
61
61
  yarn typecheck
62
62
  ```
63
63
 
64
+ ### Step 4: Propose Before Fixing
65
+
66
+ Once you have diagnosed the root cause, **do not apply the fix immediately**. First present:
67
+
68
+ 1. The **root cause** — what is actually broken and why.
69
+ 2. The **proposed fix** — the exact commands and/or code changes you intend to apply.
70
+
71
+ Then **wait for explicit user confirmation** before applying any **mutating** change. This keeps the developer in control and avoids surprise edits, migrations, or restarts.
72
+
73
+ **Read-only diagnostics may run without asking** — they only gather information and change nothing:
74
+
75
+ | Allowed without confirmation (read-only) | Requires confirmation (mutating) |
76
+ |------------------------------------------|----------------------------------|
77
+ | `yarn typecheck` | `yarn generate` |
78
+ | `grep` / file reads / `ls` | `yarn db:generate` |
79
+ | log / browser-console inspection | `yarn db:migrate` |
80
+ | `docker compose ps` | editing files |
81
+ | `curl` against a running endpoint (GET) | restarting the dev server (`yarn dev`) |
82
+ | | `docker compose up` |
83
+
84
+ When in doubt about whether an action mutates state, treat it as mutating and ask first. Once the user confirms, apply the fix and verify it.
85
+
64
86
  ---
65
87
 
66
88
  ## 2. Module Issues
@@ -76,24 +98,24 @@ yarn typecheck
76
98
  // Must have this entry:
77
99
  { id: '<module_id>', from: '@app' }
78
100
  ```
79
- Fix: Add the entry and run `yarn generate`.
101
+ Proposed fix: Add the entry and run `yarn generate`.
80
102
 
81
103
  2. **Did you run `yarn generate`?**
82
104
  Check if `.mercato/generated/` contains your module's entries.
83
- Fix: Run `yarn generate`.
105
+ Proposed fix: Run `yarn generate`.
84
106
 
85
107
  3. **Is the module folder named correctly?**
86
108
  Must be plural, snake_case: `src/modules/<module_id>/`
87
- Fix: Rename folder to match module ID.
109
+ Proposed fix: Rename folder to match module ID.
88
110
 
89
111
  4. **Does `index.ts` export `metadata`?**
90
112
  ```typescript
91
113
  export const metadata: ModuleInfo = { name: '<module_id>', ... }
92
114
  ```
93
- Fix: Add the metadata export.
115
+ Proposed fix: Add the metadata export.
94
116
 
95
117
  5. **Is the dev server running with latest changes?**
96
- Fix: Restart with `yarn dev`.
118
+ Proposed fix: Restart with `yarn dev`.
97
119
 
98
120
  ### Module loads but pages 404
99
121
 
@@ -104,17 +126,17 @@ yarn typecheck
104
126
  1. **Are backend page files in the right location?**
105
127
  - List page: `backend/page.tsx` (not `backend/index.tsx`)
106
128
  - Detail page: `backend/<entities>/[id].tsx` (bracket notation)
107
- Fix: Rename to match auto-discovery convention.
129
+ Proposed fix: Rename to match auto-discovery convention.
108
130
 
109
131
  2. **Do pages export `metadata` with `requireAuth`?**
110
132
  ```typescript
111
133
  export const metadata = { requireAuth: true, features: ['<module_id>.view'] }
112
134
  ```
113
- Fix: Add metadata export.
135
+ Proposed fix: Add metadata export.
114
136
 
115
137
  3. **Does the user have the required ACL features?**
116
138
  Check `setup.ts` has `defaultRoleFeatures` for the user's role.
117
- Fix: Add features to role defaults, re-run setup.
139
+ Proposed fix: Add features to role defaults, re-run setup.
118
140
 
119
141
  ---
120
142
 
@@ -130,22 +152,22 @@ yarn typecheck
130
152
  ```bash
131
153
  yarn db:generate # Probes/creates migration file
132
154
  ```
133
- Fix: Run `yarn db:generate` to inspect the required migration, then keep only the scoped SQL for your module and update `src/modules/<module_id>/migrations/.snapshot-open-mercato.json`.
155
+ Proposed fix: Run `yarn db:generate` to inspect the required migration, then keep only the scoped SQL for your module and update `src/modules/<module_id>/migrations/.snapshot-open-mercato.json`.
134
156
 
135
157
  2. **Is the entity declared in the right file with the right imports?**
136
158
  Entity classes belong in `src/modules/<module_id>/data/entities.ts` and decorators must come from `@mikro-orm/decorators/legacy`.
137
- Fix: move stale `entities/<Entity>.ts` patterns into `data/entities.ts` and fix the imports before regenerating the migration.
159
+ Proposed fix: move stale `entities/<Entity>.ts` patterns into `data/entities.ts` and fix the imports before regenerating the migration.
138
160
 
139
161
  3. **Did you apply the migration?**
140
162
  ```bash
141
163
  yarn db:migrate # Applies pending migrations
142
164
  ```
143
- Fix: Run `yarn db:migrate`.
165
+ Proposed fix: Run `yarn db:migrate`.
144
166
 
145
167
  4. **Is the migration file correct?**
146
168
  Check `src/modules/<module_id>/migrations/` for the latest migration.
147
169
  Verify it has the expected columns and types.
148
- Fix: If wrong, delete the migration file, fix the entity, and regenerate.
170
+ Proposed fix: If wrong, delete the migration file, fix the entity, and regenerate.
149
171
 
150
172
  ### Migration generation creates unexpected changes
151
173
 
@@ -160,11 +182,11 @@ yarn typecheck
160
182
 
161
183
  2. **Did you modify a core module entity without ejecting?**
162
184
  Never edit `node_modules/@open-mercato/*`.
163
- Fix: Revert changes to node_modules. Use UMES extensions instead, or eject the module.
185
+ Proposed fix: Revert changes to node_modules. Use UMES extensions instead, or eject the module.
164
186
 
165
187
  3. **Is a module snapshot stale?**
166
188
  Check whether the generated SQL recreates a table or column that already has a committed migration.
167
- Fix: update that module's `migrations/.snapshot-open-mercato.json` to include the already-migrated schema, then re-run `yarn db:generate` and expect `no changes`.
189
+ Proposed fix: update that module's `migrations/.snapshot-open-mercato.json` to include the already-migrated schema, then re-run `yarn db:generate` and expect `no changes`.
168
190
 
169
191
  ### Entity changes not reflected
170
192
 
@@ -443,9 +465,10 @@ yarn dev # 5. Restart dev server
443
465
 
444
466
  ## Rules
445
467
 
446
- - **ALWAYS** run `yarn generate` as first diagnostic step
468
+ - **ALWAYS** present the diagnosed root cause and the proposed fix (commands/code), then **wait for explicit user confirmation before applying any mutating change** (see [Step 4](#step-4-propose-before-fixing)). Only read-only diagnostics may run without asking.
447
469
  - **ALWAYS** check server logs / browser console for actual error messages
448
470
  - **NEVER** edit files in `.mercato/generated/` or `node_modules/`
449
471
  - **NEVER** assume the issue — verify with actual error output
472
+ - Treat `yarn generate` as the most likely first fix, but propose it before running — it regenerates files and is a mutating action
450
473
  - Fix the root cause, not the symptom — temporary workarounds become permanent bugs
451
- - When suggesting a fix, include the exact command or code change needed
474
+ - When proposing a fix, include the exact command or code change needed
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/lib/generators/extensions/ai-agents.ts"],
4
- "sourcesContent": ["import { VariableDeclarationKind, type WriterFunction } from 'ts-morph'\nimport type { GeneratorExtension } from '../extension'\nimport {\n arrayLiteral,\n arrowFunction,\n binaryExpression,\n identifier,\n methodCall,\n objectLiteral,\n parenthesized,\n propertyAccess,\n writeValue,\n} from '../ast'\nimport {\n emptyArray,\n emptyObject,\n moduleEntry,\n namespaceFallback,\n namespaceImportSpec,\n renderGeneratedTsSource,\n} from './shared'\n\n/**\n * Generator extension for `<module>/ai-agents.ts` files.\n *\n * Each module's `ai-agents.ts` may export both base agent contributions\n * (`aiAgents`), additive extensions for existing agents\n * (`aiAgentExtensions`), AND cross-module override declarations\n * (`aiAgentOverrides`). The generator scans the file once, emits the\n * configuration entry with all fields, and produces filtered\n * exports inside `ai-agents.generated.ts`:\n *\n * - `aiAgentConfigEntries` (entries that declare base agents)\n * - `aiAgentExtensionEntries` / `allAiAgentExtensions` (entries that append to agents)\n * - `aiAgentOverrideEntries` (entries that declare overrides)\n *\n * The runtime (`@open-mercato/ai-assistant`) reads\n * `aiAgentConfigEntries` to populate the agent registry,\n * `aiAgentOverrideEntries` to apply cross-module replacements, and\n * `allAiAgentExtensions` to append safe metadata after the base load. See spec\n * `.ai/specs/2026-04-30-ai-overrides-and-module-disable.md`.\n */\nexport function createAiAgentsExtension(): GeneratorExtension {\n const imports = [] as Array<ReturnType<typeof namespaceImportSpec>>\n const entries: WriterFunction[] = []\n\n return {\n id: 'registry.ai-agents',\n outputFiles: ['ai-agents.generated.ts'],\n scanModule(ctx) {\n ctx.processStandaloneConfig({\n roots: ctx.roots,\n imps: ctx.imps,\n modId: ctx.moduleId,\n relativePath: 'ai-agents.ts',\n prefix: 'AI_AGENTS',\n importIdRef: ctx.importIdRef,\n standaloneImports: imports,\n standaloneEntries: entries,\n writeConfig: ({ importName, moduleId }) =>\n moduleEntry(moduleId, [\n {\n name: 'agents',\n value: namespaceFallback({\n importName,\n members: ['aiAgents', 'default'],\n fallback: emptyArray(),\n castType: 'unknown[]',\n }),\n },\n {\n name: 'overrides',\n value: namespaceFallback({\n importName,\n members: ['aiAgentOverrides'],\n fallback: emptyObject(),\n castType: 'Record<string, unknown>',\n }),\n },\n {\n name: 'extensions',\n value: namespaceFallback({\n importName,\n members: ['aiAgentExtensions'],\n fallback: emptyArray(),\n castType: 'unknown[]',\n }),\n },\n ]),\n })\n },\n generateOutput() {\n const output = renderGeneratedTsSource({\n fileName: 'ai-agents.generated.ts',\n imports,\n build(sourceFile) {\n sourceFile.addTypeAlias({\n name: 'AiAgentConfigEntry',\n type: '{ moduleId: string; agents: unknown[]; overrides: Record<string, unknown>; extensions: unknown[] }',\n })\n sourceFile.addTypeAlias({\n name: 'AiAgentOverrideConfigEntry',\n type: '{ moduleId: string; overrides: Record<string, unknown> }',\n })\n sourceFile.addTypeAlias({\n name: 'AiAgentExtensionConfigEntry',\n type: '{ moduleId: string; extensions: unknown[] }',\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n declarations: [\n {\n name: 'aiAgentConfigEntriesRaw',\n type: 'AiAgentConfigEntry[]',\n initializer: arrayLiteral(entries, writeValue),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'aiAgentConfigEntries',\n type: 'AiAgentConfigEntry[]',\n initializer: methodCall(identifier('aiAgentConfigEntriesRaw'), 'filter', [\n arrowFunction({\n parameters: ['entry'],\n body: binaryExpression(propertyAccess(propertyAccess(identifier('entry'), 'agents'), 'length'), '>', 0),\n }),\n ]),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'aiAgentExtensionEntries',\n type: 'AiAgentExtensionConfigEntry[]',\n initializer: methodCall(\n methodCall(identifier('aiAgentConfigEntriesRaw'), 'filter', [\n arrowFunction({\n parameters: ['entry'],\n body: binaryExpression(propertyAccess(propertyAccess(identifier('entry'), 'extensions'), 'length'), '>', 0),\n }),\n ]),\n 'map',\n [\n arrowFunction({\n parameters: ['entry'],\n body: parenthesized(\n objectLiteral([\n { name: 'moduleId', value: propertyAccess(identifier('entry'), 'moduleId') },\n { name: 'extensions', value: propertyAccess(identifier('entry'), 'extensions') },\n ]),\n ),\n }),\n ],\n ),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'allAiAgentExtensions',\n initializer: methodCall(identifier('aiAgentExtensionEntries'), 'flatMap', [\n arrowFunction({\n parameters: ['entry'],\n body: propertyAccess(identifier('entry'), 'extensions'),\n }),\n ]),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'allAiAgents',\n initializer: methodCall(identifier('aiAgentConfigEntries'), 'flatMap', [\n arrowFunction({\n parameters: ['entry'],\n body: propertyAccess(identifier('entry'), 'agents'),\n }),\n ]),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'aiAgentOverrideEntries',\n type: 'AiAgentOverrideConfigEntry[]',\n initializer: methodCall(\n methodCall(identifier('aiAgentConfigEntriesRaw'), 'filter', [\n arrowFunction({\n parameters: ['entry'],\n body: binaryExpression(\n propertyAccess(\n methodCall(identifier('Object'), 'keys', [\n propertyAccess(identifier('entry'), 'overrides'),\n ]),\n 'length',\n ),\n '>',\n 0,\n ),\n }),\n ]),\n 'map',\n [\n arrowFunction({\n parameters: ['entry'],\n body: parenthesized(\n objectLiteral([\n { name: 'moduleId', value: propertyAccess(identifier('entry'), 'moduleId') },\n { name: 'overrides', value: propertyAccess(identifier('entry'), 'overrides') },\n ]),\n ),\n }),\n ],\n ),\n },\n ],\n })\n },\n })\n\n return new Map([['ai-agents.generated.ts', output]])\n },\n }\n}\n"],
4
+ "sourcesContent": ["import { VariableDeclarationKind, type WriterFunction } from 'ts-morph'\nimport type { GeneratorExtension } from '../extension'\nimport {\n arrayLiteral,\n arrowFunction,\n binaryExpression,\n identifier,\n methodCall,\n objectLiteral,\n parenthesized,\n propertyAccess,\n writeValue,\n} from '../ast'\nimport {\n emptyArray,\n emptyObject,\n moduleEntry,\n namespaceFallback,\n namespaceImportSpec,\n renderGeneratedTsSource,\n} from './shared'\n\n/**\n * Generator extension for `<module>/ai-agents.ts` files.\n *\n * Each module's `ai-agents.ts` may export both base agent contributions\n * (`aiAgents`), additive extensions for existing agents\n * (`aiAgentExtensions`), AND cross-module override declarations\n * (`aiAgentOverrides`). The generator scans the file once, emits the\n * configuration entry with all fields, and produces filtered\n * exports inside `ai-agents.generated.ts`:\n *\n * - `aiAgentConfigEntries` (entries that declare base agents)\n * - `aiAgentExtensionEntries` / `allAiAgentExtensions` (entries that append to agents)\n * - `aiAgentOverrideEntries` (entries that declare overrides)\n *\n * The runtime (`@open-mercato/ai-assistant`) reads\n * `aiAgentConfigEntries` to populate the agent registry,\n * `aiAgentOverrideEntries` to apply cross-module replacements, and\n * `allAiAgentExtensions` to append safe metadata after the base load. See spec\n * `.ai/specs/implemented/2026-04-30-ai-overrides-and-module-disable.md`.\n */\nexport function createAiAgentsExtension(): GeneratorExtension {\n const imports = [] as Array<ReturnType<typeof namespaceImportSpec>>\n const entries: WriterFunction[] = []\n\n return {\n id: 'registry.ai-agents',\n outputFiles: ['ai-agents.generated.ts'],\n scanModule(ctx) {\n ctx.processStandaloneConfig({\n roots: ctx.roots,\n imps: ctx.imps,\n modId: ctx.moduleId,\n relativePath: 'ai-agents.ts',\n prefix: 'AI_AGENTS',\n importIdRef: ctx.importIdRef,\n standaloneImports: imports,\n standaloneEntries: entries,\n writeConfig: ({ importName, moduleId }) =>\n moduleEntry(moduleId, [\n {\n name: 'agents',\n value: namespaceFallback({\n importName,\n members: ['aiAgents', 'default'],\n fallback: emptyArray(),\n castType: 'unknown[]',\n }),\n },\n {\n name: 'overrides',\n value: namespaceFallback({\n importName,\n members: ['aiAgentOverrides'],\n fallback: emptyObject(),\n castType: 'Record<string, unknown>',\n }),\n },\n {\n name: 'extensions',\n value: namespaceFallback({\n importName,\n members: ['aiAgentExtensions'],\n fallback: emptyArray(),\n castType: 'unknown[]',\n }),\n },\n ]),\n })\n },\n generateOutput() {\n const output = renderGeneratedTsSource({\n fileName: 'ai-agents.generated.ts',\n imports,\n build(sourceFile) {\n sourceFile.addTypeAlias({\n name: 'AiAgentConfigEntry',\n type: '{ moduleId: string; agents: unknown[]; overrides: Record<string, unknown>; extensions: unknown[] }',\n })\n sourceFile.addTypeAlias({\n name: 'AiAgentOverrideConfigEntry',\n type: '{ moduleId: string; overrides: Record<string, unknown> }',\n })\n sourceFile.addTypeAlias({\n name: 'AiAgentExtensionConfigEntry',\n type: '{ moduleId: string; extensions: unknown[] }',\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n declarations: [\n {\n name: 'aiAgentConfigEntriesRaw',\n type: 'AiAgentConfigEntry[]',\n initializer: arrayLiteral(entries, writeValue),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'aiAgentConfigEntries',\n type: 'AiAgentConfigEntry[]',\n initializer: methodCall(identifier('aiAgentConfigEntriesRaw'), 'filter', [\n arrowFunction({\n parameters: ['entry'],\n body: binaryExpression(propertyAccess(propertyAccess(identifier('entry'), 'agents'), 'length'), '>', 0),\n }),\n ]),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'aiAgentExtensionEntries',\n type: 'AiAgentExtensionConfigEntry[]',\n initializer: methodCall(\n methodCall(identifier('aiAgentConfigEntriesRaw'), 'filter', [\n arrowFunction({\n parameters: ['entry'],\n body: binaryExpression(propertyAccess(propertyAccess(identifier('entry'), 'extensions'), 'length'), '>', 0),\n }),\n ]),\n 'map',\n [\n arrowFunction({\n parameters: ['entry'],\n body: parenthesized(\n objectLiteral([\n { name: 'moduleId', value: propertyAccess(identifier('entry'), 'moduleId') },\n { name: 'extensions', value: propertyAccess(identifier('entry'), 'extensions') },\n ]),\n ),\n }),\n ],\n ),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'allAiAgentExtensions',\n initializer: methodCall(identifier('aiAgentExtensionEntries'), 'flatMap', [\n arrowFunction({\n parameters: ['entry'],\n body: propertyAccess(identifier('entry'), 'extensions'),\n }),\n ]),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'allAiAgents',\n initializer: methodCall(identifier('aiAgentConfigEntries'), 'flatMap', [\n arrowFunction({\n parameters: ['entry'],\n body: propertyAccess(identifier('entry'), 'agents'),\n }),\n ]),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'aiAgentOverrideEntries',\n type: 'AiAgentOverrideConfigEntry[]',\n initializer: methodCall(\n methodCall(identifier('aiAgentConfigEntriesRaw'), 'filter', [\n arrowFunction({\n parameters: ['entry'],\n body: binaryExpression(\n propertyAccess(\n methodCall(identifier('Object'), 'keys', [\n propertyAccess(identifier('entry'), 'overrides'),\n ]),\n 'length',\n ),\n '>',\n 0,\n ),\n }),\n ]),\n 'map',\n [\n arrowFunction({\n parameters: ['entry'],\n body: parenthesized(\n objectLiteral([\n { name: 'moduleId', value: propertyAccess(identifier('entry'), 'moduleId') },\n { name: 'overrides', value: propertyAccess(identifier('entry'), 'overrides') },\n ]),\n ),\n }),\n ],\n ),\n },\n ],\n })\n },\n })\n\n return new Map([['ai-agents.generated.ts', output]])\n },\n }\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/lib/generators/extensions/ai-tools.ts"],
4
- "sourcesContent": ["import { VariableDeclarationKind, type WriterFunction } from 'ts-morph'\nimport type { GeneratorExtension } from '../extension'\nimport {\n arrayLiteral,\n arrowFunction,\n binaryExpression,\n identifier,\n methodCall,\n objectLiteral,\n parenthesized,\n propertyAccess,\n writeValue,\n} from '../ast'\nimport {\n emptyArray,\n emptyObject,\n moduleEntry,\n namespaceFallback,\n namespaceImportSpec,\n renderGeneratedTsSource,\n} from './shared'\n\n/**\n * Generator extension for `<module>/ai-tools.ts` files.\n *\n * Each module's `ai-tools.ts` may export both base tool contributions\n * (`aiTools`) AND cross-module override declarations (`aiToolOverrides`).\n * The generator scans the file once and emits two filtered exports\n * inside `ai-tools.generated.ts`:\n *\n * - `aiToolConfigEntries` (entries that declare base tools)\n * - `aiToolOverrideEntries` (entries that declare overrides)\n *\n * The runtime (`@open-mercato/ai-assistant`) reads `aiToolConfigEntries`\n * to populate the tool registry and `aiToolOverrideEntries` to apply\n * cross-module replacements after the base load. See spec\n * `.ai/specs/2026-04-30-ai-overrides-and-module-disable.md`.\n */\nexport function createAiToolsExtension(): GeneratorExtension {\n const imports = [] as Array<ReturnType<typeof namespaceImportSpec>>\n const entries: WriterFunction[] = []\n\n return {\n id: 'registry.ai-tools',\n outputFiles: ['ai-tools.generated.ts'],\n scanModule(ctx) {\n ctx.processStandaloneConfig({\n roots: ctx.roots,\n imps: ctx.imps,\n modId: ctx.moduleId,\n relativePath: 'ai-tools.ts',\n prefix: 'AI_TOOLS',\n importIdRef: ctx.importIdRef,\n standaloneImports: imports,\n standaloneEntries: entries,\n writeConfig: ({ importName, moduleId }) =>\n moduleEntry(moduleId, [\n {\n name: 'tools',\n value: namespaceFallback({\n importName,\n members: ['aiTools', 'default'],\n fallback: emptyArray(),\n castType: 'unknown[]',\n }),\n },\n {\n name: 'overrides',\n value: namespaceFallback({\n importName,\n members: ['aiToolOverrides'],\n fallback: emptyObject(),\n castType: 'Record<string, unknown>',\n }),\n },\n ]),\n })\n },\n generateOutput() {\n const output = renderGeneratedTsSource({\n fileName: 'ai-tools.generated.ts',\n imports,\n build(sourceFile) {\n sourceFile.addTypeAlias({\n name: 'AiToolConfigEntry',\n type: '{ moduleId: string; tools: unknown[]; overrides: Record<string, unknown> }',\n })\n sourceFile.addTypeAlias({\n name: 'AiToolOverrideConfigEntry',\n type: '{ moduleId: string; overrides: Record<string, unknown> }',\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n declarations: [\n {\n name: 'aiToolConfigEntriesRaw',\n type: 'AiToolConfigEntry[]',\n initializer: arrayLiteral(entries, writeValue),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'aiToolConfigEntries',\n type: 'AiToolConfigEntry[]',\n initializer: methodCall(identifier('aiToolConfigEntriesRaw'), 'filter', [\n arrowFunction({\n parameters: ['entry'],\n body: binaryExpression(propertyAccess(propertyAccess(identifier('entry'), 'tools'), 'length'), '>', 0),\n }),\n ]),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'allAiTools',\n initializer: methodCall(identifier('aiToolConfigEntries'), 'flatMap', [\n arrowFunction({\n parameters: ['entry'],\n body: propertyAccess(identifier('entry'), 'tools'),\n }),\n ]),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'aiToolOverrideEntries',\n type: 'AiToolOverrideConfigEntry[]',\n initializer: methodCall(\n methodCall(identifier('aiToolConfigEntriesRaw'), 'filter', [\n arrowFunction({\n parameters: ['entry'],\n body: binaryExpression(\n propertyAccess(\n methodCall(identifier('Object'), 'keys', [\n propertyAccess(identifier('entry'), 'overrides'),\n ]),\n 'length',\n ),\n '>',\n 0,\n ),\n }),\n ]),\n 'map',\n [\n arrowFunction({\n parameters: ['entry'],\n body: parenthesized(\n objectLiteral([\n { name: 'moduleId', value: propertyAccess(identifier('entry'), 'moduleId') },\n { name: 'overrides', value: propertyAccess(identifier('entry'), 'overrides') },\n ]),\n ),\n }),\n ],\n ),\n },\n ],\n })\n },\n })\n\n return new Map([['ai-tools.generated.ts', output]])\n },\n }\n}\n"],
4
+ "sourcesContent": ["import { VariableDeclarationKind, type WriterFunction } from 'ts-morph'\nimport type { GeneratorExtension } from '../extension'\nimport {\n arrayLiteral,\n arrowFunction,\n binaryExpression,\n identifier,\n methodCall,\n objectLiteral,\n parenthesized,\n propertyAccess,\n writeValue,\n} from '../ast'\nimport {\n emptyArray,\n emptyObject,\n moduleEntry,\n namespaceFallback,\n namespaceImportSpec,\n renderGeneratedTsSource,\n} from './shared'\n\n/**\n * Generator extension for `<module>/ai-tools.ts` files.\n *\n * Each module's `ai-tools.ts` may export both base tool contributions\n * (`aiTools`) AND cross-module override declarations (`aiToolOverrides`).\n * The generator scans the file once and emits two filtered exports\n * inside `ai-tools.generated.ts`:\n *\n * - `aiToolConfigEntries` (entries that declare base tools)\n * - `aiToolOverrideEntries` (entries that declare overrides)\n *\n * The runtime (`@open-mercato/ai-assistant`) reads `aiToolConfigEntries`\n * to populate the tool registry and `aiToolOverrideEntries` to apply\n * cross-module replacements after the base load. See spec\n * `.ai/specs/implemented/2026-04-30-ai-overrides-and-module-disable.md`.\n */\nexport function createAiToolsExtension(): GeneratorExtension {\n const imports = [] as Array<ReturnType<typeof namespaceImportSpec>>\n const entries: WriterFunction[] = []\n\n return {\n id: 'registry.ai-tools',\n outputFiles: ['ai-tools.generated.ts'],\n scanModule(ctx) {\n ctx.processStandaloneConfig({\n roots: ctx.roots,\n imps: ctx.imps,\n modId: ctx.moduleId,\n relativePath: 'ai-tools.ts',\n prefix: 'AI_TOOLS',\n importIdRef: ctx.importIdRef,\n standaloneImports: imports,\n standaloneEntries: entries,\n writeConfig: ({ importName, moduleId }) =>\n moduleEntry(moduleId, [\n {\n name: 'tools',\n value: namespaceFallback({\n importName,\n members: ['aiTools', 'default'],\n fallback: emptyArray(),\n castType: 'unknown[]',\n }),\n },\n {\n name: 'overrides',\n value: namespaceFallback({\n importName,\n members: ['aiToolOverrides'],\n fallback: emptyObject(),\n castType: 'Record<string, unknown>',\n }),\n },\n ]),\n })\n },\n generateOutput() {\n const output = renderGeneratedTsSource({\n fileName: 'ai-tools.generated.ts',\n imports,\n build(sourceFile) {\n sourceFile.addTypeAlias({\n name: 'AiToolConfigEntry',\n type: '{ moduleId: string; tools: unknown[]; overrides: Record<string, unknown> }',\n })\n sourceFile.addTypeAlias({\n name: 'AiToolOverrideConfigEntry',\n type: '{ moduleId: string; overrides: Record<string, unknown> }',\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n declarations: [\n {\n name: 'aiToolConfigEntriesRaw',\n type: 'AiToolConfigEntry[]',\n initializer: arrayLiteral(entries, writeValue),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'aiToolConfigEntries',\n type: 'AiToolConfigEntry[]',\n initializer: methodCall(identifier('aiToolConfigEntriesRaw'), 'filter', [\n arrowFunction({\n parameters: ['entry'],\n body: binaryExpression(propertyAccess(propertyAccess(identifier('entry'), 'tools'), 'length'), '>', 0),\n }),\n ]),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'allAiTools',\n initializer: methodCall(identifier('aiToolConfigEntries'), 'flatMap', [\n arrowFunction({\n parameters: ['entry'],\n body: propertyAccess(identifier('entry'), 'tools'),\n }),\n ]),\n },\n ],\n })\n sourceFile.addVariableStatement({\n declarationKind: VariableDeclarationKind.Const,\n isExported: true,\n declarations: [\n {\n name: 'aiToolOverrideEntries',\n type: 'AiToolOverrideConfigEntry[]',\n initializer: methodCall(\n methodCall(identifier('aiToolConfigEntriesRaw'), 'filter', [\n arrowFunction({\n parameters: ['entry'],\n body: binaryExpression(\n propertyAccess(\n methodCall(identifier('Object'), 'keys', [\n propertyAccess(identifier('entry'), 'overrides'),\n ]),\n 'length',\n ),\n '>',\n 0,\n ),\n }),\n ]),\n 'map',\n [\n arrowFunction({\n parameters: ['entry'],\n body: parenthesized(\n objectLiteral([\n { name: 'moduleId', value: propertyAccess(identifier('entry'), 'moduleId') },\n { name: 'overrides', value: propertyAccess(identifier('entry'), 'overrides') },\n ]),\n ),\n }),\n ],\n ),\n },\n ],\n })\n },\n })\n\n return new Map([['ai-tools.generated.ts', output]])\n },\n }\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,89 @@
1
+ import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
2
+ import { resolveQueueStrategy } from "@open-mercato/queue";
3
+ const MULTI_INSTANCE_SAFE_STRATEGIES = {
4
+ cache: ["redis"],
5
+ queue: ["async"],
6
+ rateLimit: ["redis"]
7
+ };
8
+ const COMPONENT_ENV_VARS = {
9
+ cache: "CACHE_STRATEGY",
10
+ queue: "QUEUE_STRATEGY",
11
+ rateLimit: "RATE_LIMIT_STRATEGY"
12
+ };
13
+ class SingleInstanceStrategyError extends Error {
14
+ constructor(result) {
15
+ super(
16
+ `Refusing to start: single-instance infrastructure strategies (${result.offenders.map((offender) => `${offender.envVar}=${offender.configured}`).join(", ")}) are configured under a declared multi-instance topology. Switch to a multi-instance-safe strategy or set OM_ALLOW_SINGLE_INSTANCE_STRATEGIES=1 to override.`
17
+ );
18
+ this.name = "SingleInstanceStrategyError";
19
+ this.offenders = result.offenders;
20
+ }
21
+ }
22
+ function readInfraStrategySnapshot(env = process.env) {
23
+ return {
24
+ cacheStrategy: env.CACHE_STRATEGY?.trim() || "memory",
25
+ queueStrategy: resolveQueueStrategy(),
26
+ rateLimitStrategy: env.RATE_LIMIT_STRATEGY?.trim() || "memory"
27
+ };
28
+ }
29
+ function resolveMultiInstanceHint(env) {
30
+ if (parseBooleanWithDefault(env.OM_MULTI_INSTANCE, false)) return true;
31
+ const instanceCount = Number.parseInt(env.OM_INSTANCE_COUNT ?? "", 10);
32
+ return Number.isFinite(instanceCount) && instanceCount > 1;
33
+ }
34
+ function evaluateSingleInstanceGuard(snapshot, env = process.env) {
35
+ const configured = {
36
+ cache: snapshot.cacheStrategy,
37
+ queue: snapshot.queueStrategy,
38
+ rateLimit: snapshot.rateLimitStrategy
39
+ };
40
+ const offenders = [];
41
+ for (const component of Object.keys(configured)) {
42
+ const safeValues = MULTI_INSTANCE_SAFE_STRATEGIES[component];
43
+ if (!safeValues.includes(configured[component])) {
44
+ offenders.push({
45
+ component,
46
+ envVar: COMPONENT_ENV_VARS[component],
47
+ configured: configured[component],
48
+ safeValues
49
+ });
50
+ }
51
+ }
52
+ const production = (env.NODE_ENV ?? "").trim() === "production";
53
+ const multiInstance = resolveMultiInstanceHint(env);
54
+ const overridden = parseBooleanWithDefault(env.OM_ALLOW_SINGLE_INSTANCE_STRATEGIES, false);
55
+ let action = "ok";
56
+ if (offenders.length > 0 && production) {
57
+ action = multiInstance && !overridden ? "fail" : "warn";
58
+ }
59
+ return { action, offenders, production, multiInstance, overridden };
60
+ }
61
+ function formatSingleInstanceGuardMessage(result) {
62
+ const offenderLines = result.offenders.map(
63
+ (offender) => ` - ${offender.envVar}=${offender.configured} (multi-instance-safe: ${offender.safeValues.join(", ")})`
64
+ );
65
+ const header = result.action === "fail" ? "[server] Refusing to start: single-instance infrastructure strategies under a multi-instance topology." : "[server] WARNING: single-instance infrastructure strategies detected in production.";
66
+ const guidance = result.action === "fail" ? "[server] Switch each strategy above to a multi-instance-safe value, or set OM_ALLOW_SINGLE_INSTANCE_STRATEGIES=1 to override (accepting duplicate jobs, stale ACLs, and weakened rate limits)." : result.multiInstance ? "[server] OM_ALLOW_SINGLE_INSTANCE_STRATEGIES=1 is set \u2014 proceeding despite the risks above (duplicate jobs, stale ACLs, weakened rate limits)." : "[server] Running multiple instances against these strategies will cause duplicate jobs, stale ACLs, and weakened rate limits. Set OM_MULTI_INSTANCE=1 to make this a hard failure once you scale out.";
67
+ return [header, ...offenderLines, guidance];
68
+ }
69
+ function assertSingleInstanceStrategies(env = process.env, options) {
70
+ const snapshot = options?.snapshot ?? readInfraStrategySnapshot(env);
71
+ const result = evaluateSingleInstanceGuard(snapshot, env);
72
+ const logger = options?.logger ?? console;
73
+ if (result.action === "fail") {
74
+ for (const line of formatSingleInstanceGuardMessage(result)) logger.error(line);
75
+ throw new SingleInstanceStrategyError(result);
76
+ }
77
+ if (result.action === "warn") {
78
+ for (const line of formatSingleInstanceGuardMessage(result)) logger.warn(line);
79
+ }
80
+ return result;
81
+ }
82
+ export {
83
+ SingleInstanceStrategyError,
84
+ assertSingleInstanceStrategies,
85
+ evaluateSingleInstanceGuard,
86
+ formatSingleInstanceGuardMessage,
87
+ readInfraStrategySnapshot
88
+ };
89
+ //# sourceMappingURL=single-instance-strategy-guard.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/single-instance-strategy-guard.ts"],
4
+ "sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { resolveQueueStrategy } from '@open-mercato/queue'\n\n/**\n * Boot-time guard that refuses to start (or warns) when an infrastructure\n * strategy that is only safe for a single process/instance is configured under\n * a declared multi-instance topology.\n *\n * Background: `CACHE_STRATEGY`, `RATE_LIMIT_STRATEGY` and `QUEUE_STRATEGY` all\n * default to single-instance modes (`memory`/`memory`/`local`). Running more\n * than one app instance against those defaults silently breaks three\n * correctness invariants at once \u2014 stale RBAC caches after a privilege\n * revocation, rate limits multiplied by the instance count, and duplicate job\n * processing from the leaderless local file queue. None of them warn.\n *\n * Only strategies that coordinate across processes via shared external state\n * are considered multi-instance safe. Everything else (the in-process memory\n * strategies, the local file queue, and disk-backed caches that are not shared\n * across hosts) is treated as single-instance.\n */\nconst MULTI_INSTANCE_SAFE_STRATEGIES = {\n cache: ['redis'],\n queue: ['async'],\n rateLimit: ['redis'],\n} as const\n\nexport type SingleInstanceGuardComponent = 'cache' | 'queue' | 'rateLimit'\n\nexport type SingleInstanceGuardOffender = {\n component: SingleInstanceGuardComponent\n envVar: string\n configured: string\n safeValues: readonly string[]\n}\n\nexport type SingleInstanceGuardAction = 'ok' | 'warn' | 'fail'\n\nexport type SingleInstanceGuardResult = {\n action: SingleInstanceGuardAction\n offenders: SingleInstanceGuardOffender[]\n production: boolean\n multiInstance: boolean\n overridden: boolean\n}\n\nexport type InfraStrategySnapshot = {\n cacheStrategy: string\n queueStrategy: string\n rateLimitStrategy: string\n}\n\nconst COMPONENT_ENV_VARS: Record<SingleInstanceGuardComponent, string> = {\n cache: 'CACHE_STRATEGY',\n queue: 'QUEUE_STRATEGY',\n rateLimit: 'RATE_LIMIT_STRATEGY',\n}\n\nexport class SingleInstanceStrategyError extends Error {\n readonly offenders: SingleInstanceGuardOffender[]\n\n constructor(result: SingleInstanceGuardResult) {\n super(\n `Refusing to start: single-instance infrastructure strategies (${result.offenders\n .map((offender) => `${offender.envVar}=${offender.configured}`)\n .join(', ')}) are configured under a declared multi-instance topology. ` +\n 'Switch to a multi-instance-safe strategy or set OM_ALLOW_SINGLE_INSTANCE_STRATEGIES=1 to override.',\n )\n this.name = 'SingleInstanceStrategyError'\n this.offenders = result.offenders\n }\n}\n\n/**\n * Read the configured infrastructure strategies. Queue resolution reuses the\n * canonical `resolveQueueStrategy()` so the default stays single-sourced; cache\n * and rate-limit values are read directly with their documented defaults\n * (`packages/cache/src/service.ts`, `packages/shared/src/lib/ratelimit/config.ts`)\n * because the guard's safe-set policy \u2014 not the resolver default \u2014 decides what\n * counts as single-instance.\n */\nexport function readInfraStrategySnapshot(env: NodeJS.ProcessEnv = process.env): InfraStrategySnapshot {\n return {\n cacheStrategy: env.CACHE_STRATEGY?.trim() || 'memory',\n queueStrategy: resolveQueueStrategy(),\n rateLimitStrategy: env.RATE_LIMIT_STRATEGY?.trim() || 'memory',\n }\n}\n\nfunction resolveMultiInstanceHint(env: NodeJS.ProcessEnv): boolean {\n if (parseBooleanWithDefault(env.OM_MULTI_INSTANCE, false)) return true\n const instanceCount = Number.parseInt(env.OM_INSTANCE_COUNT ?? '', 10)\n return Number.isFinite(instanceCount) && instanceCount > 1\n}\n\nexport function evaluateSingleInstanceGuard(\n snapshot: InfraStrategySnapshot,\n env: NodeJS.ProcessEnv = process.env,\n): SingleInstanceGuardResult {\n const configured: Record<SingleInstanceGuardComponent, string> = {\n cache: snapshot.cacheStrategy,\n queue: snapshot.queueStrategy,\n rateLimit: snapshot.rateLimitStrategy,\n }\n\n const offenders: SingleInstanceGuardOffender[] = []\n for (const component of Object.keys(configured) as SingleInstanceGuardComponent[]) {\n const safeValues: readonly string[] = MULTI_INSTANCE_SAFE_STRATEGIES[component]\n if (!safeValues.includes(configured[component])) {\n offenders.push({\n component,\n envVar: COMPONENT_ENV_VARS[component],\n configured: configured[component],\n safeValues,\n })\n }\n }\n\n const production = (env.NODE_ENV ?? '').trim() === 'production'\n const multiInstance = resolveMultiInstanceHint(env)\n const overridden = parseBooleanWithDefault(env.OM_ALLOW_SINGLE_INSTANCE_STRATEGIES, false)\n\n let action: SingleInstanceGuardAction = 'ok'\n if (offenders.length > 0 && production) {\n action = multiInstance && !overridden ? 'fail' : 'warn'\n }\n\n return { action, offenders, production, multiInstance, overridden }\n}\n\nexport function formatSingleInstanceGuardMessage(result: SingleInstanceGuardResult): string[] {\n const offenderLines = result.offenders.map(\n (offender) =>\n ` - ${offender.envVar}=${offender.configured} (multi-instance-safe: ${offender.safeValues.join(', ')})`,\n )\n const header =\n result.action === 'fail'\n ? '[server] Refusing to start: single-instance infrastructure strategies under a multi-instance topology.'\n : '[server] WARNING: single-instance infrastructure strategies detected in production.'\n const guidance =\n result.action === 'fail'\n ? '[server] Switch each strategy above to a multi-instance-safe value, or set OM_ALLOW_SINGLE_INSTANCE_STRATEGIES=1 to override (accepting duplicate jobs, stale ACLs, and weakened rate limits).'\n : result.multiInstance\n ? '[server] OM_ALLOW_SINGLE_INSTANCE_STRATEGIES=1 is set \u2014 proceeding despite the risks above (duplicate jobs, stale ACLs, weakened rate limits).'\n : '[server] Running multiple instances against these strategies will cause duplicate jobs, stale ACLs, and weakened rate limits. Set OM_MULTI_INSTANCE=1 to make this a hard failure once you scale out.'\n return [header, ...offenderLines, guidance]\n}\n\nexport type SingleInstanceGuardLogger = Pick<Console, 'warn' | 'error'>\n\n/**\n * Evaluate the guard and enforce it: throw on `fail`, log prominently on\n * `warn`, and stay silent on `ok`. Safe to call on every `start`; it never\n * fires for dev boots or single-instance production deployments.\n */\nexport function assertSingleInstanceStrategies(\n env: NodeJS.ProcessEnv = process.env,\n options?: { snapshot?: InfraStrategySnapshot; logger?: SingleInstanceGuardLogger },\n): SingleInstanceGuardResult {\n const snapshot = options?.snapshot ?? readInfraStrategySnapshot(env)\n const result = evaluateSingleInstanceGuard(snapshot, env)\n const logger = options?.logger ?? console\n\n if (result.action === 'fail') {\n for (const line of formatSingleInstanceGuardMessage(result)) logger.error(line)\n throw new SingleInstanceStrategyError(result)\n }\n if (result.action === 'warn') {\n for (const line of formatSingleInstanceGuardMessage(result)) logger.warn(line)\n }\n\n return result\n}\n"],
5
+ "mappings": "AAAA,SAAS,+BAA+B;AACxC,SAAS,4BAA4B;AAmBrC,MAAM,iCAAiC;AAAA,EACrC,OAAO,CAAC,OAAO;AAAA,EACf,OAAO,CAAC,OAAO;AAAA,EACf,WAAW,CAAC,OAAO;AACrB;AA2BA,MAAM,qBAAmE;AAAA,EACvE,OAAO;AAAA,EACP,OAAO;AAAA,EACP,WAAW;AACb;AAEO,MAAM,oCAAoC,MAAM;AAAA,EAGrD,YAAY,QAAmC;AAC7C;AAAA,MACE,iEAAiE,OAAO,UACrE,IAAI,CAAC,aAAa,GAAG,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE,EAC7D,KAAK,IAAI,CAAC;AAAA,IAEf;AACA,SAAK,OAAO;AACZ,SAAK,YAAY,OAAO;AAAA,EAC1B;AACF;AAUO,SAAS,0BAA0B,MAAyB,QAAQ,KAA4B;AACrG,SAAO;AAAA,IACL,eAAe,IAAI,gBAAgB,KAAK,KAAK;AAAA,IAC7C,eAAe,qBAAqB;AAAA,IACpC,mBAAmB,IAAI,qBAAqB,KAAK,KAAK;AAAA,EACxD;AACF;AAEA,SAAS,yBAAyB,KAAiC;AACjE,MAAI,wBAAwB,IAAI,mBAAmB,KAAK,EAAG,QAAO;AAClE,QAAM,gBAAgB,OAAO,SAAS,IAAI,qBAAqB,IAAI,EAAE;AACrE,SAAO,OAAO,SAAS,aAAa,KAAK,gBAAgB;AAC3D;AAEO,SAAS,4BACd,UACA,MAAyB,QAAQ,KACN;AAC3B,QAAM,aAA2D;AAAA,IAC/D,OAAO,SAAS;AAAA,IAChB,OAAO,SAAS;AAAA,IAChB,WAAW,SAAS;AAAA,EACtB;AAEA,QAAM,YAA2C,CAAC;AAClD,aAAW,aAAa,OAAO,KAAK,UAAU,GAAqC;AACjF,UAAM,aAAgC,+BAA+B,SAAS;AAC9E,QAAI,CAAC,WAAW,SAAS,WAAW,SAAS,CAAC,GAAG;AAC/C,gBAAU,KAAK;AAAA,QACb;AAAA,QACA,QAAQ,mBAAmB,SAAS;AAAA,QACpC,YAAY,WAAW,SAAS;AAAA,QAChC;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,cAAc,IAAI,YAAY,IAAI,KAAK,MAAM;AACnD,QAAM,gBAAgB,yBAAyB,GAAG;AAClD,QAAM,aAAa,wBAAwB,IAAI,qCAAqC,KAAK;AAEzF,MAAI,SAAoC;AACxC,MAAI,UAAU,SAAS,KAAK,YAAY;AACtC,aAAS,iBAAiB,CAAC,aAAa,SAAS;AAAA,EACnD;AAEA,SAAO,EAAE,QAAQ,WAAW,YAAY,eAAe,WAAW;AACpE;AAEO,SAAS,iCAAiC,QAA6C;AAC5F,QAAM,gBAAgB,OAAO,UAAU;AAAA,IACrC,CAAC,aACC,OAAO,SAAS,MAAM,IAAI,SAAS,UAAU,0BAA0B,SAAS,WAAW,KAAK,IAAI,CAAC;AAAA,EACzG;AACA,QAAM,SACJ,OAAO,WAAW,SACd,2GACA;AACN,QAAM,WACJ,OAAO,WAAW,SACd,mMACA,OAAO,gBACL,wJACA;AACR,SAAO,CAAC,QAAQ,GAAG,eAAe,QAAQ;AAC5C;AASO,SAAS,+BACd,MAAyB,QAAQ,KACjC,SAC2B;AAC3B,QAAM,WAAW,SAAS,YAAY,0BAA0B,GAAG;AACnE,QAAM,SAAS,4BAA4B,UAAU,GAAG;AACxD,QAAM,SAAS,SAAS,UAAU;AAElC,MAAI,OAAO,WAAW,QAAQ;AAC5B,eAAW,QAAQ,iCAAiC,MAAM,EAAG,QAAO,MAAM,IAAI;AAC9E,UAAM,IAAI,4BAA4B,MAAM;AAAA,EAC9C;AACA,MAAI,OAAO,WAAW,QAAQ;AAC5B,eAAW,QAAQ,iCAAiC,MAAM,EAAG,QAAO,KAAK,IAAI;AAAA,EAC/E;AAEA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -1,4 +1,3 @@
1
- import { GenericContainer } from "testcontainers";
2
1
  import { createServer } from "node:net";
3
2
  import { existsSync, readFileSync } from "node:fs";
4
3
  import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
@@ -2322,6 +2321,7 @@ async function startEphemeralEnvironment(options) {
2322
2321
  const databaseName = "mercato_test";
2323
2322
  const databaseUser = "mercato";
2324
2323
  const databasePassword = "secret";
2324
+ const { GenericContainer } = await import("testcontainers");
2325
2325
  const databaseContainer = await new GenericContainer("postgres:16").withEnvironment({
2326
2326
  POSTGRES_DB: databaseName,
2327
2327
  POSTGRES_USER: databaseUser,