@open-mercato/cli 0.7.1-develop.7185.1.0f280ef1f1 → 0.7.1-develop.7186.1.6e080a5017

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 (91) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/AGENTS.md +1 -1
  3. package/dist/agentic/guides/module-facts.json +136 -136
  4. package/dist/agentic/guides/module-facts.v2.json +136 -136
  5. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  6. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  7. package/dist/agentic/guides/modules/api_keys/index.md +1 -1
  8. package/dist/agentic/guides/modules/attachments/index.md +1 -1
  9. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  10. package/dist/agentic/guides/modules/auth/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/customers/index.md +1 -1
  28. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  29. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  30. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  31. package/dist/agentic/guides/modules/devices/index.md +1 -1
  32. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  33. package/dist/agentic/guides/modules/directory/index.md +1 -1
  34. package/dist/agentic/guides/modules/documents/index.md +1 -1
  35. package/dist/agentic/guides/modules/entities/index.md +1 -1
  36. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  37. package/dist/agentic/guides/modules/events/index.md +1 -1
  38. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  39. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  40. package/dist/agentic/guides/modules/generators/index.md +1 -1
  41. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  42. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  43. package/dist/agentic/guides/modules/messages/index.md +1 -1
  44. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  45. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  46. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  47. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  48. package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
  49. package/dist/agentic/guides/modules/planner/index.md +1 -1
  50. package/dist/agentic/guides/modules/portal/index.md +1 -1
  51. package/dist/agentic/guides/modules/progress/index.md +1 -1
  52. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  53. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  54. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  55. package/dist/agentic/guides/modules/resources/index.md +1 -1
  56. package/dist/agentic/guides/modules/sales/index.md +1 -1
  57. package/dist/agentic/guides/modules/scheduler/index.md +1 -1
  58. package/dist/agentic/guides/modules/search/index.md +1 -1
  59. package/dist/agentic/guides/modules/security/index.md +1 -1
  60. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  61. package/dist/agentic/guides/modules/sso/index.md +1 -1
  62. package/dist/agentic/guides/modules/staff/index.md +1 -1
  63. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  64. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  65. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  66. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  67. package/dist/agentic/guides/modules/tillio/index.md +1 -1
  68. package/dist/agentic/guides/modules/translations/index.md +1 -1
  69. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  70. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  71. package/dist/agentic/guides/modules/wms/index.md +1 -1
  72. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  73. package/dist/agentic/guides/reference-module-facts.json +1 -1
  74. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +1 -0
  75. package/dist/agentic/guides/upstream/manifest.json +2 -2
  76. package/dist/lib/generate-watch-structure.js +1 -0
  77. package/dist/lib/generate-watch-structure.js.map +2 -2
  78. package/dist/lib/generators/module-registry.js +45 -0
  79. package/dist/lib/generators/module-registry.js.map +2 -2
  80. package/dist/lib/module-runtimes.js +99 -0
  81. package/dist/lib/module-runtimes.js.map +7 -0
  82. package/dist/mercato.js +9 -0
  83. package/dist/mercato.js.map +2 -2
  84. package/package.json +6 -6
  85. package/src/__tests__/module-runtimes.test.ts +267 -0
  86. package/src/lib/__tests__/generate-watch-structure.test.ts +18 -0
  87. package/src/lib/generate-watch-structure.ts +1 -0
  88. package/src/lib/generators/__tests__/registry-variant-parity.test.ts +28 -1
  89. package/src/lib/generators/module-registry.ts +52 -0
  90. package/src/lib/module-runtimes.ts +195 -0
  91. package/src/mercato.ts +20 -0
