@open-mercato/cli 0.6.7-develop.6553.1.2ec7246e04 → 0.6.7-develop.6566.1.15385fa179
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 +3 -3
- package/AGENTS.md +1 -0
- package/dist/agentic/guides/module-facts.json +53 -53
- package/dist/agentic/guides/modules/ai_assistant.md +1 -1
- package/dist/agentic/guides/modules/api_docs.md +1 -1
- package/dist/agentic/guides/modules/api_keys.md +1 -1
- package/dist/agentic/guides/modules/attachments.md +1 -1
- package/dist/agentic/guides/modules/audit_logs.md +1 -1
- package/dist/agentic/guides/modules/auth.md +1 -1
- package/dist/agentic/guides/modules/business_rules.md +1 -1
- package/dist/agentic/guides/modules/catalog.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail.md +1 -1
- package/dist/agentic/guides/modules/channel_imap.md +1 -1
- package/dist/agentic/guides/modules/checkout.md +1 -1
- package/dist/agentic/guides/modules/communication_channels.md +1 -1
- package/dist/agentic/guides/modules/configs.md +1 -1
- package/dist/agentic/guides/modules/content.md +1 -1
- package/dist/agentic/guides/modules/currencies.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts.md +1 -1
- package/dist/agentic/guides/modules/customers.md +1 -1
- package/dist/agentic/guides/modules/dashboards.md +1 -1
- package/dist/agentic/guides/modules/data_sync.md +1 -1
- package/dist/agentic/guides/modules/dictionaries.md +1 -1
- package/dist/agentic/guides/modules/directory.md +1 -1
- package/dist/agentic/guides/modules/entities.md +1 -1
- package/dist/agentic/guides/modules/events.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
- package/dist/agentic/guides/modules/generators.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops.md +1 -1
- package/dist/agentic/guides/modules/integrations.md +1 -1
- package/dist/agentic/guides/modules/messages.md +1 -1
- package/dist/agentic/guides/modules/notifications.md +1 -1
- package/dist/agentic/guides/modules/onboarding.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways.md +1 -1
- package/dist/agentic/guides/modules/perspectives.md +1 -1
- package/dist/agentic/guides/modules/planner.md +1 -1
- package/dist/agentic/guides/modules/portal.md +1 -1
- package/dist/agentic/guides/modules/progress.md +1 -1
- package/dist/agentic/guides/modules/query_index.md +1 -1
- package/dist/agentic/guides/modules/record_locks.md +1 -1
- package/dist/agentic/guides/modules/resources.md +1 -1
- package/dist/agentic/guides/modules/sales.md +1 -1
- package/dist/agentic/guides/modules/scheduler.md +1 -1
- package/dist/agentic/guides/modules/search.md +1 -1
- package/dist/agentic/guides/modules/security.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
- package/dist/agentic/guides/modules/sso.md +1 -1
- package/dist/agentic/guides/modules/staff.md +1 -1
- package/dist/agentic/guides/modules/storage_s3.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
- package/dist/agentic/guides/modules/sync_excel.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
- package/dist/agentic/guides/modules/translations.md +1 -1
- package/dist/agentic/guides/modules/webhooks.md +1 -1
- package/dist/agentic/guides/modules/workflows.md +1 -1
- package/dist/agentic/shared/AGENTS.md.template +1 -1
- package/dist/agentic/shared/ai/skills/tiers.schema.json +15 -0
- package/dist/agentic/shared/scripts/install-skills.sh +244 -37
- package/dist/lib/__integration__/TC-INT-008.spec.js +9 -11
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-setup.js +14 -2
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/db/commands.js +1 -1
- package/dist/lib/db/commands.js.map +1 -1
- package/dist/lib/generate-watch-events.js +106 -0
- package/dist/lib/generate-watch-events.js.map +7 -0
- package/dist/lib/generators/ast/imports.js +1 -1
- package/dist/lib/generators/ast/imports.js.map +1 -1
- package/dist/lib/generators/entity-ids.js +21 -14
- package/dist/lib/generators/entity-ids.js.map +2 -2
- package/dist/lib/generators/extensions/injection-widgets.js +1 -1
- package/dist/lib/generators/extensions/injection-widgets.js.map +1 -1
- package/dist/lib/generators/module-facts.js +1 -1
- package/dist/lib/generators/module-facts.js.map +1 -1
- package/dist/lib/generators/module-registry.js +1 -1
- package/dist/lib/generators/module-registry.js.map +1 -1
- package/dist/lib/generators/openapi.js +114 -12
- package/dist/lib/generators/openapi.js.map +2 -2
- package/dist/lib/in-process-generate-watcher.js +13 -2
- package/dist/lib/in-process-generate-watcher.js.map +2 -2
- package/dist/lib/modules-config.js +1 -1
- package/dist/lib/modules-config.js.map +1 -1
- package/dist/lib/resolver.js +1 -1
- package/dist/lib/resolver.js.map +1 -1
- package/dist/mercato.js +68 -23
- package/dist/mercato.js.map +2 -2
- package/package.json +7 -6
- package/src/__tests__/mercato.test.ts +53 -2
- package/src/lib/__integration__/TC-INT-008.spec.ts +15 -15
- package/src/lib/__tests__/generate-watch-events.test.ts +112 -0
- package/src/lib/__tests__/generate-watch-structure.test.ts +20 -0
- package/src/lib/__tests__/in-process-generate-watcher.test.ts +193 -2
- package/src/lib/agentic-setup.ts +23 -4
- package/src/lib/db/commands.ts +1 -1
- package/src/lib/generate-watch-events.ts +125 -0
- package/src/lib/generators/__tests__/entity-ids.test.ts +98 -1
- package/src/lib/generators/__tests__/openapi.test.ts +174 -0
- package/src/lib/generators/__tests__/structural-contracts.test.ts +1 -1
- package/src/lib/generators/ast/imports.ts +1 -1
- package/src/lib/generators/entity-ids.ts +34 -14
- package/src/lib/generators/extensions/injection-widgets.ts +1 -1
- package/src/lib/generators/module-facts.ts +1 -1
- package/src/lib/generators/module-registry.ts +1 -1
- package/src/lib/generators/openapi.ts +167 -14
- package/src/lib/in-process-generate-watcher.ts +35 -6
- package/src/lib/modules-config.ts +1 -1
- package/src/lib/resolver.ts +1 -1
- package/src/mercato.ts +75 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6566.1.15385fa179",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,21 +60,22 @@
|
|
|
60
60
|
"@mikro-orm/decorators": "^7.1.5",
|
|
61
61
|
"@mikro-orm/migrations": "^7.1.5",
|
|
62
62
|
"@mikro-orm/postgresql": "^7.1.5",
|
|
63
|
-
"@open-mercato/queue": "0.6.7-develop.
|
|
64
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
63
|
+
"@open-mercato/queue": "0.6.7-develop.6566.1.15385fa179",
|
|
64
|
+
"@open-mercato/shared": "0.6.7-develop.6566.1.15385fa179",
|
|
65
65
|
"cross-spawn": "^7.0.6",
|
|
66
66
|
"pg": "8.22.0",
|
|
67
67
|
"semver": "^7.8.5",
|
|
68
68
|
"testcontainers": "^12.0.3",
|
|
69
69
|
"ts-morph": "^28.0.0",
|
|
70
70
|
"tsx": "^4.22.4",
|
|
71
|
-
"typescript": "
|
|
71
|
+
"typescript": "7.0.2",
|
|
72
|
+
"typescript-js": "npm:typescript@6.0.3"
|
|
72
73
|
},
|
|
73
74
|
"peerDependencies": {
|
|
74
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
75
|
+
"@open-mercato/shared": "0.6.7-develop.6566.1.15385fa179"
|
|
75
76
|
},
|
|
76
77
|
"devDependencies": {
|
|
77
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
78
|
+
"@open-mercato/shared": "0.6.7-develop.6566.1.15385fa179",
|
|
78
79
|
"@types/jest": "^30.0.0",
|
|
79
80
|
"jest": "^30.4.2",
|
|
80
81
|
"ts-jest": "^29.4.11"
|
|
@@ -547,7 +547,11 @@ describe('generate post-step structural cache purge', () => {
|
|
|
547
547
|
it('runs structural cache purge after successful generation when configs cache CLI is available', async () => {
|
|
548
548
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
549
549
|
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
550
|
-
const generateEntityIds = jest.fn().mockResolvedValue(
|
|
550
|
+
const generateEntityIds = jest.fn().mockResolvedValue({
|
|
551
|
+
filesWritten: ['/tmp/test-app/.mercato/generated/entities.ids.generated.ts'],
|
|
552
|
+
filesUnchanged: [],
|
|
553
|
+
errors: [],
|
|
554
|
+
})
|
|
551
555
|
const generateModuleRegistry = jest.fn().mockResolvedValue(undefined)
|
|
552
556
|
const generateModuleRegistryApp = jest.fn().mockResolvedValue(undefined)
|
|
553
557
|
const generateModuleRegistryCli = jest.fn().mockResolvedValue(undefined)
|
|
@@ -594,10 +598,57 @@ describe('generate post-step structural cache purge', () => {
|
|
|
594
598
|
consoleLogSpy.mockRestore()
|
|
595
599
|
})
|
|
596
600
|
|
|
601
|
+
it('skips structural invalidation when every generated output is unchanged', async () => {
|
|
602
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
603
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
604
|
+
const unchangedResult = {
|
|
605
|
+
filesWritten: [],
|
|
606
|
+
filesUnchanged: ['/tmp/test-app/.mercato/generated/unchanged.generated.ts'],
|
|
607
|
+
errors: [],
|
|
608
|
+
}
|
|
609
|
+
const generators = {
|
|
610
|
+
generateEntityIds: jest.fn().mockResolvedValue(unchangedResult),
|
|
611
|
+
generateModuleRegistry: jest.fn().mockResolvedValue(unchangedResult),
|
|
612
|
+
generateModuleRegistryApp: jest.fn().mockResolvedValue(unchangedResult),
|
|
613
|
+
generateModuleRegistryCli: jest.fn().mockResolvedValue(unchangedResult),
|
|
614
|
+
generateModuleEntities: jest.fn().mockResolvedValue(unchangedResult),
|
|
615
|
+
generateModuleDi: jest.fn().mockResolvedValue(unchangedResult),
|
|
616
|
+
generateModulePackageSources: jest.fn().mockResolvedValue(unchangedResult),
|
|
617
|
+
generateOpenApi: jest.fn().mockResolvedValue(unchangedResult),
|
|
618
|
+
}
|
|
619
|
+
const bootstrapFromAppRoot = jest.fn()
|
|
620
|
+
|
|
621
|
+
jest.doMock('../lib/generators', () => generators)
|
|
622
|
+
jest.doMock('../lib/resolver', () => ({
|
|
623
|
+
createResolver: () => ({
|
|
624
|
+
getAppDir: () => '/tmp/test-app',
|
|
625
|
+
}),
|
|
626
|
+
}))
|
|
627
|
+
jest.doMock('@open-mercato/shared/lib/bootstrap/dynamicLoader', () => ({
|
|
628
|
+
bootstrapFromAppRoot,
|
|
629
|
+
}))
|
|
630
|
+
|
|
631
|
+
const mercato = await import('../mercato')
|
|
632
|
+
const exitCode = await mercato.run(['node', 'mercato', 'generate'])
|
|
633
|
+
|
|
634
|
+
expect(exitCode).toBe(0)
|
|
635
|
+
expect(bootstrapFromAppRoot).not.toHaveBeenCalled()
|
|
636
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
637
|
+
'[generate] Generated outputs unchanged; skipping structural cache purge.',
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
consoleErrorSpy.mockRestore()
|
|
641
|
+
consoleLogSpy.mockRestore()
|
|
642
|
+
})
|
|
643
|
+
|
|
597
644
|
it('keeps generation successful when the post-generate cache purge bootstrap fails', async () => {
|
|
598
645
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
599
646
|
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
600
|
-
const generateEntityIds = jest.fn().mockResolvedValue(
|
|
647
|
+
const generateEntityIds = jest.fn().mockResolvedValue({
|
|
648
|
+
filesWritten: ['/tmp/test-app/.mercato/generated/entities.ids.generated.ts'],
|
|
649
|
+
filesUnchanged: [],
|
|
650
|
+
errors: [],
|
|
651
|
+
})
|
|
601
652
|
const generateModuleRegistry = jest.fn().mockResolvedValue(undefined)
|
|
602
653
|
const generateModuleRegistryApp = jest.fn().mockResolvedValue(undefined)
|
|
603
654
|
const generateModuleRegistryCli = jest.fn().mockResolvedValue(undefined)
|
|
@@ -282,28 +282,28 @@ test.describe('TC-INT-008: CLI agentic init mirrors standalone scaffolding asset
|
|
|
282
282
|
expect(fs.existsSync(path.join(appDir, '.ai', 'skills', 'om-spec-writing'))).toBe(false)
|
|
283
283
|
expect(fs.existsSync(path.join(appDir, 'scripts', 'install-skills.sh'))).toBe(true)
|
|
284
284
|
|
|
285
|
-
// install-skills.sh (run by agentic:init)
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
expect(fs.lstatSync(cursorSkillsLinkPath).isSymbolicLink()).toBe(true)
|
|
290
|
-
expect(normalizePath(fs.readlinkSync(cursorSkillsLinkPath))).toBe('../.ai/skills')
|
|
291
|
-
|
|
285
|
+
// install-skills.sh (run by agentic:init) installs every local tier skill once,
|
|
286
|
+
// into the canonical .agents/skills/. Codex and Cursor read that directory
|
|
287
|
+
// natively, so they get no skills directory of their own; Claude Code cannot,
|
|
288
|
+
// so it keeps a link layer pointing back at the canonical copy.
|
|
292
289
|
const tiersManifest = JSON.parse(
|
|
293
290
|
fs.readFileSync(path.join(agenticRoot, 'shared', 'ai', 'skills', 'tiers.json'), 'utf8'),
|
|
294
291
|
) as { default: string[]; tiers: Record<string, { skills: string[] }> }
|
|
295
292
|
const defaultTierSkills = tiersManifest.default.flatMap((tierName) => tiersManifest.tiers[tierName].skills)
|
|
296
293
|
expect(defaultTierSkills.length).toBeGreaterThan(0)
|
|
297
294
|
|
|
298
|
-
for (const
|
|
299
|
-
const
|
|
300
|
-
expect(fs.lstatSync(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
295
|
+
for (const skillName of defaultTierSkills) {
|
|
296
|
+
const canonicalLinkPath = path.join(appDir, '.agents', 'skills', skillName)
|
|
297
|
+
expect(fs.lstatSync(canonicalLinkPath).isSymbolicLink()).toBe(true)
|
|
298
|
+
expect(normalizePath(fs.readlinkSync(canonicalLinkPath))).toBe(`../../.ai/skills/${skillName}`)
|
|
299
|
+
|
|
300
|
+
const claudeLinkPath = path.join(appDir, '.claude', 'skills', skillName)
|
|
301
|
+
expect(fs.lstatSync(claudeLinkPath).isSymbolicLink()).toBe(true)
|
|
302
|
+
expect(normalizePath(fs.readlinkSync(claudeLinkPath))).toBe(`../../.agents/skills/${skillName}`)
|
|
306
303
|
}
|
|
304
|
+
|
|
305
|
+
expect(fs.existsSync(path.join(appDir, '.codex', 'skills'))).toBe(false)
|
|
306
|
+
expect(fs.existsSync(path.join(appDir, '.cursor', 'skills'))).toBe(false)
|
|
307
307
|
} finally {
|
|
308
308
|
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
309
309
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import {
|
|
3
|
+
createGenerateWatchChangeSignal,
|
|
4
|
+
type GenerateWatchTarget,
|
|
5
|
+
} from '../generate-watch-events'
|
|
6
|
+
|
|
7
|
+
type RegisteredWatcher = {
|
|
8
|
+
target: GenerateWatchTarget
|
|
9
|
+
onChange: () => void
|
|
10
|
+
onError: () => void
|
|
11
|
+
close: jest.Mock
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createWatchHarness(initialTargets: GenerateWatchTarget[]) {
|
|
15
|
+
let targets = initialTargets
|
|
16
|
+
const registered: RegisteredWatcher[] = []
|
|
17
|
+
const watchDirectory = jest.fn((
|
|
18
|
+
target: GenerateWatchTarget,
|
|
19
|
+
onChange: () => void,
|
|
20
|
+
onError: () => void,
|
|
21
|
+
) => {
|
|
22
|
+
const watcher = { target, onChange, onError, close: jest.fn() }
|
|
23
|
+
registered.push(watcher)
|
|
24
|
+
return watcher
|
|
25
|
+
})
|
|
26
|
+
const signal = createGenerateWatchChangeSignal({
|
|
27
|
+
getWatchTargets: () => targets,
|
|
28
|
+
watchDirectory,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
signal,
|
|
33
|
+
registered,
|
|
34
|
+
watchDirectory,
|
|
35
|
+
setTargets: (nextTargets: GenerateWatchTarget[]) => { targets = nextTargets },
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('createGenerateWatchChangeSignal', () => {
|
|
40
|
+
it('deduplicates targets and increments its version on filesystem events', async () => {
|
|
41
|
+
const target = { directory: './modules', recursive: true }
|
|
42
|
+
const { signal, registered, watchDirectory } = createWatchHarness([target, target])
|
|
43
|
+
|
|
44
|
+
await signal.refresh()
|
|
45
|
+
expect(watchDirectory).toHaveBeenCalledTimes(1)
|
|
46
|
+
expect(registered[0].target.directory).toBe(path.resolve('./modules'))
|
|
47
|
+
expect(signal.currentVersion()).toBe(0)
|
|
48
|
+
|
|
49
|
+
registered[0].onChange()
|
|
50
|
+
registered[0].onChange()
|
|
51
|
+
expect(signal.currentVersion()).toBe(2)
|
|
52
|
+
expect(signal.usesPollingFallback()).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('refreshes changed watch roots without recreating stable watchers', async () => {
|
|
56
|
+
const first = { directory: './modules-a', recursive: true }
|
|
57
|
+
const second = { directory: './modules-b', recursive: true }
|
|
58
|
+
const harness = createWatchHarness([first])
|
|
59
|
+
|
|
60
|
+
await harness.signal.refresh()
|
|
61
|
+
harness.setTargets([first, second])
|
|
62
|
+
await harness.signal.refresh()
|
|
63
|
+
expect(harness.watchDirectory).toHaveBeenCalledTimes(2)
|
|
64
|
+
expect(harness.registered[0].close).not.toHaveBeenCalled()
|
|
65
|
+
|
|
66
|
+
harness.setTargets([second])
|
|
67
|
+
await harness.signal.refresh()
|
|
68
|
+
expect(harness.registered[0].close).toHaveBeenCalledTimes(1)
|
|
69
|
+
expect(harness.registered[1].close).not.toHaveBeenCalled()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('falls back to checksum polling when watcher setup fails', async () => {
|
|
73
|
+
const firstClose = jest.fn()
|
|
74
|
+
const watchDirectory = jest.fn()
|
|
75
|
+
.mockReturnValueOnce({ close: firstClose })
|
|
76
|
+
.mockImplementationOnce(() => { throw new Error('watch unavailable') })
|
|
77
|
+
const signal = createGenerateWatchChangeSignal({
|
|
78
|
+
getWatchTargets: () => [
|
|
79
|
+
{ directory: './modules-a', recursive: true },
|
|
80
|
+
{ directory: './modules-b', recursive: true },
|
|
81
|
+
],
|
|
82
|
+
watchDirectory,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await signal.refresh()
|
|
86
|
+
expect(signal.usesPollingFallback()).toBe(true)
|
|
87
|
+
expect(signal.currentVersion()).toBe(1)
|
|
88
|
+
expect(firstClose).toHaveBeenCalledTimes(1)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('falls back to checksum polling when an active watcher errors', async () => {
|
|
92
|
+
const { signal, registered } = createWatchHarness([
|
|
93
|
+
{ directory: './modules', recursive: true },
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
await signal.refresh()
|
|
97
|
+
registered[0].onError()
|
|
98
|
+
expect(signal.usesPollingFallback()).toBe(true)
|
|
99
|
+
expect(registered[0].close).toHaveBeenCalledTimes(1)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('closes filesystem watchers exactly once', async () => {
|
|
103
|
+
const { signal, registered } = createWatchHarness([
|
|
104
|
+
{ directory: './modules', recursive: true },
|
|
105
|
+
])
|
|
106
|
+
|
|
107
|
+
await signal.refresh()
|
|
108
|
+
await signal.close()
|
|
109
|
+
await signal.close()
|
|
110
|
+
expect(registered[0].close).toHaveBeenCalledTimes(1)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -77,4 +77,24 @@ describe('calculateGenerateWatchStructureChecksum', () => {
|
|
|
77
77
|
|
|
78
78
|
expect(currentChecksum()).not.toBe(before)
|
|
79
79
|
})
|
|
80
|
+
|
|
81
|
+
it('changes when a discovered worker is added and removed', () => {
|
|
82
|
+
const before = currentChecksum()
|
|
83
|
+
const workerPath = path.join(pkgModule, 'workers', 'sync-customers.ts')
|
|
84
|
+
|
|
85
|
+
write(workerPath, 'export default async function syncCustomers() {}\n')
|
|
86
|
+
const afterAdd = currentChecksum()
|
|
87
|
+
expect(afterAdd).not.toBe(before)
|
|
88
|
+
|
|
89
|
+
fs.rmSync(workerPath)
|
|
90
|
+
expect(currentChecksum()).toBe(before)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('changes when the module registry configuration changes', () => {
|
|
94
|
+
const before = currentChecksum()
|
|
95
|
+
|
|
96
|
+
write(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = ["customers"]\n')
|
|
97
|
+
|
|
98
|
+
expect(currentChecksum()).not.toBe(before)
|
|
99
|
+
})
|
|
80
100
|
})
|
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
startInProcessGenerateWatcher,
|
|
3
|
+
type GenerateWatcherChangeSignal,
|
|
4
|
+
} from '../in-process-generate-watcher'
|
|
2
5
|
|
|
3
6
|
const silentLogger = { log: jest.fn(), error: jest.fn() }
|
|
4
7
|
|
|
5
8
|
async function flushAsync(times = 5): Promise<void> {
|
|
6
9
|
for (let i = 0; i < times; i++) {
|
|
7
|
-
// eslint-disable-next-line no-await-in-loop
|
|
8
10
|
await Promise.resolve()
|
|
9
11
|
}
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
function createFakeChangeSignal() {
|
|
15
|
+
let version = 0
|
|
16
|
+
let fallback = false
|
|
17
|
+
const signal: GenerateWatcherChangeSignal = {
|
|
18
|
+
currentVersion: () => version,
|
|
19
|
+
refresh: jest.fn(async () => undefined),
|
|
20
|
+
usesPollingFallback: () => fallback,
|
|
21
|
+
close: jest.fn(async () => undefined),
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
signal,
|
|
25
|
+
markChanged: () => { version += 1 },
|
|
26
|
+
usePollingFallback: () => { fallback = true },
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
describe('startInProcessGenerateWatcher', () => {
|
|
13
31
|
beforeEach(() => {
|
|
14
32
|
jest.useFakeTimers()
|
|
@@ -97,6 +115,179 @@ describe('startInProcessGenerateWatcher', () => {
|
|
|
97
115
|
await handle.done
|
|
98
116
|
})
|
|
99
117
|
|
|
118
|
+
it('does not recompute the full checksum during event-gated idle polls', async () => {
|
|
119
|
+
const runGenerators = jest.fn(async () => undefined)
|
|
120
|
+
const computeStructureChecksum = jest.fn(async () => 'stable')
|
|
121
|
+
const { signal } = createFakeChangeSignal()
|
|
122
|
+
const handle = startInProcessGenerateWatcher({
|
|
123
|
+
pollMs: 1000,
|
|
124
|
+
skipInitial: true,
|
|
125
|
+
quiet: true,
|
|
126
|
+
logger: silentLogger,
|
|
127
|
+
changeSignal: signal,
|
|
128
|
+
computeStructureChecksum,
|
|
129
|
+
runGenerators,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
await flushAsync(8)
|
|
133
|
+
expect(computeStructureChecksum).toHaveBeenCalledTimes(1)
|
|
134
|
+
|
|
135
|
+
for (let poll = 0; poll < 20; poll += 1) {
|
|
136
|
+
jest.advanceTimersByTime(1000)
|
|
137
|
+
await flushAsync(8)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
expect(computeStructureChecksum).toHaveBeenCalledTimes(1)
|
|
141
|
+
expect(runGenerators).not.toHaveBeenCalled()
|
|
142
|
+
await handle.close()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('coalesces filesystem event bursts into one checksum and regeneration', async () => {
|
|
146
|
+
let currentChecksum = 'before'
|
|
147
|
+
const runGenerators = jest.fn(async () => undefined)
|
|
148
|
+
const computeStructureChecksum = jest.fn(async () => currentChecksum)
|
|
149
|
+
const { signal, markChanged } = createFakeChangeSignal()
|
|
150
|
+
const handle = startInProcessGenerateWatcher({
|
|
151
|
+
pollMs: 1000,
|
|
152
|
+
skipInitial: true,
|
|
153
|
+
quiet: true,
|
|
154
|
+
logger: silentLogger,
|
|
155
|
+
changeSignal: signal,
|
|
156
|
+
computeStructureChecksum,
|
|
157
|
+
runGenerators,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
await flushAsync(8)
|
|
161
|
+
currentChecksum = 'after'
|
|
162
|
+
markChanged()
|
|
163
|
+
markChanged()
|
|
164
|
+
markChanged()
|
|
165
|
+
jest.advanceTimersByTime(1000)
|
|
166
|
+
await flushAsync(12)
|
|
167
|
+
|
|
168
|
+
expect(computeStructureChecksum).toHaveBeenCalledTimes(2)
|
|
169
|
+
expect(runGenerators).toHaveBeenCalledTimes(1)
|
|
170
|
+
expect(runGenerators).toHaveBeenCalledWith('structure change')
|
|
171
|
+
await handle.close()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('validates an unrelated filesystem event without regenerating', async () => {
|
|
175
|
+
const runGenerators = jest.fn(async () => undefined)
|
|
176
|
+
const computeStructureChecksum = jest.fn(async () => 'stable')
|
|
177
|
+
const { signal, markChanged } = createFakeChangeSignal()
|
|
178
|
+
const handle = startInProcessGenerateWatcher({
|
|
179
|
+
pollMs: 1000,
|
|
180
|
+
skipInitial: true,
|
|
181
|
+
quiet: true,
|
|
182
|
+
logger: silentLogger,
|
|
183
|
+
changeSignal: signal,
|
|
184
|
+
computeStructureChecksum,
|
|
185
|
+
runGenerators,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
await flushAsync(8)
|
|
189
|
+
markChanged()
|
|
190
|
+
jest.advanceTimersByTime(1000)
|
|
191
|
+
await flushAsync(12)
|
|
192
|
+
|
|
193
|
+
expect(computeStructureChecksum).toHaveBeenCalledTimes(2)
|
|
194
|
+
expect(runGenerators).not.toHaveBeenCalled()
|
|
195
|
+
await handle.close()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('retries a dirty checksum after a transient checksum failure', async () => {
|
|
199
|
+
const runGenerators = jest.fn(async () => undefined)
|
|
200
|
+
const computeStructureChecksum = jest.fn()
|
|
201
|
+
.mockResolvedValueOnce('before')
|
|
202
|
+
.mockRejectedValueOnce(new Error('temporary read failure'))
|
|
203
|
+
.mockResolvedValue('after')
|
|
204
|
+
const { signal, markChanged } = createFakeChangeSignal()
|
|
205
|
+
const handle = startInProcessGenerateWatcher({
|
|
206
|
+
pollMs: 1000,
|
|
207
|
+
skipInitial: true,
|
|
208
|
+
quiet: true,
|
|
209
|
+
logger: silentLogger,
|
|
210
|
+
changeSignal: signal,
|
|
211
|
+
computeStructureChecksum,
|
|
212
|
+
runGenerators,
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
await flushAsync(8)
|
|
216
|
+
markChanged()
|
|
217
|
+
jest.advanceTimersByTime(1000)
|
|
218
|
+
await flushAsync(8)
|
|
219
|
+
expect(runGenerators).not.toHaveBeenCalled()
|
|
220
|
+
|
|
221
|
+
jest.advanceTimersByTime(1000)
|
|
222
|
+
await flushAsync(12)
|
|
223
|
+
expect(computeStructureChecksum).toHaveBeenCalledTimes(3)
|
|
224
|
+
expect(runGenerators).toHaveBeenCalledTimes(1)
|
|
225
|
+
await handle.close()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('keeps an event that arrives while a checksum is in flight dirty for the next poll', async () => {
|
|
229
|
+
let releaseChecksum: ((value: string) => void) | null = null
|
|
230
|
+
const runGenerators = jest.fn(async () => undefined)
|
|
231
|
+
const computeStructureChecksum = jest.fn()
|
|
232
|
+
.mockResolvedValueOnce('before')
|
|
233
|
+
.mockImplementationOnce(() => new Promise<string>((resolve) => {
|
|
234
|
+
releaseChecksum = resolve
|
|
235
|
+
}))
|
|
236
|
+
.mockResolvedValue('after')
|
|
237
|
+
const { signal, markChanged } = createFakeChangeSignal()
|
|
238
|
+
const handle = startInProcessGenerateWatcher({
|
|
239
|
+
pollMs: 1000,
|
|
240
|
+
skipInitial: true,
|
|
241
|
+
quiet: true,
|
|
242
|
+
logger: silentLogger,
|
|
243
|
+
changeSignal: signal,
|
|
244
|
+
computeStructureChecksum,
|
|
245
|
+
runGenerators,
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
await flushAsync(8)
|
|
249
|
+
markChanged()
|
|
250
|
+
jest.advanceTimersByTime(1000)
|
|
251
|
+
await flushAsync(8)
|
|
252
|
+
expect(computeStructureChecksum).toHaveBeenCalledTimes(2)
|
|
253
|
+
|
|
254
|
+
markChanged()
|
|
255
|
+
releaseChecksum?.('after')
|
|
256
|
+
await flushAsync(12)
|
|
257
|
+
expect(runGenerators).toHaveBeenCalledTimes(1)
|
|
258
|
+
|
|
259
|
+
jest.advanceTimersByTime(1000)
|
|
260
|
+
await flushAsync(12)
|
|
261
|
+
expect(computeStructureChecksum).toHaveBeenCalledTimes(3)
|
|
262
|
+
await handle.close()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('falls back to full checksum polling when filesystem watching is unavailable', async () => {
|
|
266
|
+
const runGenerators = jest.fn(async () => undefined)
|
|
267
|
+
const computeStructureChecksum = jest.fn(async () => 'stable')
|
|
268
|
+
const { signal, usePollingFallback } = createFakeChangeSignal()
|
|
269
|
+
usePollingFallback()
|
|
270
|
+
const handle = startInProcessGenerateWatcher({
|
|
271
|
+
pollMs: 1000,
|
|
272
|
+
skipInitial: true,
|
|
273
|
+
quiet: true,
|
|
274
|
+
logger: silentLogger,
|
|
275
|
+
changeSignal: signal,
|
|
276
|
+
computeStructureChecksum,
|
|
277
|
+
runGenerators,
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
await flushAsync(8)
|
|
281
|
+
jest.advanceTimersByTime(1000)
|
|
282
|
+
await flushAsync(8)
|
|
283
|
+
jest.advanceTimersByTime(1000)
|
|
284
|
+
await flushAsync(8)
|
|
285
|
+
|
|
286
|
+
expect(computeStructureChecksum).toHaveBeenCalledTimes(3)
|
|
287
|
+
await handle.close()
|
|
288
|
+
expect(signal.close).toHaveBeenCalledTimes(1)
|
|
289
|
+
})
|
|
290
|
+
|
|
100
291
|
it('does not start a new poll while a regeneration is in flight', async () => {
|
|
101
292
|
let release: (() => void) | null = null
|
|
102
293
|
const runGenerators = jest.fn(async (_reason: string) => {
|
package/src/lib/agentic-setup.ts
CHANGED
|
@@ -409,8 +409,8 @@ function generateCodex(config: AgenticConfig): void {
|
|
|
409
409
|
|
|
410
410
|
copyFile(srcDir, 'mcp.json.example', join(targetDir, '.codex', 'mcp.json.example'))
|
|
411
411
|
|
|
412
|
-
//
|
|
413
|
-
|
|
412
|
+
// No .codex/skills directory: Codex reads the canonical .agents/skills/,
|
|
413
|
+
// which scripts/install-skills.sh populates.
|
|
414
414
|
}
|
|
415
415
|
|
|
416
416
|
function generateCursor(config: AgenticConfig): void {
|
|
@@ -424,8 +424,8 @@ function generateCursor(config: AgenticConfig): void {
|
|
|
424
424
|
copyFile(srcDir, 'hooks/entity-migration-check.mjs', join(targetDir, '.cursor', 'hooks', 'entity-migration-check.mjs'))
|
|
425
425
|
copyFile(srcDir, 'mcp.json.example', join(targetDir, '.cursor', 'mcp.json.example'))
|
|
426
426
|
|
|
427
|
-
//
|
|
428
|
-
|
|
427
|
+
// No .cursor/skills directory: Cursor reads the canonical .agents/skills/,
|
|
428
|
+
// which scripts/install-skills.sh populates.
|
|
429
429
|
}
|
|
430
430
|
|
|
431
431
|
function ensureSkillsLink(linkPath: string, target: string): void {
|
|
@@ -518,6 +518,7 @@ export async function runAgenticSetup(
|
|
|
518
518
|
if (selectedIds.includes('codex')) generateCodex(config)
|
|
519
519
|
if (selectedIds.includes('cursor')) generateCursor(config)
|
|
520
520
|
|
|
521
|
+
persistAgentSelection(targetDir, selectedIds)
|
|
521
522
|
installSkills(targetDir)
|
|
522
523
|
|
|
523
524
|
console.log('')
|
|
@@ -539,6 +540,24 @@ export async function runAgenticSetup(
|
|
|
539
540
|
console.log('')
|
|
540
541
|
}
|
|
541
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Persist the agent selection so later `yarn install-skills` runs keep honoring
|
|
545
|
+
* it: agents the user did not pick go into `agents.ignore` in tiers.json and
|
|
546
|
+
* never get a skills directory of their own.
|
|
547
|
+
*/
|
|
548
|
+
function persistAgentSelection(targetDir: string, selectedIds: string[]): void {
|
|
549
|
+
const manifestPath = join(targetDir, '.ai', 'skills', 'tiers.json')
|
|
550
|
+
if (!existsSync(manifestPath)) return
|
|
551
|
+
const ignore = SELECTABLE.map((tool) => tool.id).filter((id) => !selectedIds.includes(id))
|
|
552
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as Record<string, unknown>
|
|
553
|
+
if (ignore.length > 0) {
|
|
554
|
+
manifest.agents = { ignore }
|
|
555
|
+
} else {
|
|
556
|
+
delete manifest.agents
|
|
557
|
+
}
|
|
558
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
559
|
+
}
|
|
560
|
+
|
|
542
561
|
function installSkills(targetDir: string): void {
|
|
543
562
|
const installScript = join(targetDir, 'scripts', 'install-skills.sh')
|
|
544
563
|
if (!existsSync(installScript)) return
|
package/src/lib/db/commands.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
|
-
import ts from 'typescript'
|
|
4
|
+
import ts from 'typescript-js'
|
|
5
5
|
import { MikroORM, MetadataStorage, type Logger } from '@mikro-orm/core'
|
|
6
6
|
import { ReflectMetadataProvider } from '@mikro-orm/decorators/legacy'
|
|
7
7
|
import { Migrator } from '@mikro-orm/migrations'
|