@open-mercato/cli 0.6.3-develop.3894.1.352abf4240 → 0.6.3
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -1
- package/dist/agentic/shared/AGENTS.md.template +1 -0
- package/dist/lib/dev-env-reload.js +40 -1
- package/dist/lib/dev-env-reload.js.map +2 -2
- package/dist/lib/generate-watch-structure.js +153 -0
- package/dist/lib/generate-watch-structure.js.map +7 -0
- package/dist/lib/testing/integration.js +14 -0
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +244 -98
- package/dist/mercato.js.map +2 -2
- package/package.json +7 -8
- package/src/__tests__/mercato.test.ts +13 -3
- package/src/lib/__tests__/dev-env-reload.test.ts +21 -1
- package/src/lib/__tests__/generate-watch-structure.test.ts +80 -0
- package/src/lib/dev-env-reload.ts +47 -0
- package/src/lib/generate-watch-structure.ts +170 -0
- package/src/lib/testing/integration.ts +14 -0
- package/src/mercato.ts +241 -67
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.6.3
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -59,21 +59,21 @@
|
|
|
59
59
|
"@mikro-orm/decorators": "^7.1.1",
|
|
60
60
|
"@mikro-orm/migrations": "^7.1.1",
|
|
61
61
|
"@mikro-orm/postgresql": "^7.1.1",
|
|
62
|
-
"@open-mercato/queue": "0.6.3
|
|
63
|
-
"@open-mercato/shared": "0.6.3
|
|
62
|
+
"@open-mercato/queue": "0.6.3",
|
|
63
|
+
"@open-mercato/shared": "0.6.3",
|
|
64
64
|
"cross-spawn": "^7.0.6",
|
|
65
65
|
"pg": "8.21.0",
|
|
66
66
|
"semver": "^7.8.1",
|
|
67
|
-
"testcontainers": "^12.0.
|
|
67
|
+
"testcontainers": "^12.0.1",
|
|
68
68
|
"ts-morph": "^28.0.0",
|
|
69
69
|
"tsx": "^4.22.3",
|
|
70
70
|
"typescript": "^6.0.3"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@open-mercato/shared": "0.6.3
|
|
73
|
+
"@open-mercato/shared": "0.6.3"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.6.3
|
|
76
|
+
"@open-mercato/shared": "0.6.3",
|
|
77
77
|
"@types/jest": "^30.0.0",
|
|
78
78
|
"jest": "^30.4.2",
|
|
79
79
|
"ts-jest": "^29.4.11"
|
|
@@ -85,6 +85,5 @@
|
|
|
85
85
|
"type": "git",
|
|
86
86
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
87
87
|
"directory": "packages/cli"
|
|
88
|
-
}
|
|
89
|
-
"stableVersion": "0.6.2"
|
|
88
|
+
}
|
|
90
89
|
}
|
|
@@ -37,9 +37,18 @@ function buildMockChildProcessModule(routeAutoExit: MockChildSpawnRouter) {
|
|
|
37
37
|
if (autoExit) {
|
|
38
38
|
queueMicrotask(() => {
|
|
39
39
|
if (child.exitCode !== null || child.signalCode !== null) return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
if (pathIncludes(spawnargs[1] ?? '', 'next/dist/bin/next') && spawnargs.includes('dev')) {
|
|
41
|
+
child.stdout.emit('data', '✓ Ready in 1ms\n')
|
|
42
|
+
}
|
|
43
|
+
queueMicrotask(() => {
|
|
44
|
+
child.exitCode = autoExit.code
|
|
45
|
+
child.signalCode = autoExit.signal ?? null
|
|
46
|
+
child.emit('exit', child.exitCode, child.signalCode)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
} else if (pathIncludes(spawnargs[1] ?? '', 'next/dist/bin/next') && spawnargs.includes('dev')) {
|
|
50
|
+
queueMicrotask(() => {
|
|
51
|
+
child.stdout.emit('data', '✓ Ready in 1ms\n')
|
|
43
52
|
})
|
|
44
53
|
}
|
|
45
54
|
|
|
@@ -1049,6 +1058,7 @@ describe('server start managed process exits', () => {
|
|
|
1049
1058
|
envChangeCallback = onChange
|
|
1050
1059
|
return jest.fn()
|
|
1051
1060
|
}),
|
|
1061
|
+
watchDevRuntimeFiles: jest.fn(() => jest.fn()),
|
|
1052
1062
|
}))
|
|
1053
1063
|
jest.doMock('../lib/generators', () => ({
|
|
1054
1064
|
generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
-
import { createDevEnvReloader, resolveDevEnvFilePaths } from '../dev-env-reload'
|
|
4
|
+
import { createDevEnvReloader, resolveDevEnvFilePaths, watchDevRuntimeFiles } from '../dev-env-reload'
|
|
5
5
|
import { normalizeTestPath } from './path-helpers'
|
|
6
6
|
|
|
7
7
|
describe('dev env reload helpers', () => {
|
|
@@ -60,4 +60,24 @@ describe('dev env reload helpers', () => {
|
|
|
60
60
|
expect(environment.SHELL_VALUE).toBe('shell-value')
|
|
61
61
|
expect(environment.REMOVED_LATER).toBeUndefined()
|
|
62
62
|
})
|
|
63
|
+
|
|
64
|
+
it('watches generated runtime files when explicitly requested', async () => {
|
|
65
|
+
const generatedDir = path.join(appDir, '.mercato', 'generated')
|
|
66
|
+
fs.mkdirSync(generatedDir, { recursive: true })
|
|
67
|
+
const generatedFile = path.join(generatedDir, 'backend-routes.generated.ts')
|
|
68
|
+
fs.writeFileSync(generatedFile, 'export const routes = []\n')
|
|
69
|
+
|
|
70
|
+
let stop = () => {}
|
|
71
|
+
const seen = new Promise<string>((resolve) => {
|
|
72
|
+
stop = watchDevRuntimeFiles(appDir, (filePath) => {
|
|
73
|
+
stop()
|
|
74
|
+
resolve(filePath)
|
|
75
|
+
}, { debounceMs: 10 })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
79
|
+
fs.writeFileSync(generatedFile, 'export const routes = [1]\n')
|
|
80
|
+
|
|
81
|
+
await expect(seen.finally(stop)).resolves.toBe(generatedFile)
|
|
82
|
+
})
|
|
63
83
|
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { calculateGenerateWatchStructureChecksum } from '../generate-watch-structure'
|
|
5
|
+
|
|
6
|
+
function write(filePath: string, content: string): void {
|
|
7
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
8
|
+
fs.writeFileSync(filePath, content)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('calculateGenerateWatchStructureChecksum', () => {
|
|
12
|
+
let root: string
|
|
13
|
+
let appDir: string
|
|
14
|
+
let pkgModule: string
|
|
15
|
+
let appModule: string
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'om-generate-watch-'))
|
|
19
|
+
appDir = path.join(root, 'apps', 'mercato')
|
|
20
|
+
pkgModule = path.join(root, 'packages', 'core', 'src', 'modules', 'customers')
|
|
21
|
+
appModule = path.join(appDir, 'src', 'modules', 'customers')
|
|
22
|
+
write(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
|
|
23
|
+
write(path.join(pkgModule, 'index.ts'), 'export const metadata = { id: "customers" }\n')
|
|
24
|
+
write(path.join(pkgModule, 'backend', 'customers', 'people', 'page.tsx'), 'export default function Page() { return null }\n')
|
|
25
|
+
write(path.join(pkgModule, 'components', 'detail', 'PersonDetailTabs.tsx'), 'export function PersonDetailTabs() { return null }\n')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
function currentChecksum(): string {
|
|
33
|
+
return calculateGenerateWatchStructureChecksum({
|
|
34
|
+
modulesFile: path.join(appDir, 'src', 'modules.ts'),
|
|
35
|
+
moduleRoots: [{ appBase: appModule, pkgBase: pkgModule }],
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
it('ignores ordinary component edits outside generator discovery paths', () => {
|
|
40
|
+
const before = currentChecksum()
|
|
41
|
+
|
|
42
|
+
write(path.join(pkgModule, 'components', 'detail', 'PersonDetailTabs.tsx'), 'export function PersonDetailTabs() { return "changed" }\n')
|
|
43
|
+
|
|
44
|
+
expect(currentChecksum()).toBe(before)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('changes when a discovered backend page is added', () => {
|
|
48
|
+
const before = currentChecksum()
|
|
49
|
+
|
|
50
|
+
write(path.join(pkgModule, 'backend', 'customers', 'companies', 'page.tsx'), 'export default function Page() { return null }\n')
|
|
51
|
+
|
|
52
|
+
expect(currentChecksum()).not.toBe(before)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('changes when route metadata changes', () => {
|
|
56
|
+
write(path.join(pkgModule, 'backend', 'customers', 'people', 'page.meta.ts'), 'export const metadata = { nav: { label: "People" } }\n')
|
|
57
|
+
const before = currentChecksum()
|
|
58
|
+
|
|
59
|
+
write(path.join(pkgModule, 'backend', 'customers', 'people', 'page.meta.ts'), 'export const metadata = { nav: { label: "Contacts" } }\n')
|
|
60
|
+
|
|
61
|
+
expect(currentChecksum()).not.toBe(before)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('changes when inline page metadata changes', () => {
|
|
65
|
+
write(path.join(pkgModule, 'backend', 'customers', 'people', 'page.tsx'), 'export const metadata = { nav: { label: "People" } }\nexport default function Page() { return null }\n')
|
|
66
|
+
const before = currentChecksum()
|
|
67
|
+
|
|
68
|
+
write(path.join(pkgModule, 'backend', 'customers', 'people', 'page.tsx'), 'export const metadata = { nav: { label: "Contacts" } }\nexport default function Page() { return null }\n')
|
|
69
|
+
|
|
70
|
+
expect(currentChecksum()).not.toBe(before)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('changes when a convention file changes', () => {
|
|
74
|
+
const before = currentChecksum()
|
|
75
|
+
|
|
76
|
+
write(path.join(pkgModule, 'acl.ts'), 'export const features = [{ id: "customers.view" }]\n')
|
|
77
|
+
|
|
78
|
+
expect(currentChecksum()).not.toBe(before)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -2,6 +2,8 @@ import dotenv from 'dotenv'
|
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
|
+
const RUNTIME_GRAPH_FILE_PATTERN = /\.generated(?:\.[a-z0-9]+)?(?:\.tsx?|\.checksum)$/i
|
|
6
|
+
|
|
5
7
|
const DEV_ENV_PRIORITY = [
|
|
6
8
|
'.env',
|
|
7
9
|
'.env.development',
|
|
@@ -108,3 +110,48 @@ export function watchDevEnvFiles(
|
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
}
|
|
113
|
+
|
|
114
|
+
export function watchDevRuntimeFiles(
|
|
115
|
+
appDir: string,
|
|
116
|
+
onChange: (filePath: string) => void,
|
|
117
|
+
options: { debounceMs?: number } = {},
|
|
118
|
+
): () => void {
|
|
119
|
+
const debounceMs = options.debounceMs ?? 250
|
|
120
|
+
const generatedDir = path.join(appDir, '.mercato', 'generated')
|
|
121
|
+
const timers = new Map<string, NodeJS.Timeout>()
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(generatedDir)) {
|
|
124
|
+
return () => {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let watcher: fs.FSWatcher | null = null
|
|
128
|
+
try {
|
|
129
|
+
watcher = fs.watch(generatedDir, (eventType, fileName) => {
|
|
130
|
+
if (eventType !== 'change' && eventType !== 'rename') return
|
|
131
|
+
const name = String(fileName ?? '')
|
|
132
|
+
if (!name) return
|
|
133
|
+
if (!RUNTIME_GRAPH_FILE_PATTERN.test(name)) return
|
|
134
|
+
|
|
135
|
+
const filePath = path.join(generatedDir, name)
|
|
136
|
+
const existingTimer = timers.get(filePath)
|
|
137
|
+
if (existingTimer) {
|
|
138
|
+
clearTimeout(existingTimer)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
timers.set(filePath, setTimeout(() => {
|
|
142
|
+
timers.delete(filePath)
|
|
143
|
+
onChange(filePath)
|
|
144
|
+
}, debounceMs))
|
|
145
|
+
})
|
|
146
|
+
} catch {
|
|
147
|
+
return () => {}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return () => {
|
|
151
|
+
for (const timer of timers.values()) {
|
|
152
|
+
clearTimeout(timer)
|
|
153
|
+
}
|
|
154
|
+
timers.clear()
|
|
155
|
+
watcher?.close()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
MODULE_CODE_EXTENSIONS,
|
|
6
|
+
SCAN_CONFIGS,
|
|
7
|
+
scanModuleDir,
|
|
8
|
+
stripModuleCodeExtension,
|
|
9
|
+
type ModuleRoots,
|
|
10
|
+
} from './generators/scanner'
|
|
11
|
+
|
|
12
|
+
const STRUCTURAL_CONVENTION_FILES = [
|
|
13
|
+
'index.ts',
|
|
14
|
+
'cli.ts',
|
|
15
|
+
'di.ts',
|
|
16
|
+
'acl.ts',
|
|
17
|
+
'setup.ts',
|
|
18
|
+
'encryption.ts',
|
|
19
|
+
'ce.ts',
|
|
20
|
+
'search.ts',
|
|
21
|
+
'events.ts',
|
|
22
|
+
'notifications.ts',
|
|
23
|
+
'notifications.client.ts',
|
|
24
|
+
'notifications.handlers.ts',
|
|
25
|
+
'translations.ts',
|
|
26
|
+
'generators.ts',
|
|
27
|
+
'ai-tools.ts',
|
|
28
|
+
'ai-agents.ts',
|
|
29
|
+
'analytics.ts',
|
|
30
|
+
'workflows.ts',
|
|
31
|
+
'inbox-actions.ts',
|
|
32
|
+
'message-types.ts',
|
|
33
|
+
'message-objects.ts',
|
|
34
|
+
'integration.ts',
|
|
35
|
+
'security.mfa-providers.ts',
|
|
36
|
+
'security.sudo.ts',
|
|
37
|
+
'data/entities.ts',
|
|
38
|
+
'data/extensions.ts',
|
|
39
|
+
'data/fields.ts',
|
|
40
|
+
'data/enrichers.ts',
|
|
41
|
+
'data/guards.ts',
|
|
42
|
+
'api/interceptors.ts',
|
|
43
|
+
'commands/interceptors.ts',
|
|
44
|
+
'widgets/components.ts',
|
|
45
|
+
'widgets/injection-table.ts',
|
|
46
|
+
'frontend/middleware.ts',
|
|
47
|
+
'backend/middleware.ts',
|
|
48
|
+
] as const
|
|
49
|
+
|
|
50
|
+
const CONTENT_SENSITIVE_SCAN_CONFIGS = [
|
|
51
|
+
SCAN_CONFIGS.apiRoutes,
|
|
52
|
+
SCAN_CONFIGS.apiPlainFiles,
|
|
53
|
+
SCAN_CONFIGS.subscribers,
|
|
54
|
+
SCAN_CONFIGS.workers,
|
|
55
|
+
SCAN_CONFIGS.dashboardWidgets,
|
|
56
|
+
SCAN_CONFIGS.injectionWidgets,
|
|
57
|
+
] as const
|
|
58
|
+
|
|
59
|
+
const ROUTE_SHAPE_SCAN_CONFIGS = [
|
|
60
|
+
SCAN_CONFIGS.frontendPages,
|
|
61
|
+
SCAN_CONFIGS.backendPages,
|
|
62
|
+
] as const
|
|
63
|
+
|
|
64
|
+
function checksum(value: string): string {
|
|
65
|
+
return crypto.createHash('md5').update(value).digest('hex')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function fileRecord(filePath: string, base: string, mode: 'content' | 'shape'): string | null {
|
|
69
|
+
if (!fs.existsSync(filePath)) return null
|
|
70
|
+
let stat: fs.Stats
|
|
71
|
+
try {
|
|
72
|
+
stat = fs.statSync(filePath)
|
|
73
|
+
} catch {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
if (!stat.isFile()) return null
|
|
77
|
+
const rel = path.relative(base, filePath).replace(/\\/g, '/')
|
|
78
|
+
if (mode === 'shape') {
|
|
79
|
+
return `file:${rel}`
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return `file:${rel}:${stat.size}:${checksum(fs.readFileSync(filePath, 'utf8'))}`
|
|
83
|
+
} catch {
|
|
84
|
+
return `file:${rel}:unreadable`
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveCodeFile(base: string, relativePath: string): string | null {
|
|
89
|
+
const stripped = stripModuleCodeExtension(relativePath)
|
|
90
|
+
const candidates = MODULE_CODE_EXTENSIONS.map((extension) => `${stripped}${extension}`)
|
|
91
|
+
for (const candidate of candidates) {
|
|
92
|
+
const filePath = path.join(base, ...candidate.split('/'))
|
|
93
|
+
if (fs.existsSync(filePath)) return filePath
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function hasInlinePageMetadata(filePath: string): boolean {
|
|
99
|
+
try {
|
|
100
|
+
const source = fs.readFileSync(filePath, 'utf8')
|
|
101
|
+
return /\bexport\s+(?:const|let|var|function|class)\s+metadata\b/.test(source)
|
|
102
|
+
|| /\bexport\s+\{[^}]*\bmetadata\b[^}]*\}/.test(source)
|
|
103
|
+
} catch {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function addConventionRecords(records: string[], roots: ModuleRoots): void {
|
|
109
|
+
for (const base of [roots.pkgBase, roots.appBase]) {
|
|
110
|
+
records.push(`module-root:${base}:${fs.existsSync(base) ? 'present' : 'missing'}`)
|
|
111
|
+
for (const relativePath of STRUCTURAL_CONVENTION_FILES) {
|
|
112
|
+
const filePath = resolveCodeFile(base, relativePath)
|
|
113
|
+
if (!filePath) {
|
|
114
|
+
records.push(`missing:${base}:${relativePath}`)
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
const record = fileRecord(filePath, base, 'content')
|
|
118
|
+
if (record) records.push(record)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function addScannedRecords(records: string[], roots: ModuleRoots): void {
|
|
124
|
+
for (const config of CONTENT_SENSITIVE_SCAN_CONFIGS) {
|
|
125
|
+
for (const scanned of scanModuleDir(roots, config)) {
|
|
126
|
+
const base = scanned.fromApp ? roots.appBase : roots.pkgBase
|
|
127
|
+
const filePath = path.join(base, ...config.folder.split('/'), ...scanned.relPath.split('/'))
|
|
128
|
+
const record = fileRecord(filePath, base, 'content')
|
|
129
|
+
if (record) records.push(`${config.folder}:${record}`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const config of ROUTE_SHAPE_SCAN_CONFIGS) {
|
|
134
|
+
for (const scanned of scanModuleDir(roots, config)) {
|
|
135
|
+
const base = scanned.fromApp ? roots.appBase : roots.pkgBase
|
|
136
|
+
const folderPath = path.join(base, ...config.folder.split('/'))
|
|
137
|
+
const filePath = path.join(folderPath, ...scanned.relPath.split('/'))
|
|
138
|
+
const pageRecord = fileRecord(filePath, base, hasInlinePageMetadata(filePath) ? 'content' : 'shape')
|
|
139
|
+
if (pageRecord) records.push(`${config.folder}:${pageRecord}`)
|
|
140
|
+
|
|
141
|
+
const dir = path.dirname(filePath)
|
|
142
|
+
const stem = stripModuleCodeExtension(path.basename(filePath))
|
|
143
|
+
const metaCandidates = stem === 'page'
|
|
144
|
+
? ['page.meta', 'meta']
|
|
145
|
+
: [`${stem}.meta`, 'meta']
|
|
146
|
+
for (const candidate of metaCandidates) {
|
|
147
|
+
const metaPath = resolveCodeFile(dir, candidate)
|
|
148
|
+
if (!metaPath) continue
|
|
149
|
+
const record = fileRecord(metaPath, base, 'content')
|
|
150
|
+
if (record) records.push(`${config.folder}:meta:${record}`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function calculateGenerateWatchStructureChecksum(options: {
|
|
157
|
+
modulesFile: string
|
|
158
|
+
moduleRoots: ModuleRoots[]
|
|
159
|
+
}): string {
|
|
160
|
+
const records: string[] = []
|
|
161
|
+
const modulesRecord = fileRecord(options.modulesFile, path.dirname(options.modulesFile), 'content')
|
|
162
|
+
records.push(modulesRecord ?? `missing:${options.modulesFile}`)
|
|
163
|
+
|
|
164
|
+
for (const roots of options.moduleRoots) {
|
|
165
|
+
addConventionRecords(records, roots)
|
|
166
|
+
addScannedRecords(records, roots)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return checksum(records.sort((a, b) => a.localeCompare(b)).join('\n'))
|
|
170
|
+
}
|
|
@@ -2942,6 +2942,20 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
|
|
|
2942
2942
|
TENANT_DATA_ENCRYPTION_FALLBACK_KEY: process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY ?? 'om-ephemeral-integration-fallback-key',
|
|
2943
2943
|
AUTO_SPAWN_WORKERS: process.env.AUTO_SPAWN_WORKERS ?? 'true',
|
|
2944
2944
|
AUTO_SPAWN_SCHEDULER: 'false',
|
|
2945
|
+
// Hide the demo feedback floating action button — it lives at
|
|
2946
|
+
// `fixed bottom-6 right-6 z-banner` and consistently intercepts pointer
|
|
2947
|
+
// events on row-action menus and other bottom-of-viewport UI elements
|
|
2948
|
+
// (e.g. TC-WF-006 Delete menuitem click). The widget is already gated
|
|
2949
|
+
// on `DEMO_MODE !== 'false'` in the backend layout, so opting out here
|
|
2950
|
+
// only affects the integration test runtime — dev + prod stay unchanged.
|
|
2951
|
+
DEMO_MODE: 'false',
|
|
2952
|
+
// Disable the feature-toggle resolution cache. Tests flip overrides
|
|
2953
|
+
// (e.g. example_customers_sync enabled/bidirectional) between cases; the
|
|
2954
|
+
// 1-minute resolution cache can serve a stale value across set/clear
|
|
2955
|
+
// under fast churn, which made the example.todo.* persistent subscribers
|
|
2956
|
+
// skip enqueueing inbound sync jobs (TC-CRM-028 inbound trio flake).
|
|
2957
|
+
// Fresh DB reads make flag-gated sync deterministic in tests only.
|
|
2958
|
+
OM_FEATURE_TOGGLES_CACHE_DISABLED: '1',
|
|
2945
2959
|
OM_CLI_QUIET: '1',
|
|
2946
2960
|
MERCATO_QUIET: '1',
|
|
2947
2961
|
QUEUE_BASE_DIR: EPHEMERAL_QUEUE_BASE_DIR,
|