@@ -0,0 +1,195 @@
1
+ // Starting and stopping the module runtimes declared by `runtime.ts` (SPEC-072).
2
+ //
3
+ // Kept out of mercato.ts so the contract can be tested directly: the ordering, the failure
4
+ // semantics, the shutdown and both timeouts are the whole substance of this hook, and none of
5
+ // them are reachable through a CLI command in a test.
6
+
7
+ import type { AppContainer } from '@open-mercato/shared/lib/di/container'
8
+ import {
9
+ moduleRuntimeAppliesTo,
10
+ type ModuleRuntime,
11
+ type ModuleRuntimeHandle,
12
+ type ModuleRuntimeRole,
13
+ } from '@open-mercato/shared/modules/runtime'
14
+
15
+ export const DEFAULT_START_TIMEOUT_MS = 30_000
16
+ export const DEFAULT_STOP_TIMEOUT_MS = 30_000
17
+
18
+ export const START_TIMEOUT_ENV_VAR = 'OM_MODULE_RUNTIME_START_TIMEOUT_MS'
19
+ export const STOP_TIMEOUT_ENV_VAR = 'OM_MODULE_RUNTIME_STOP_TIMEOUT_MS'
20
+
21
+ export type ModuleRuntimeCarrier = { id: string; runtime?: ModuleRuntime }
22
+
23
+ export type StartModuleRuntimesOptions = {
24
+ modules: ModuleRuntimeCarrier[]
25
+ container: AppContainer
26
+ role: ModuleRuntimeRole
27
+ /** One line per start and stop. Defaults to console. */
28
+ log?: (message: string) => void
29
+ /** Defaults to `OM_MODULE_RUNTIME_START_TIMEOUT_MS`, then 30s. */
30
+ startTimeoutMs?: number
31
+ /** Defaults to `OM_MODULE_RUNTIME_STOP_TIMEOUT_MS`, then 30s. */
32
+ stopTimeoutMs?: number
33
+ /** Where the timeout overrides are read from. Defaults to `process.env`. */
34
+ env?: NodeJS.ProcessEnv
35
+ }
36
+
37
+ export type StartedModuleRuntimes = {
38
+ /** Module ids whose runtime is running, in start order. */
39
+ started: string[]
40
+ /** Aborts the shared signal, then stops each runtime in reverse start order. Idempotent. */
41
+ stop(): Promise<void>
42
+ }
43
+
44
+ class ModuleRuntimeTimeoutError extends Error {
45
+ constructor(moduleId: string, phase: 'start' | 'stop', timeoutMs: number) {
46
+ super(`Module "${moduleId}" did not ${phase} within ${timeoutMs}ms.`)
47
+ this.name = 'ModuleRuntimeTimeoutError'
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Reads a timeout override, falling back to `fallbackMs` when it is unset or not a usable number.
53
+ *
54
+ * A start timeout is fatal by design (contract §3), so an operator whose runtime legitimately needs
55
+ * longer than the default — acquiring a lease, waiting on a broker — needs a supported way to say
56
+ * so rather than a worker that exits non-zero on every boot. A malformed value falls back loudly:
57
+ * refusing to start over a typo in a tuning knob would be worse than the default it replaces.
58
+ */
59
+ function resolveTimeoutMs(
60
+ env: NodeJS.ProcessEnv,
61
+ name: string,
62
+ fallbackMs: number,
63
+ log: (message: string) => void,
64
+ ): number {
65
+ const raw = env[name]
66
+ if (raw == null || raw.trim() === '') return fallbackMs
67
+ const parsed = Number(raw)
68
+ if (!Number.isFinite(parsed) || parsed <= 0) {
69
+ log(`[runtime] ignoring ${name}="${raw}": expected a positive number of milliseconds, using ${fallbackMs}ms`)
70
+ return fallbackMs
71
+ }
72
+ return parsed
73
+ }
74
+
75
+ /**
76
+ * Rejects if `work` outlives `timeoutMs`.
77
+ *
78
+ * The timer is always cleared, including on the winning path: an uncleared timer keeps the event
79
+ * loop alive, so a CLI command that finished its work would hang until the timeout elapsed — the
80
+ * kind of bug that only shows up as "the process takes 30 seconds to exit".
81
+ */
82
+ async function withTimeout<T>(work: Promise<T>, timeoutMs: number, onTimeout: () => Error): Promise<T> {
83
+ let timer: NodeJS.Timeout | undefined
84
+ try {
85
+ return await Promise.race([
86
+ work,
87
+ new Promise<never>((_resolve, reject) => {
88
+ timer = setTimeout(() => reject(onTimeout()), timeoutMs)
89
+ }),
90
+ ])
91
+ } finally {
92
+ if (timer) clearTimeout(timer)
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Starts every module runtime that applies to this role, once.
98
+ *
99
+ * A throwing or timing-out `start` is fatal: the runtimes already started are stopped, and the
100
+ * error is rethrown for the caller to exit on. A process that came up without a runtime it was
101
+ * supposed to have looks healthy while doing nothing, which is worse than not coming up.
102
+ */
103
+ export async function startModuleRuntimes(options: StartModuleRuntimesOptions): Promise<StartedModuleRuntimes> {
104
+ const log = options.log ?? ((message: string) => console.log(message))
105
+ const env = options.env ?? process.env
106
+ const startTimeoutMs = options.startTimeoutMs
107
+ ?? resolveTimeoutMs(env, START_TIMEOUT_ENV_VAR, DEFAULT_START_TIMEOUT_MS, log)
108
+ const stopTimeoutMs = options.stopTimeoutMs
109
+ ?? resolveTimeoutMs(env, STOP_TIMEOUT_ENV_VAR, DEFAULT_STOP_TIMEOUT_MS, log)
110
+
111
+ const controller = new AbortController()
112
+ const started: Array<{ id: string; handle: ModuleRuntimeHandle | void }> = []
113
+
114
+ // Sorted by module id so a failure is reproducible rather than dependent on registry order.
115
+ // Compared by code point rather than `localeCompare`, which depends on the host's default locale
116
+ // and ICU build — "reproducible" must not mean "on machines with the same locale".
117
+ //
118
+ // No dependency graph on purpose: a module that needs another module's runtime should depend on
119
+ // its service through DI, which already expresses that and already detects cycles.
120
+ const applicable = options.modules
121
+ .filter((m): m is ModuleRuntimeCarrier & { runtime: ModuleRuntime } =>
122
+ Boolean(m.runtime) && moduleRuntimeAppliesTo(m.runtime as ModuleRuntime, options.role))
123
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
124
+
125
+ const stopStarted = async (): Promise<void> => {
126
+ controller.abort()
127
+ for (const entry of [...started].reverse()) {
128
+ if (!entry.handle) continue
129
+ try {
130
+ await withTimeout(
131
+ Promise.resolve(entry.handle.stop()),
132
+ stopTimeoutMs,
133
+ () => new ModuleRuntimeTimeoutError(entry.id, 'stop', stopTimeoutMs),
134
+ )
135
+ log(`[runtime] stopped "${entry.id}"`)
136
+ } catch (error) {
137
+ // A shutdown that cannot finish must not become a shutdown that never finishes: report and
138
+ // carry on to the next runtime, so one stuck module cannot hold the process open.
139
+ log(`[runtime] "${entry.id}" failed to stop: ${error instanceof Error ? error.message : String(error)}`)
140
+ }
141
+ }
142
+ started.length = 0
143
+ }
144
+
145
+ for (const module of applicable) {
146
+ const startedAt = Date.now()
147
+ // Called through an async wrapper so a `start` that throws synchronously rejects rather than
148
+ // escaping the try below, which would skip stopping the runtimes already started.
149
+ const starting = (async () =>
150
+ module.runtime.start({ container: options.container, role: options.role, signal: controller.signal }))()
151
+ try {
152
+ const handle = await withTimeout(
153
+ starting,
154
+ startTimeoutMs,
155
+ () => new ModuleRuntimeTimeoutError(module.id, 'start', startTimeoutMs),
156
+ )
157
+ started.push({ id: module.id, handle })
158
+ log(`[runtime] started "${module.id}" (${options.role}, ${Date.now() - startedAt}ms)`)
159
+ } catch (error) {
160
+ // A timed-out `start` keeps running: its handle was never pushed to `started`, so shutdown
161
+ // would not reach it. Trail the promise and release whatever it eventually hands back — and
162
+ // swallow a late rejection, which would otherwise surface as an unhandled rejection long
163
+ // after the error below has already been reported.
164
+ if (error instanceof ModuleRuntimeTimeoutError) {
165
+ void starting
166
+ .then((handle) => handle?.stop())
167
+ .catch(() => {})
168
+ }
169
+ await stopStarted()
170
+ throw error
171
+ }
172
+ }
173
+
174
+ let stopping: Promise<void> | null = null
175
+ return {
176
+ started: started.map((entry) => entry.id),
177
+ stop: () => (stopping ??= stopStarted()),
178
+ }
179
+ }
180
+
181
+ /**
182
+ * True when this process is a Next production build.
183
+ *
184
+ * A build evaluates application code and must not acquire brokers, sockets or leases — and must
185
+ * certainly not briefly own work it cannot finish. Hosts have been carrying this check by hand;
186
+ * it belongs with the runner.
187
+ *
188
+ * At its only call site today — `mercato queue worker --all` — it is always false: `NEXT_PHASE` is
189
+ * set by Next, not by a worker process. It guards the entry that matters once the `server` role
190
+ * lands, where module code really is evaluated during `next build`; it is here so that entry
191
+ * inherits the check rather than reinventing it.
192
+ */
193
+ export function isProductionBuildPhase(env: NodeJS.ProcessEnv = process.env): boolean {
194
+ return env.NEXT_PHASE === 'phase-production-build'
195
+ }
package/src/mercato.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // Commands that need to run before generation (e.g., `init`) handle missing modules gracefully.
3
3
 
4
4
  import { registerWorkerShutdownHook, runWorker } from '@open-mercato/queue/worker'
5
+ import { isProductionBuildPhase, startModuleRuntimes } from './lib/module-runtimes'
5
6
  import type { Module, ModuleWorker } from '@open-mercato/shared/modules/registry'
6
7
  import { getCliModules, hasCliModules, registerCliModules } from './registry'
7
8
  export { getCliModules, hasCliModules, registerCliModules }
@@ -1741,6 +1742,25 @@ export async function run(argv = process.argv) {
1741
1742
  console.log('[worker] Local scheduler started in the shared worker process.')
1742
1743
  }
1743
1744
 
1745
+ // SPEC-072 — module runtimes, once per process. After the queue workers are bound so
1746
+ // a runtime may rely on them, and before the process announces itself as up.
1747
+ //
1748
+ // Only on `--all`: that is the process a deployment runs and the one `server start`
1749
+ // spawns. A single-queue worker is a targeted invocation, and starting every module's
1750
+ // runtime in each of N of them would run N copies of each.
1751
+ //
1752
+ // The build-phase guard is always false here — NEXT_PHASE is set by Next, not by a
1753
+ // worker — and is kept so the check lives with the runner rather than being reinvented
1754
+ // by the Next-side entry, where module code really is evaluated during `next build`.
1755
+ if (!isProductionBuildPhase()) {
1756
+ const runtimes = await startModuleRuntimes({
1757
+ modules: getCliModules(),
1758
+ container: await createRequestContainer(),
1759
+ role: 'worker',
1760
+ })
1761
+ if (runtimes.started.length > 0) registerWorkerShutdownHook(() => runtimes.stop())
1762
+ }
1763
+
1744
1764
  console.log('[worker] All workers started. Press Ctrl+C to stop')
1745
1765
 
1746
1766
  // Keep the process alive