@open-mercato/cli 0.9.1-develop.7477.1.64ba865101 → 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.
Files changed (90) hide show
  1. package/dist/agentic/guides/module-facts.json +172 -172
  2. package/dist/agentic/guides/module-facts.v2.json +172 -172
  3. package/dist/agentic/guides/modules/agent_orchestrator/index.md +1 -1
  4. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  5. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  6. package/dist/agentic/guides/modules/api_keys/index.md +1 -1
  7. package/dist/agentic/guides/modules/attachments/index.md +1 -1
  8. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  9. package/dist/agentic/guides/modules/auth/index.md +1 -1
  10. package/dist/agentic/guides/modules/availability/index.md +1 -1
  11. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  12. package/dist/agentic/guides/modules/catalog/index.md +1 -1
  13. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  14. package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
  15. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  16. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  17. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  18. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  19. package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
  20. package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
  21. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  22. package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
  23. package/dist/agentic/guides/modules/configs/index.md +1 -1
  24. package/dist/agentic/guides/modules/content/index.md +1 -1
  25. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  26. package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
  27. package/dist/agentic/guides/modules/customer_groups/index.md +1 -1
  28. package/dist/agentic/guides/modules/customers/active-extension-bindings.md +4 -4
  29. package/dist/agentic/guides/modules/customers/index.md +1 -1
  30. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  31. package/dist/agentic/guides/modules/data_sync/di-registrations-rich.md +8 -8
  32. package/dist/agentic/guides/modules/data_sync/di-service-tokens.md +4 -4
  33. package/dist/agentic/guides/modules/data_sync/exact-override-targets.md +8 -8
  34. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  35. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  36. package/dist/agentic/guides/modules/devices/index.md +1 -1
  37. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  38. package/dist/agentic/guides/modules/directory/index.md +1 -1
  39. package/dist/agentic/guides/modules/documents/index.md +1 -1
  40. package/dist/agentic/guides/modules/ecommerce/index.md +1 -1
  41. package/dist/agentic/guides/modules/entities/index.md +1 -1
  42. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  43. package/dist/agentic/guides/modules/events/index.md +1 -1
  44. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  45. package/dist/agentic/guides/modules/forms/index.md +1 -1
  46. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  47. package/dist/agentic/guides/modules/gateway_tpay/index.md +1 -1
  48. package/dist/agentic/guides/modules/generators/index.md +1 -1
  49. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  50. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  51. package/dist/agentic/guides/modules/messages/index.md +1 -1
  52. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  53. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  54. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  55. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  56. package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
  57. package/dist/agentic/guides/modules/planner/index.md +1 -1
  58. package/dist/agentic/guides/modules/portal/index.md +1 -1
  59. package/dist/agentic/guides/modules/progress/index.md +1 -1
  60. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  61. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  62. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  63. package/dist/agentic/guides/modules/resources/index.md +1 -1
  64. package/dist/agentic/guides/modules/sales/index.md +1 -1
  65. package/dist/agentic/guides/modules/scheduler/index.md +1 -1
  66. package/dist/agentic/guides/modules/search/index.md +1 -1
  67. package/dist/agentic/guides/modules/security/index.md +1 -1
  68. package/dist/agentic/guides/modules/seeds/index.md +1 -1
  69. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  70. package/dist/agentic/guides/modules/sso/index.md +1 -1
  71. package/dist/agentic/guides/modules/staff/index.md +1 -1
  72. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  73. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  74. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  75. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  76. package/dist/agentic/guides/modules/telemetry/index.md +1 -1
  77. package/dist/agentic/guides/modules/tillio/index.md +1 -1
  78. package/dist/agentic/guides/modules/translations/index.md +1 -1
  79. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  80. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  81. package/dist/agentic/guides/modules/wms/index.md +1 -1
  82. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  83. package/dist/agentic/guides/reference-module-facts.json +1 -1
  84. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +4 -0
  85. package/dist/agentic/guides/upstream/manifest.json +2 -2
  86. package/dist/mercato.js +71 -5
  87. package/dist/mercato.js.map +2 -2
  88. package/package.json +7 -7
  89. package/src/__tests__/mercato.test.ts +260 -0
  90. 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.7477.1.64ba865101",
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.7477.1.64ba865101",
64
- "@open-mercato/queue": "0.9.1-develop.7477.1.64ba865101",
65
- "@open-mercato/shared": "0.9.1-develop.7477.1.64ba865101",
66
- "@open-mercato/telemetry": "0.9.1-develop.7477.1.64ba865101",
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.7477.1.64ba865101"
77
+ "@open-mercato/shared": "0.9.1-develop.7496.1.3346415f94"
78
78
  },
79
79
  "devDependencies": {
80
- "@open-mercato/shared": "0.9.1-develop.7477.1.64ba865101",
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/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(`📋 Copied .env.example → .env (edit ${envPath} to customize)`)
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
- await ensureEnvLoaded({
901
- createIfMissing: first !== 'deploy' && first !== 'telemetry',
902
- quiet: first === 'deploy' || first === 'telemetry',
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'