@open-mercato/cli 0.9.1-develop.7476.1.342138d35e → 0.9.1-develop.7496.1.3346415f94
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 +172 -172
- package/dist/agentic/guides/module-facts.v2.json +172 -172
- package/dist/agentic/guides/modules/agent_orchestrator/index.md +1 -1
- package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
- package/dist/agentic/guides/modules/api_docs/index.md +1 -1
- package/dist/agentic/guides/modules/api_keys/index.md +1 -1
- package/dist/agentic/guides/modules/attachments/index.md +1 -1
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/availability/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/index.md +1 -1
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
- package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
- package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
- package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
- package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
- package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
- package/dist/agentic/guides/modules/configs/index.md +1 -1
- package/dist/agentic/guides/modules/content/index.md +1 -1
- package/dist/agentic/guides/modules/currencies/index.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
- package/dist/agentic/guides/modules/customer_groups/index.md +1 -1
- package/dist/agentic/guides/modules/customers/active-extension-bindings.md +4 -4
- package/dist/agentic/guides/modules/customers/index.md +1 -1
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/di-registrations-rich.md +8 -8
- package/dist/agentic/guides/modules/data_sync/di-service-tokens.md +4 -4
- package/dist/agentic/guides/modules/data_sync/exact-override-targets.md +8 -8
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- package/dist/agentic/guides/modules/design_system/index.md +1 -1
- package/dist/agentic/guides/modules/devices/index.md +1 -1
- package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
- package/dist/agentic/guides/modules/directory/index.md +1 -1
- package/dist/agentic/guides/modules/documents/index.md +1 -1
- package/dist/agentic/guides/modules/ecommerce/index.md +1 -1
- package/dist/agentic/guides/modules/entities/index.md +1 -1
- package/dist/agentic/guides/modules/eudr/index.md +1 -1
- package/dist/agentic/guides/modules/events/index.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
- package/dist/agentic/guides/modules/forms/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_tpay/index.md +1 -1
- package/dist/agentic/guides/modules/generators/index.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
- package/dist/agentic/guides/modules/integrations/index.md +1 -1
- package/dist/agentic/guides/modules/messages/index.md +1 -1
- package/dist/agentic/guides/modules/notifications/index.md +1 -1
- package/dist/agentic/guides/modules/onboarding/index.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
- package/dist/agentic/guides/modules/perspectives/index.md +1 -1
- package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
- package/dist/agentic/guides/modules/planner/index.md +1 -1
- package/dist/agentic/guides/modules/portal/index.md +1 -1
- package/dist/agentic/guides/modules/progress/index.md +1 -1
- package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
- package/dist/agentic/guides/modules/query_index/index.md +1 -1
- package/dist/agentic/guides/modules/record_locks/index.md +1 -1
- package/dist/agentic/guides/modules/resources/index.md +1 -1
- package/dist/agentic/guides/modules/sales/index.md +1 -1
- package/dist/agentic/guides/modules/scheduler/index.md +1 -1
- package/dist/agentic/guides/modules/search/index.md +1 -1
- package/dist/agentic/guides/modules/security/index.md +1 -1
- package/dist/agentic/guides/modules/seeds/index.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
- package/dist/agentic/guides/modules/sso/index.md +1 -1
- package/dist/agentic/guides/modules/staff/index.md +1 -1
- package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
- package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
- package/dist/agentic/guides/modules/telemetry/index.md +1 -1
- package/dist/agentic/guides/modules/tillio/index.md +1 -1
- package/dist/agentic/guides/modules/translations/index.md +1 -1
- package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
- package/dist/agentic/guides/modules/webhooks/index.md +1 -1
- package/dist/agentic/guides/modules/wms/index.md +1 -1
- package/dist/agentic/guides/modules/workflows/index.md +1 -1
- package/dist/agentic/guides/reference-module-facts.json +1 -1
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +4 -0
- package/dist/agentic/guides/upstream/manifest.json +2 -2
- package/dist/bin.js +2 -0
- package/dist/bin.js.map +2 -2
- package/dist/mercato.js +71 -5
- package/dist/mercato.js.map +2 -2
- package/package.json +7 -7
- package/src/__tests__/mercato.test.ts +260 -0
- package/src/bin.ts +5 -0
- package/src/lib/__tests__/bin-next-subpath-hook.test.ts +25 -0
- package/src/mercato.ts +87 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.9.1-develop.
|
|
3
|
+
"version": "0.9.1-develop.7496.1.3346415f94",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
"@mikro-orm/decorators": "^7.1.14",
|
|
61
61
|
"@mikro-orm/migrations": "^7.1.14",
|
|
62
62
|
"@mikro-orm/postgresql": "^7.1.14",
|
|
63
|
-
"@open-mercato/events": "0.9.1-develop.
|
|
64
|
-
"@open-mercato/queue": "0.9.1-develop.
|
|
65
|
-
"@open-mercato/shared": "0.9.1-develop.
|
|
66
|
-
"@open-mercato/telemetry": "0.9.1-develop.
|
|
63
|
+
"@open-mercato/events": "0.9.1-develop.7496.1.3346415f94",
|
|
64
|
+
"@open-mercato/queue": "0.9.1-develop.7496.1.3346415f94",
|
|
65
|
+
"@open-mercato/shared": "0.9.1-develop.7496.1.3346415f94",
|
|
66
|
+
"@open-mercato/telemetry": "0.9.1-develop.7496.1.3346415f94",
|
|
67
67
|
"cross-spawn": "^7.0.6",
|
|
68
68
|
"pg": "8.23.0",
|
|
69
69
|
"semver": "^7.8.5",
|
|
@@ -74,10 +74,10 @@
|
|
|
74
74
|
"typescript-js": "npm:typescript@6.0.3"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@open-mercato/shared": "0.9.1-develop.
|
|
77
|
+
"@open-mercato/shared": "0.9.1-develop.7496.1.3346415f94"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
-
"@open-mercato/shared": "0.9.1-develop.
|
|
80
|
+
"@open-mercato/shared": "0.9.1-develop.7496.1.3346415f94",
|
|
81
81
|
"@types/jest": "^30.0.0",
|
|
82
82
|
"jest": "^30.4.2",
|
|
83
83
|
"ts-jest": "^29.4.12"
|
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
registerCliModules,
|
|
4
4
|
getCliModules,
|
|
5
5
|
hasCliModules,
|
|
6
|
+
isHelpRequest,
|
|
6
7
|
padByCodePointWidth,
|
|
8
|
+
resolveEnvBootstrapOptions,
|
|
7
9
|
run,
|
|
8
10
|
} from '../mercato'
|
|
9
11
|
import { pathIncludes } from '../lib/__tests__/path-helpers'
|
|
@@ -187,6 +189,264 @@ describe('padByCodePointWidth', () => {
|
|
|
187
189
|
})
|
|
188
190
|
})
|
|
189
191
|
|
|
192
|
+
describe('--help never triggers side effects (issue #5581)', () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
jest.restoreAllMocks()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('isHelpRequest', () => {
|
|
198
|
+
it('matches the long and short help flags in any position', () => {
|
|
199
|
+
expect(isHelpRequest(['db', 'generate', '--help'])).toBe(true)
|
|
200
|
+
expect(isHelpRequest(['-h'])).toBe(true)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('does not match arguments that merely contain the flag text', () => {
|
|
204
|
+
expect(isHelpRequest(['db', 'generate'])).toBe(false)
|
|
205
|
+
expect(isHelpRequest(['--help-me'])).toBe(false)
|
|
206
|
+
expect(isHelpRequest(['--no-help'])).toBe(false)
|
|
207
|
+
expect(isHelpRequest([])).toBe(false)
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
describe('resolveEnvBootstrapOptions', () => {
|
|
212
|
+
it('refuses to create .env from .env.example for a help invocation', () => {
|
|
213
|
+
expect(resolveEnvBootstrapOptions(['db', 'generate', '--help'])).toEqual({
|
|
214
|
+
createIfMissing: false,
|
|
215
|
+
quiet: true,
|
|
216
|
+
})
|
|
217
|
+
expect(resolveEnvBootstrapOptions(['-h'])).toEqual({ createIfMissing: false, quiet: true })
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('still creates .env for a real command invocation', () => {
|
|
221
|
+
expect(resolveEnvBootstrapOptions(['db', 'generate'])).toEqual({
|
|
222
|
+
createIfMissing: true,
|
|
223
|
+
quiet: false,
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('keeps the pre-existing deploy and telemetry opt-outs', () => {
|
|
228
|
+
expect(resolveEnvBootstrapOptions(['deploy', 'railway'])).toEqual({
|
|
229
|
+
createIfMissing: false,
|
|
230
|
+
quiet: true,
|
|
231
|
+
})
|
|
232
|
+
expect(resolveEnvBootstrapOptions(['telemetry', 'init'])).toEqual({
|
|
233
|
+
createIfMissing: false,
|
|
234
|
+
quiet: true,
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe('dispatcher help branch', () => {
|
|
240
|
+
it('prints usage instead of running db:generate when --help is passed', async () => {
|
|
241
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
242
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
243
|
+
const dbGenerate = jest.fn()
|
|
244
|
+
|
|
245
|
+
registerCliModules([
|
|
246
|
+
{
|
|
247
|
+
id: 'db',
|
|
248
|
+
cli: [{ command: 'generate', help: 'Writes migrations. Applies nothing.', run: dbGenerate }],
|
|
249
|
+
} as any,
|
|
250
|
+
])
|
|
251
|
+
|
|
252
|
+
const exitCode = await run(['node', 'mercato', 'db', 'generate', '--help'])
|
|
253
|
+
|
|
254
|
+
expect(exitCode).toBe(0)
|
|
255
|
+
expect(dbGenerate).not.toHaveBeenCalled()
|
|
256
|
+
const printed = consoleLogSpy.mock.calls.map((call) => String(call[0])).join('\n')
|
|
257
|
+
expect(printed).toContain('mercato db generate [args]')
|
|
258
|
+
expect(printed).toContain('Writes migrations. Applies nothing.')
|
|
259
|
+
|
|
260
|
+
consoleLogSpy.mockRestore()
|
|
261
|
+
consoleErrorSpy.mockRestore()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('prints usage instead of running db:migrate when -h is passed', async () => {
|
|
265
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
266
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
267
|
+
const dbMigrate = jest.fn()
|
|
268
|
+
|
|
269
|
+
registerCliModules([
|
|
270
|
+
{ id: 'db', cli: [{ command: 'migrate', run: dbMigrate }] } as any,
|
|
271
|
+
])
|
|
272
|
+
|
|
273
|
+
const exitCode = await run(['node', 'mercato', 'db', 'migrate', '-h'])
|
|
274
|
+
|
|
275
|
+
expect(exitCode).toBe(0)
|
|
276
|
+
expect(dbMigrate).not.toHaveBeenCalled()
|
|
277
|
+
|
|
278
|
+
consoleLogSpy.mockRestore()
|
|
279
|
+
consoleErrorSpy.mockRestore()
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('prints usage instead of destroying data when greenfield --yes --help is passed', async () => {
|
|
283
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
284
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
285
|
+
const dbGreenfield = jest.fn()
|
|
286
|
+
|
|
287
|
+
registerCliModules([
|
|
288
|
+
{
|
|
289
|
+
id: 'db',
|
|
290
|
+
cli: [{
|
|
291
|
+
command: 'greenfield',
|
|
292
|
+
help: 'Destroys all data.',
|
|
293
|
+
run: dbGreenfield,
|
|
294
|
+
}],
|
|
295
|
+
} as any,
|
|
296
|
+
])
|
|
297
|
+
|
|
298
|
+
// --yes is the flag that skips greenfield's confirmation prompt, so pairing it with
|
|
299
|
+
// --help is the scenario that must never reach `run`, regardless of flag order.
|
|
300
|
+
const exitCode = await run(['node', 'mercato', 'db', 'greenfield', '--yes', '--help'])
|
|
301
|
+
|
|
302
|
+
expect(exitCode).toBe(0)
|
|
303
|
+
expect(dbGreenfield).not.toHaveBeenCalled()
|
|
304
|
+
|
|
305
|
+
consoleLogSpy.mockRestore()
|
|
306
|
+
consoleErrorSpy.mockRestore()
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('forwards --help to commands that declare handlesHelp', async () => {
|
|
310
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
311
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
312
|
+
const deploy = jest.fn()
|
|
313
|
+
|
|
314
|
+
registerCliModules([
|
|
315
|
+
{ id: 'deploy', cli: [{ command: 'railway', handlesHelp: true, run: deploy }] } as any,
|
|
316
|
+
])
|
|
317
|
+
|
|
318
|
+
const exitCode = await run(['node', 'mercato', 'deploy', 'railway', '--help'])
|
|
319
|
+
|
|
320
|
+
expect(exitCode).toBe(0)
|
|
321
|
+
expect(deploy).toHaveBeenCalledWith(['--help'])
|
|
322
|
+
|
|
323
|
+
consoleLogSpy.mockRestore()
|
|
324
|
+
consoleErrorSpy.mockRestore()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('lists the module commands when the help flag sits in the command slot', async () => {
|
|
328
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
329
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
330
|
+
const dbGenerate = jest.fn()
|
|
331
|
+
|
|
332
|
+
registerCliModules([
|
|
333
|
+
{
|
|
334
|
+
id: 'db',
|
|
335
|
+
cli: [
|
|
336
|
+
{ command: 'generate', run: dbGenerate },
|
|
337
|
+
{ command: 'migrate', run: jest.fn() },
|
|
338
|
+
],
|
|
339
|
+
} as any,
|
|
340
|
+
])
|
|
341
|
+
|
|
342
|
+
const exitCode = await run(['node', 'mercato', 'db', '--help'])
|
|
343
|
+
|
|
344
|
+
expect(exitCode).toBe(0)
|
|
345
|
+
expect(dbGenerate).not.toHaveBeenCalled()
|
|
346
|
+
expect(consoleErrorSpy).not.toHaveBeenCalled()
|
|
347
|
+
const printed = consoleLogSpy.mock.calls.map((call) => String(call[0])).join('\n')
|
|
348
|
+
expect(printed).toContain('Commands for "db": generate, migrate')
|
|
349
|
+
|
|
350
|
+
consoleLogSpy.mockRestore()
|
|
351
|
+
consoleErrorSpy.mockRestore()
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('lets a self-documenting command print its own subcommand help', async () => {
|
|
355
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
356
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
357
|
+
// Mirrors packages/core/src/modules/configs/cli.ts, whose `cache` command routes
|
|
358
|
+
// --help to its own printCacheHelp() rather than to the dispatcher's generic usage.
|
|
359
|
+
const cache = jest.fn((rest: string[]) => {
|
|
360
|
+
if (rest[0] === '--help') console.log('configs cache: stats | purge | structural')
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
registerCliModules([
|
|
364
|
+
{ id: 'configs', cli: [{ command: 'cache', handlesHelp: true, run: cache }] } as any,
|
|
365
|
+
])
|
|
366
|
+
|
|
367
|
+
const exitCode = await run(['node', 'mercato', 'configs', 'cache', '--help'])
|
|
368
|
+
|
|
369
|
+
expect(exitCode).toBe(0)
|
|
370
|
+
expect(cache).toHaveBeenCalledWith(['--help'])
|
|
371
|
+
const printed = consoleLogSpy.mock.calls.map((call) => String(call[0])).join('\n')
|
|
372
|
+
expect(printed).toContain('configs cache: stats | purge | structural')
|
|
373
|
+
expect(printed).not.toContain('Usage: ✨ mercato configs cache [args]')
|
|
374
|
+
|
|
375
|
+
consoleLogSpy.mockRestore()
|
|
376
|
+
consoleErrorSpy.mockRestore()
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('still runs the command when no help flag is present', async () => {
|
|
380
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
381
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
382
|
+
const dbGenerate = jest.fn()
|
|
383
|
+
|
|
384
|
+
registerCliModules([
|
|
385
|
+
{ id: 'db', cli: [{ command: 'generate', run: dbGenerate }] } as any,
|
|
386
|
+
])
|
|
387
|
+
|
|
388
|
+
const exitCode = await run(['node', 'mercato', 'db', 'generate'])
|
|
389
|
+
|
|
390
|
+
expect(exitCode).toBe(0)
|
|
391
|
+
expect(dbGenerate).toHaveBeenCalled()
|
|
392
|
+
|
|
393
|
+
consoleLogSpy.mockRestore()
|
|
394
|
+
consoleErrorSpy.mockRestore()
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
describe('top-level command help', () => {
|
|
399
|
+
it('prints usage instead of initializing the app when init --help is passed', async () => {
|
|
400
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
401
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
402
|
+
|
|
403
|
+
const exitCode = await run(['node', 'mercato', 'init', '--help'])
|
|
404
|
+
|
|
405
|
+
expect(exitCode).toBe(0)
|
|
406
|
+
const printed = consoleLogSpy.mock.calls.map((call) => String(call[0])).join('\n')
|
|
407
|
+
expect(printed).toContain('Usage: mercato init')
|
|
408
|
+
|
|
409
|
+
consoleLogSpy.mockRestore()
|
|
410
|
+
consoleErrorSpy.mockRestore()
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('prints usage instead of seeding when seed:defaults --help is passed', async () => {
|
|
414
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
415
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
416
|
+
|
|
417
|
+
const exitCode = await run(['node', 'mercato', 'seed:defaults', '--help'])
|
|
418
|
+
|
|
419
|
+
expect(exitCode).toBe(0)
|
|
420
|
+
const printed = consoleLogSpy.mock.calls.map((call) => String(call[0])).join('\n')
|
|
421
|
+
expect(printed).toContain('Usage: mercato seed:defaults')
|
|
422
|
+
|
|
423
|
+
consoleLogSpy.mockRestore()
|
|
424
|
+
consoleErrorSpy.mockRestore()
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it.each(['constructor', 'toString', 'valueOf', 'hasOwnProperty', '__proto__'])(
|
|
428
|
+
'reports %s as an unknown module instead of matching an inherited prototype key',
|
|
429
|
+
async (inheritedKey) => {
|
|
430
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
431
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
432
|
+
|
|
433
|
+
registerCliModules([
|
|
434
|
+
{ id: 'db', cli: [{ command: 'generate', run: jest.fn() }] } as any,
|
|
435
|
+
])
|
|
436
|
+
|
|
437
|
+
const exitCode = await run(['node', 'mercato', inheritedKey, '--help'])
|
|
438
|
+
|
|
439
|
+
expect(exitCode).toBe(1)
|
|
440
|
+
const errors = consoleErrorSpy.mock.calls.map((call) => String(call[0])).join('\n')
|
|
441
|
+
expect(errors).toContain(`Module not found: "${inheritedKey}"`)
|
|
442
|
+
|
|
443
|
+
consoleLogSpy.mockRestore()
|
|
444
|
+
consoleErrorSpy.mockRestore()
|
|
445
|
+
},
|
|
446
|
+
)
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
|
|
190
450
|
describe('db command failure output', () => {
|
|
191
451
|
const originalDatabaseUrl = process.env.DATABASE_URL
|
|
192
452
|
|
package/src/bin.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Uses dynamic app resolution to find generated files at .mercato/generated/
|
|
6
6
|
*/
|
|
7
7
|
import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
8
|
+
import { installNextSubpathResolveHook } from '@open-mercato/shared/lib/bootstrap/next-subpath-resolve-hook'
|
|
8
9
|
import { resolveCliBootstrapMode, type CliBootstrapMode } from './lib/cli-bootstrap-mode.js'
|
|
9
10
|
import { flushBeforeExit } from './lib/flush-before-exit.js'
|
|
10
11
|
// `run` is imported dynamically inside `main()` so telemetry can initialize
|
|
@@ -72,6 +73,10 @@ async function tryBootstrap(mode: Exclude<CliBootstrapMode, 'none'>): Promise<bo
|
|
|
72
73
|
|
|
73
74
|
async function main(): Promise<void> {
|
|
74
75
|
assertNode24Runtime()
|
|
76
|
+
// Commands and workers run in plain Node, where a bare `next/<subpath>` such
|
|
77
|
+
// as `next/dynamic` or `next/link` does not resolve (#7102). Install the
|
|
78
|
+
// retry-with-`.js` hook before any package dist can be imported.
|
|
79
|
+
installNextSubpathResolveHook()
|
|
75
80
|
const bootstrapMode = resolveCliBootstrapMode(process.argv)
|
|
76
81
|
|
|
77
82
|
if (bootstrapMode !== 'none') {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Regression (#7102): CLI commands and workers load package dists that import
|
|
6
|
+
* bare `next/dynamic`, `next/link`, … which plain Node ESM cannot resolve. The
|
|
7
|
+
* entry must install the resolve hook before anything can import such a dist.
|
|
8
|
+
*/
|
|
9
|
+
describe('bin.ts next/<subpath> resolve hook', () => {
|
|
10
|
+
const binSource = fs.readFileSync(path.resolve(__dirname, '../../bin.ts'), 'utf8')
|
|
11
|
+
|
|
12
|
+
it('imports the shared resolve hook', () => {
|
|
13
|
+
expect(binSource).toContain(
|
|
14
|
+
"import { installNextSubpathResolveHook } from '@open-mercato/shared/lib/bootstrap/next-subpath-resolve-hook'",
|
|
15
|
+
)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('installs the hook before bootstrapping or loading the mercato entry', () => {
|
|
19
|
+
const installCall = binSource.indexOf('installNextSubpathResolveHook()')
|
|
20
|
+
expect(installCall).toBeGreaterThan(-1)
|
|
21
|
+
expect(installCall).toBeLessThan(binSource.indexOf('await loadAppEnv()'))
|
|
22
|
+
expect(installCall).toBeLessThan(binSource.indexOf('await tryBootstrap(bootstrapMode)'))
|
|
23
|
+
expect(installCall).toBeLessThan(binSource.indexOf("const { run } = await import('./mercato.js')"))
|
|
24
|
+
})
|
|
25
|
+
})
|
package/src/mercato.ts
CHANGED
|
@@ -327,7 +327,11 @@ async function ensureEnvLoaded(options: { createIfMissing?: boolean; quiet?: boo
|
|
|
327
327
|
const examplePath = path.join(appDir, '.env.example')
|
|
328
328
|
if (fs.existsSync(examplePath)) {
|
|
329
329
|
fs.copyFileSync(examplePath, envPath)
|
|
330
|
-
console.log(
|
|
330
|
+
console.log('')
|
|
331
|
+
console.log('⚠️ No .env was found, so one was created from .env.example.')
|
|
332
|
+
console.log(` ${envPath}`)
|
|
333
|
+
console.log(' It carries the example placeholders — review DATABASE_URL and secrets before continuing.')
|
|
334
|
+
console.log('')
|
|
331
335
|
}
|
|
332
336
|
}
|
|
333
337
|
if (fs.existsSync(envPath)) {
|
|
@@ -894,14 +898,67 @@ async function buildAllModules(): Promise<Module[]> {
|
|
|
894
898
|
return all
|
|
895
899
|
}
|
|
896
900
|
|
|
901
|
+
export function isHelpRequest(args: string[]): boolean {
|
|
902
|
+
return args.some((arg) => arg === '--help' || arg === '-h')
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Top-level commands that return before the module dispatcher and therefore never reach its
|
|
906
|
+
// generic help branch. Without an entry here, `mercato init --help` runs a full install.
|
|
907
|
+
const TOP_LEVEL_COMMAND_HELP: Record<string, string[]> = {
|
|
908
|
+
init: [
|
|
909
|
+
'Usage: mercato init [--reinstall|-r] [--no-examples] [--stresstest [--lite] [--count=<n>]]',
|
|
910
|
+
'Initializes this app: installs dependencies, runs generators, applies migrations and seeds data.',
|
|
911
|
+
],
|
|
912
|
+
'agentic:init': [
|
|
913
|
+
'Usage: mercato agentic:init [--force]',
|
|
914
|
+
'Scaffolds the agentic harness (AGENTS.md, .ai/ config and skills) into this app.',
|
|
915
|
+
],
|
|
916
|
+
'seed:defaults': [
|
|
917
|
+
'Usage: mercato seed:defaults [--module <moduleId>]',
|
|
918
|
+
'Runs every enabled module\'s seedDefaults hook, or only the named module\'s.',
|
|
919
|
+
],
|
|
920
|
+
'umes:list': [
|
|
921
|
+
'Usage: mercato umes:list',
|
|
922
|
+
'Lists the extension points every enabled module declares.',
|
|
923
|
+
],
|
|
924
|
+
'umes:inspect': [
|
|
925
|
+
'Usage: mercato umes:inspect --module <moduleId>',
|
|
926
|
+
'Prints the extension surface a single module declares.',
|
|
927
|
+
],
|
|
928
|
+
'umes:check': [
|
|
929
|
+
'Usage: mercato umes:check',
|
|
930
|
+
'Validates declared extension points against the modules that implement them.',
|
|
931
|
+
],
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// A help invocation must never materialize .env from .env.example — asking what the flags are
|
|
935
|
+
// should not mutate the working tree, least of all with the example's placeholder DATABASE_URL.
|
|
936
|
+
export function resolveEnvBootstrapOptions(parts: string[]): { createIfMissing: boolean; quiet: boolean } {
|
|
937
|
+
const first = parts[0]
|
|
938
|
+
const opaqueEnvCommand = first === 'deploy' || first === 'telemetry'
|
|
939
|
+
const helpRequested = isHelpRequest(parts)
|
|
940
|
+
return {
|
|
941
|
+
createIfMissing: !helpRequested && !opaqueEnvCommand,
|
|
942
|
+
quiet: helpRequested || opaqueEnvCommand,
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
897
946
|
export async function run(argv = process.argv) {
|
|
898
947
|
const [, , ...parts] = argv
|
|
899
948
|
const [first, second, ...remaining] = parts
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
949
|
+
// Help must never have side effects: it must not create .env, and must not fall through
|
|
950
|
+
// to the command it is asking about.
|
|
951
|
+
const helpRequested = isHelpRequest(parts)
|
|
952
|
+
await ensureEnvLoaded(resolveEnvBootstrapOptions(parts))
|
|
953
|
+
|
|
954
|
+
// Object.hasOwn, not a truthiness check: the map is indexed with raw user input, and an
|
|
955
|
+
// inherited Object.prototype member (`constructor`, `toString`, …) would otherwise pass the
|
|
956
|
+
// guard and then fail to iterate instead of reporting the module as not found.
|
|
957
|
+
if (helpRequested && first && Object.hasOwn(TOP_LEVEL_COMMAND_HELP, first)) {
|
|
958
|
+
for (const line of TOP_LEVEL_COMMAND_HELP[first]) console.log(` ${line}`)
|
|
959
|
+
return 0
|
|
960
|
+
}
|
|
961
|
+
|
|
905
962
|
// Handle init command directly
|
|
906
963
|
if (first === 'init') {
|
|
907
964
|
const { execSync } = await import('child_process')
|
|
@@ -1633,6 +1690,8 @@ export async function run(argv = process.argv) {
|
|
|
1633
1690
|
cli: [
|
|
1634
1691
|
{
|
|
1635
1692
|
command: 'railway',
|
|
1693
|
+
// Parses --help/-h itself and prints railwayDeployHelp() without deploying.
|
|
1694
|
+
handlesHelp: true,
|
|
1636
1695
|
run: async (args: string[]) => {
|
|
1637
1696
|
const { runRailwayDeploy } = await import('./lib/deploy/railway/index')
|
|
1638
1697
|
await runRailwayDeploy(args)
|
|
@@ -2001,6 +2060,7 @@ export async function run(argv = process.argv) {
|
|
|
2001
2060
|
cli: [
|
|
2002
2061
|
{
|
|
2003
2062
|
command: 'generate',
|
|
2063
|
+
help: 'Generates a migration per module from the current ORM entities and refreshes each\nmodule\'s .snapshot-open-mercato.json. Writes files only — nothing is applied.',
|
|
2004
2064
|
run: async () => {
|
|
2005
2065
|
const { createResolver } = await import('./lib/resolver')
|
|
2006
2066
|
const { dbGenerate } = await import('./lib/db')
|
|
@@ -2010,6 +2070,7 @@ export async function run(argv = process.argv) {
|
|
|
2010
2070
|
},
|
|
2011
2071
|
{
|
|
2012
2072
|
command: 'migrate',
|
|
2073
|
+
help: 'Applies every pending migration to the database DATABASE_URL points at.\nThis mutates the database — check .env before running it.',
|
|
2013
2074
|
run: async () => {
|
|
2014
2075
|
const { createResolver } = await import('./lib/resolver')
|
|
2015
2076
|
const { dbMigrate } = await import('./lib/db')
|
|
@@ -2019,6 +2080,7 @@ export async function run(argv = process.argv) {
|
|
|
2019
2080
|
},
|
|
2020
2081
|
{
|
|
2021
2082
|
command: 'greenfield',
|
|
2083
|
+
help: 'Drops and recreates the schema from scratch, then re-applies every migration.\nDestroys all data. Pass --yes (-y) to skip the confirmation prompt.',
|
|
2022
2084
|
run: async (args: string[]) => {
|
|
2023
2085
|
const { createResolver } = await import('./lib/resolver')
|
|
2024
2086
|
const { dbGreenfield } = await import('./lib/db')
|
|
@@ -2689,12 +2751,31 @@ export async function run(argv = process.argv) {
|
|
|
2689
2751
|
console.log(pad(`Commands for "${modName}": ${mod.cli.map((c) => c.command).join(', ')}`))
|
|
2690
2752
|
return 1
|
|
2691
2753
|
}
|
|
2754
|
+
// A help flag in the command slot (`mercato db --help`) asks what this module offers.
|
|
2755
|
+
if (isHelpRequest([cmdName])) {
|
|
2756
|
+
console.log(pad(`Usage: ✨ mercato ${modName} <command> [args]`))
|
|
2757
|
+
console.log(pad(`Commands for "${modName}": ${mod.cli.map((c) => c.command).join(', ')}`))
|
|
2758
|
+
return 0
|
|
2759
|
+
}
|
|
2692
2760
|
const cmd = mod.cli.find((c) => c.command === cmdName)
|
|
2693
2761
|
if (!cmd) {
|
|
2694
2762
|
console.error(`🤔 Unknown command "${cmdName}". Available: ${mod.cli.map((c) => c.command).join(', ')}`)
|
|
2695
2763
|
return 1
|
|
2696
2764
|
}
|
|
2697
2765
|
|
|
2766
|
+
// `--help` must describe the command, never run it. Commands that parse the flag themselves
|
|
2767
|
+
// opt out via `handlesHelp` so their own richer usage text still wins.
|
|
2768
|
+
if (isHelpRequest(rest) && !cmd.handlesHelp) {
|
|
2769
|
+
console.log(pad(`Usage: ✨ mercato ${modName} ${cmdName} [args]`))
|
|
2770
|
+
if (cmd.help) {
|
|
2771
|
+
console.log('')
|
|
2772
|
+
for (const line of cmd.help.split('\n')) console.log(pad(line))
|
|
2773
|
+
}
|
|
2774
|
+
console.log('')
|
|
2775
|
+
console.log(pad(`Commands for "${modName}": ${mod.cli.map((c) => c.command).join(', ')}`))
|
|
2776
|
+
return 0
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2698
2779
|
console.log('')
|
|
2699
2780
|
const started = Date.now()
|
|
2700
2781
|
const loggedArgs = modName === 'deploy' && cmdName === 'railway'
|