@auto-engineer/pipeline 0.0.1
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 +5 -0
- package/LICENSE +10 -0
- package/claude.md +160 -0
- package/dist/src/builder/define.d.ts +90 -0
- package/dist/src/builder/define.d.ts.map +1 -0
- package/dist/src/builder/define.js +425 -0
- package/dist/src/builder/define.js.map +1 -0
- package/dist/src/builder/define.specs.d.ts +2 -0
- package/dist/src/builder/define.specs.d.ts.map +1 -0
- package/dist/src/builder/define.specs.js +435 -0
- package/dist/src/builder/define.specs.js.map +1 -0
- package/dist/src/config/pipeline-config.d.ts +13 -0
- package/dist/src/config/pipeline-config.d.ts.map +1 -0
- package/dist/src/config/pipeline-config.js +15 -0
- package/dist/src/config/pipeline-config.js.map +1 -0
- package/dist/src/core/descriptors.d.ts +84 -0
- package/dist/src/core/descriptors.d.ts.map +1 -0
- package/dist/src/core/descriptors.js +2 -0
- package/dist/src/core/descriptors.js.map +1 -0
- package/dist/src/core/descriptors.specs.d.ts +2 -0
- package/dist/src/core/descriptors.specs.d.ts.map +1 -0
- package/dist/src/core/descriptors.specs.js +24 -0
- package/dist/src/core/descriptors.specs.js.map +1 -0
- package/dist/src/core/types.d.ts +10 -0
- package/dist/src/core/types.d.ts.map +1 -0
- package/dist/src/core/types.js +4 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/core/types.specs.d.ts +2 -0
- package/dist/src/core/types.specs.d.ts.map +1 -0
- package/dist/src/core/types.specs.js +40 -0
- package/dist/src/core/types.specs.js.map +1 -0
- package/dist/src/graph/types.d.ts +17 -0
- package/dist/src/graph/types.d.ts.map +1 -0
- package/dist/src/graph/types.js +2 -0
- package/dist/src/graph/types.js.map +1 -0
- package/dist/src/graph/types.specs.d.ts +2 -0
- package/dist/src/graph/types.specs.d.ts.map +1 -0
- package/dist/src/graph/types.specs.js +148 -0
- package/dist/src/graph/types.specs.js.map +1 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +12 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logging/event-logger.d.ts +21 -0
- package/dist/src/logging/event-logger.d.ts.map +1 -0
- package/dist/src/logging/event-logger.js +31 -0
- package/dist/src/logging/event-logger.js.map +1 -0
- package/dist/src/logging/event-logger.specs.d.ts +2 -0
- package/dist/src/logging/event-logger.specs.d.ts.map +1 -0
- package/dist/src/logging/event-logger.specs.js +81 -0
- package/dist/src/logging/event-logger.specs.js.map +1 -0
- package/dist/src/plugins/handler-adapter.d.ts +5 -0
- package/dist/src/plugins/handler-adapter.d.ts.map +1 -0
- package/dist/src/plugins/handler-adapter.js +17 -0
- package/dist/src/plugins/handler-adapter.js.map +1 -0
- package/dist/src/plugins/handler-adapter.specs.d.ts +2 -0
- package/dist/src/plugins/handler-adapter.specs.d.ts.map +1 -0
- package/dist/src/plugins/handler-adapter.specs.js +129 -0
- package/dist/src/plugins/handler-adapter.specs.js.map +1 -0
- package/dist/src/plugins/plugin-loader.d.ts +25 -0
- package/dist/src/plugins/plugin-loader.d.ts.map +1 -0
- package/dist/src/plugins/plugin-loader.js +150 -0
- package/dist/src/plugins/plugin-loader.js.map +1 -0
- package/dist/src/plugins/plugin-loader.specs.d.ts +2 -0
- package/dist/src/plugins/plugin-loader.specs.d.ts.map +1 -0
- package/dist/src/plugins/plugin-loader.specs.js +246 -0
- package/dist/src/plugins/plugin-loader.specs.js.map +1 -0
- package/dist/src/runtime/await-tracker.d.ts +10 -0
- package/dist/src/runtime/await-tracker.d.ts.map +1 -0
- package/dist/src/runtime/await-tracker.js +42 -0
- package/dist/src/runtime/await-tracker.js.map +1 -0
- package/dist/src/runtime/await-tracker.specs.d.ts +2 -0
- package/dist/src/runtime/await-tracker.specs.d.ts.map +1 -0
- package/dist/src/runtime/await-tracker.specs.js +46 -0
- package/dist/src/runtime/await-tracker.specs.js.map +1 -0
- package/dist/src/runtime/context.d.ts +12 -0
- package/dist/src/runtime/context.d.ts.map +1 -0
- package/dist/src/runtime/context.js +2 -0
- package/dist/src/runtime/context.js.map +1 -0
- package/dist/src/runtime/context.specs.d.ts +2 -0
- package/dist/src/runtime/context.specs.d.ts.map +1 -0
- package/dist/src/runtime/context.specs.js +26 -0
- package/dist/src/runtime/context.specs.js.map +1 -0
- package/dist/src/runtime/event-command-map.d.ts +15 -0
- package/dist/src/runtime/event-command-map.d.ts.map +1 -0
- package/dist/src/runtime/event-command-map.js +26 -0
- package/dist/src/runtime/event-command-map.js.map +1 -0
- package/dist/src/runtime/event-command-map.specs.d.ts +2 -0
- package/dist/src/runtime/event-command-map.specs.d.ts.map +1 -0
- package/dist/src/runtime/event-command-map.specs.js +108 -0
- package/dist/src/runtime/event-command-map.specs.js.map +1 -0
- package/dist/src/runtime/phased-executor.d.ts +29 -0
- package/dist/src/runtime/phased-executor.d.ts.map +1 -0
- package/dist/src/runtime/phased-executor.js +164 -0
- package/dist/src/runtime/phased-executor.js.map +1 -0
- package/dist/src/runtime/phased-executor.specs.d.ts +2 -0
- package/dist/src/runtime/phased-executor.specs.d.ts.map +1 -0
- package/dist/src/runtime/phased-executor.specs.js +256 -0
- package/dist/src/runtime/phased-executor.specs.js.map +1 -0
- package/dist/src/runtime/pipeline-runtime.d.ts +17 -0
- package/dist/src/runtime/pipeline-runtime.d.ts.map +1 -0
- package/dist/src/runtime/pipeline-runtime.js +87 -0
- package/dist/src/runtime/pipeline-runtime.js.map +1 -0
- package/dist/src/runtime/pipeline-runtime.specs.d.ts +2 -0
- package/dist/src/runtime/pipeline-runtime.specs.d.ts.map +1 -0
- package/dist/src/runtime/pipeline-runtime.specs.js +192 -0
- package/dist/src/runtime/pipeline-runtime.specs.js.map +1 -0
- package/dist/src/runtime/settled-tracker.d.ts +42 -0
- package/dist/src/runtime/settled-tracker.d.ts.map +1 -0
- package/dist/src/runtime/settled-tracker.js +161 -0
- package/dist/src/runtime/settled-tracker.js.map +1 -0
- package/dist/src/runtime/settled-tracker.specs.d.ts +2 -0
- package/dist/src/runtime/settled-tracker.specs.d.ts.map +1 -0
- package/dist/src/runtime/settled-tracker.specs.js +361 -0
- package/dist/src/runtime/settled-tracker.specs.js.map +1 -0
- package/dist/src/server/full-orchestration.e2e.specs.d.ts +2 -0
- package/dist/src/server/full-orchestration.e2e.specs.d.ts.map +1 -0
- package/dist/src/server/full-orchestration.e2e.specs.js +561 -0
- package/dist/src/server/full-orchestration.e2e.specs.js.map +1 -0
- package/dist/src/server/pipeline-server.d.ts +59 -0
- package/dist/src/server/pipeline-server.d.ts.map +1 -0
- package/dist/src/server/pipeline-server.e2e.specs.d.ts +2 -0
- package/dist/src/server/pipeline-server.e2e.specs.d.ts.map +1 -0
- package/dist/src/server/pipeline-server.e2e.specs.js +381 -0
- package/dist/src/server/pipeline-server.e2e.specs.js.map +1 -0
- package/dist/src/server/pipeline-server.js +527 -0
- package/dist/src/server/pipeline-server.js.map +1 -0
- package/dist/src/server/pipeline-server.specs.d.ts +2 -0
- package/dist/src/server/pipeline-server.specs.d.ts.map +1 -0
- package/dist/src/server/pipeline-server.specs.js +662 -0
- package/dist/src/server/pipeline-server.specs.js.map +1 -0
- package/dist/src/server/sse-manager.d.ts +12 -0
- package/dist/src/server/sse-manager.d.ts.map +1 -0
- package/dist/src/server/sse-manager.js +63 -0
- package/dist/src/server/sse-manager.js.map +1 -0
- package/dist/src/server/sse-manager.specs.d.ts +2 -0
- package/dist/src/server/sse-manager.specs.d.ts.map +1 -0
- package/dist/src/server/sse-manager.specs.js +158 -0
- package/dist/src/server/sse-manager.specs.js.map +1 -0
- package/dist/src/testing/event-capture.d.ts +14 -0
- package/dist/src/testing/event-capture.d.ts.map +1 -0
- package/dist/src/testing/event-capture.js +55 -0
- package/dist/src/testing/event-capture.js.map +1 -0
- package/dist/src/testing/event-capture.specs.d.ts +2 -0
- package/dist/src/testing/event-capture.specs.d.ts.map +1 -0
- package/dist/src/testing/event-capture.specs.js +114 -0
- package/dist/src/testing/event-capture.specs.js.map +1 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.d.ts +7 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.d.ts.map +1 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.js +168 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.js.map +1 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts +2 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.specs.d.ts.map +1 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js +263 -0
- package/dist/src/testing/fixtures/kanban-full.pipeline.specs.js.map +1 -0
- package/dist/src/testing/fixtures/kanban-todo.config.d.ts +3 -0
- package/dist/src/testing/fixtures/kanban-todo.config.d.ts.map +1 -0
- package/dist/src/testing/fixtures/kanban-todo.config.js +19 -0
- package/dist/src/testing/fixtures/kanban-todo.config.js.map +1 -0
- package/dist/src/testing/fixtures/kanban.pipeline.d.ts +5 -0
- package/dist/src/testing/fixtures/kanban.pipeline.d.ts.map +1 -0
- package/dist/src/testing/fixtures/kanban.pipeline.js +76 -0
- package/dist/src/testing/fixtures/kanban.pipeline.js.map +1 -0
- package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts +2 -0
- package/dist/src/testing/fixtures/kanban.pipeline.specs.d.ts.map +1 -0
- package/dist/src/testing/fixtures/kanban.pipeline.specs.js +29 -0
- package/dist/src/testing/fixtures/kanban.pipeline.specs.js.map +1 -0
- package/dist/src/testing/kanban-todo.e2e.specs.d.ts +2 -0
- package/dist/src/testing/kanban-todo.e2e.specs.d.ts.map +1 -0
- package/dist/src/testing/kanban-todo.e2e.specs.js +160 -0
- package/dist/src/testing/kanban-todo.e2e.specs.js.map +1 -0
- package/dist/src/testing/mock-handlers.d.ts +21 -0
- package/dist/src/testing/mock-handlers.d.ts.map +1 -0
- package/dist/src/testing/mock-handlers.js +34 -0
- package/dist/src/testing/mock-handlers.js.map +1 -0
- package/dist/src/testing/mock-handlers.specs.d.ts +2 -0
- package/dist/src/testing/mock-handlers.specs.d.ts.map +1 -0
- package/dist/src/testing/mock-handlers.specs.js +193 -0
- package/dist/src/testing/mock-handlers.specs.js.map +1 -0
- package/dist/src/testing/real-execution.e2e.specs.d.ts +2 -0
- package/dist/src/testing/real-execution.e2e.specs.d.ts.map +1 -0
- package/dist/src/testing/real-execution.e2e.specs.js +140 -0
- package/dist/src/testing/real-execution.e2e.specs.js.map +1 -0
- package/dist/src/testing/real-plugin.e2e.specs.d.ts +2 -0
- package/dist/src/testing/real-plugin.e2e.specs.d.ts.map +1 -0
- package/dist/src/testing/real-plugin.e2e.specs.js +65 -0
- package/dist/src/testing/real-plugin.e2e.specs.js.map +1 -0
- package/dist/src/testing/server-startup.e2e.specs.d.ts +2 -0
- package/dist/src/testing/server-startup.e2e.specs.d.ts.map +1 -0
- package/dist/src/testing/server-startup.e2e.specs.js +104 -0
- package/dist/src/testing/server-startup.e2e.specs.js.map +1 -0
- package/dist/src/testing/snapshot-compare.d.ts +18 -0
- package/dist/src/testing/snapshot-compare.d.ts.map +1 -0
- package/dist/src/testing/snapshot-compare.js +86 -0
- package/dist/src/testing/snapshot-compare.js.map +1 -0
- package/dist/src/testing/snapshot-compare.specs.d.ts +2 -0
- package/dist/src/testing/snapshot-compare.specs.d.ts.map +1 -0
- package/dist/src/testing/snapshot-compare.specs.js +112 -0
- package/dist/src/testing/snapshot-compare.specs.js.map +1 -0
- package/dist/src/testing/snapshot-sanitize.d.ts +8 -0
- package/dist/src/testing/snapshot-sanitize.d.ts.map +1 -0
- package/dist/src/testing/snapshot-sanitize.js +10 -0
- package/dist/src/testing/snapshot-sanitize.js.map +1 -0
- package/dist/src/testing/snapshot-sanitize.specs.d.ts +2 -0
- package/dist/src/testing/snapshot-sanitize.specs.d.ts.map +1 -0
- package/dist/src/testing/snapshot-sanitize.specs.js +104 -0
- package/dist/src/testing/snapshot-sanitize.specs.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/docs/testing-analysis.md +395 -0
- package/package.json +31 -0
- package/pipeline-api-new.md +1078 -0
- package/pomodoro-plan.md +651 -0
- package/scripts/run-kanban-e2e.ts +219 -0
- package/scripts/start-server.ts +64 -0
- package/snapshots/e2e-run-2025-12-22T15-52-03.json +613 -0
- package/snapshots/e2e-run-2025-12-22T16-51-30.json +699 -0
- package/src/builder/define.specs.ts +531 -0
- package/src/builder/define.ts +700 -0
- package/src/config/pipeline-config.ts +32 -0
- package/src/core/descriptors.specs.ts +28 -0
- package/src/core/descriptors.ts +99 -0
- package/src/core/types.specs.ts +44 -0
- package/src/core/types.ts +16 -0
- package/src/graph/types.specs.ts +176 -0
- package/src/graph/types.ts +19 -0
- package/src/index.ts +54 -0
- package/src/logging/event-logger.specs.ts +100 -0
- package/src/logging/event-logger.ts +50 -0
- package/src/plugins/handler-adapter.specs.ts +164 -0
- package/src/plugins/handler-adapter.ts +21 -0
- package/src/plugins/plugin-loader.specs.ts +295 -0
- package/src/plugins/plugin-loader.ts +202 -0
- package/src/runtime/await-tracker.specs.ts +52 -0
- package/src/runtime/await-tracker.ts +50 -0
- package/src/runtime/context.specs.ts +28 -0
- package/src/runtime/context.ts +13 -0
- package/src/runtime/event-command-map.specs.ts +136 -0
- package/src/runtime/event-command-map.ts +38 -0
- package/src/runtime/phased-executor.specs.ts +358 -0
- package/src/runtime/phased-executor.ts +224 -0
- package/src/runtime/pipeline-runtime.specs.ts +214 -0
- package/src/runtime/pipeline-runtime.ts +119 -0
- package/src/runtime/settled-tracker.specs.ts +448 -0
- package/src/runtime/settled-tracker.ts +237 -0
- package/src/server/full-orchestration.e2e.specs.ts +672 -0
- package/src/server/pipeline-server.e2e.specs.ts +505 -0
- package/src/server/pipeline-server.specs.ts +761 -0
- package/src/server/pipeline-server.ts +656 -0
- package/src/server/sse-manager.specs.ts +208 -0
- package/src/server/sse-manager.ts +79 -0
- package/src/testing/event-capture.specs.ts +143 -0
- package/src/testing/event-capture.ts +65 -0
- package/src/testing/fixtures/kanban-full.pipeline.specs.ts +337 -0
- package/src/testing/fixtures/kanban-full.pipeline.ts +225 -0
- package/src/testing/fixtures/kanban-todo.config.ts +19 -0
- package/src/testing/fixtures/kanban.pipeline.specs.ts +33 -0
- package/src/testing/fixtures/kanban.pipeline.ts +124 -0
- package/src/testing/kanban-todo.e2e.specs.ts +209 -0
- package/src/testing/mock-handlers.specs.ts +229 -0
- package/src/testing/mock-handlers.ts +58 -0
- package/src/testing/real-execution.e2e.specs.ts +193 -0
- package/src/testing/real-plugin.e2e.specs.ts +94 -0
- package/src/testing/server-startup.e2e.specs.ts +162 -0
- package/src/testing/snapshot-compare.specs.ts +136 -0
- package/src/testing/snapshot-compare.ts +106 -0
- package/src/testing/snapshot-sanitize.specs.ts +131 -0
- package/src/testing/snapshot-sanitize.ts +17 -0
- package/tsconfig.json +11 -0
- package/tsconfig.test.json +9 -0
- package/vitest.config.ts +29 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { adaptHandler, adaptHandlers } from './handler-adapter';
|
|
3
|
+
import type { CommandHandlerMetadata } from './plugin-loader';
|
|
4
|
+
|
|
5
|
+
describe('handler-adapter', () => {
|
|
6
|
+
describe('adaptHandler', () => {
|
|
7
|
+
it('should preserve name from source handler', () => {
|
|
8
|
+
const source: CommandHandlerMetadata = {
|
|
9
|
+
name: 'CheckTests',
|
|
10
|
+
handle: vi.fn().mockResolvedValue({ type: 'TestsCheckPassed', data: {} }),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const adapted = adaptHandler(source);
|
|
14
|
+
|
|
15
|
+
expect(adapted.name).toBe('CheckTests');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should preserve alias from source handler', () => {
|
|
19
|
+
const source: CommandHandlerMetadata = {
|
|
20
|
+
name: 'CheckTests',
|
|
21
|
+
alias: 'check:tests',
|
|
22
|
+
handle: vi.fn().mockResolvedValue({ type: 'TestsCheckPassed', data: {} }),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const adapted = adaptHandler(source);
|
|
26
|
+
|
|
27
|
+
expect(adapted.alias).toBe('check:tests');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should preserve description from source handler', () => {
|
|
31
|
+
const source: CommandHandlerMetadata = {
|
|
32
|
+
name: 'CheckTypes',
|
|
33
|
+
description: 'Runs TypeScript type checking',
|
|
34
|
+
handle: vi.fn().mockResolvedValue({ type: 'TypeCheckPassed', data: {} }),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const adapted = adaptHandler(source);
|
|
38
|
+
|
|
39
|
+
expect(adapted.description).toBe('Runs TypeScript type checking');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should preserve events from source handler', () => {
|
|
43
|
+
const source: CommandHandlerMetadata = {
|
|
44
|
+
name: 'CheckTests',
|
|
45
|
+
events: ['TestsCheckPassed', 'TestsCheckFailed'],
|
|
46
|
+
handle: vi.fn().mockResolvedValue({ type: 'TestsCheckPassed', data: {} }),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const adapted = adaptHandler(source);
|
|
50
|
+
|
|
51
|
+
expect(adapted.events).toEqual(['TestsCheckPassed', 'TestsCheckFailed']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should preserve fields from source handler', () => {
|
|
55
|
+
const source: CommandHandlerMetadata = {
|
|
56
|
+
name: 'CheckTests',
|
|
57
|
+
fields: { targetDirectory: { type: 'string', required: true } },
|
|
58
|
+
handle: vi.fn().mockResolvedValue({ type: 'TestsCheckPassed', data: {} }),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const adapted = adaptHandler(source);
|
|
62
|
+
|
|
63
|
+
expect(adapted.fields).toEqual({ targetDirectory: { type: 'string', required: true } });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should call source handle function with command', async () => {
|
|
67
|
+
const mockHandle = vi.fn().mockResolvedValue({ type: 'TestsCheckPassed', data: {} });
|
|
68
|
+
const source: CommandHandlerMetadata = {
|
|
69
|
+
name: 'CheckTests',
|
|
70
|
+
handle: mockHandle,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const adapted = adaptHandler(source);
|
|
74
|
+
const command = { type: 'CheckTests', data: { targetDirectory: './src' } };
|
|
75
|
+
await adapted.handle(command);
|
|
76
|
+
|
|
77
|
+
expect(mockHandle).toHaveBeenCalledWith(command);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return single event from handle', async () => {
|
|
81
|
+
const source: CommandHandlerMetadata = {
|
|
82
|
+
name: 'CheckTests',
|
|
83
|
+
handle: vi.fn().mockResolvedValue({ type: 'TestsCheckPassed', data: { passed: true } }),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const adapted = adaptHandler(source);
|
|
87
|
+
const result = await adapted.handle({ type: 'CheckTests', data: {} });
|
|
88
|
+
|
|
89
|
+
expect(result).toEqual({ type: 'TestsCheckPassed', data: { passed: true } });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return array of events from handle', async () => {
|
|
93
|
+
const events = [
|
|
94
|
+
{ type: 'SliceGenerated', data: { slicePath: './slice1' } },
|
|
95
|
+
{ type: 'ServerGenerated', data: {} },
|
|
96
|
+
];
|
|
97
|
+
const source: CommandHandlerMetadata = {
|
|
98
|
+
name: 'GenerateServer',
|
|
99
|
+
handle: vi.fn().mockResolvedValue(events),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const adapted = adaptHandler(source);
|
|
103
|
+
const result = await adapted.handle({ type: 'GenerateServer', data: {} });
|
|
104
|
+
|
|
105
|
+
expect(result).toEqual(events);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle undefined optional fields', () => {
|
|
109
|
+
const source: CommandHandlerMetadata = {
|
|
110
|
+
name: 'SimpleHandler',
|
|
111
|
+
handle: vi.fn().mockResolvedValue({ type: 'Done', data: {} }),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const adapted = adaptHandler(source);
|
|
115
|
+
|
|
116
|
+
expect(adapted.alias).toBeUndefined();
|
|
117
|
+
expect(adapted.description).toBeUndefined();
|
|
118
|
+
expect(adapted.events).toBeUndefined();
|
|
119
|
+
expect(adapted.fields).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('adaptHandlers', () => {
|
|
124
|
+
it('should adapt multiple handlers', () => {
|
|
125
|
+
const sources: CommandHandlerMetadata[] = [
|
|
126
|
+
{ name: 'CheckTests', handle: vi.fn().mockResolvedValue({ type: 'TestsCheckPassed', data: {} }) },
|
|
127
|
+
{ name: 'CheckTypes', handle: vi.fn().mockResolvedValue({ type: 'TypeCheckPassed', data: {} }) },
|
|
128
|
+
{ name: 'CheckLint', handle: vi.fn().mockResolvedValue({ type: 'LintCheckPassed', data: {} }) },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const adapted = adaptHandlers(sources);
|
|
132
|
+
|
|
133
|
+
expect(adapted).toHaveLength(3);
|
|
134
|
+
expect(adapted.map((h) => h.name)).toEqual(['CheckTests', 'CheckTypes', 'CheckLint']);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should return empty array for empty input', () => {
|
|
138
|
+
const adapted = adaptHandlers([]);
|
|
139
|
+
|
|
140
|
+
expect(adapted).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should preserve all metadata on adapted handlers', () => {
|
|
144
|
+
const sources: CommandHandlerMetadata[] = [
|
|
145
|
+
{
|
|
146
|
+
name: 'CheckTests',
|
|
147
|
+
alias: 'check:tests',
|
|
148
|
+
description: 'Run tests',
|
|
149
|
+
events: ['TestsCheckPassed', 'TestsCheckFailed'],
|
|
150
|
+
fields: { scope: { type: 'string' } },
|
|
151
|
+
handle: vi.fn().mockResolvedValue({ type: 'TestsCheckPassed', data: {} }),
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const adapted = adaptHandlers(sources);
|
|
156
|
+
|
|
157
|
+
expect(adapted[0].name).toBe('CheckTests');
|
|
158
|
+
expect(adapted[0].alias).toBe('check:tests');
|
|
159
|
+
expect(adapted[0].description).toBe('Run tests');
|
|
160
|
+
expect(adapted[0].events).toEqual(['TestsCheckPassed', 'TestsCheckFailed']);
|
|
161
|
+
expect(adapted[0].fields).toEqual({ scope: { type: 'string' } });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Command, Event } from '@auto-engineer/message-bus';
|
|
2
|
+
import type { CommandHandlerWithMetadata } from '../server/pipeline-server';
|
|
3
|
+
import type { CommandHandlerMetadata } from './plugin-loader';
|
|
4
|
+
|
|
5
|
+
export function adaptHandler(source: CommandHandlerMetadata): CommandHandlerWithMetadata {
|
|
6
|
+
return {
|
|
7
|
+
name: source.name,
|
|
8
|
+
alias: source.alias,
|
|
9
|
+
description: source.description,
|
|
10
|
+
events: source.events,
|
|
11
|
+
fields: source.fields,
|
|
12
|
+
handle: async (command: Command): Promise<Event | Event[]> => {
|
|
13
|
+
const result = await source.handle(command);
|
|
14
|
+
return result as Event | Event[];
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function adaptHandlers(sources: CommandHandlerMetadata[]): CommandHandlerWithMetadata[] {
|
|
20
|
+
return sources.map(adaptHandler);
|
|
21
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { PluginLoader, type PluginLoaderDeps } from './plugin-loader';
|
|
3
|
+
|
|
4
|
+
function createMockDeps(overrides?: Partial<PluginLoaderDeps>): PluginLoaderDeps {
|
|
5
|
+
return {
|
|
6
|
+
existsSync: vi.fn().mockReturnValue(false),
|
|
7
|
+
importModule: vi.fn().mockResolvedValue(null),
|
|
8
|
+
...overrides,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createMockHandler(name: string, events?: string[]) {
|
|
13
|
+
return {
|
|
14
|
+
name,
|
|
15
|
+
alias: `${name.toLowerCase()}:cmd`,
|
|
16
|
+
description: `${name} handler`,
|
|
17
|
+
events: events ?? [`${name}Done`],
|
|
18
|
+
fields: { field1: { type: 'string' } },
|
|
19
|
+
handle: vi.fn().mockResolvedValue({ type: `${name}Done`, data: {} }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('PluginLoader', () => {
|
|
24
|
+
describe('loadPlugin', () => {
|
|
25
|
+
it('should load COMMANDS from workspace package', async () => {
|
|
26
|
+
const mockHandler = createMockHandler('CheckTests', ['TestsCheckPassed', 'TestsCheckFailed']);
|
|
27
|
+
const deps = createMockDeps({
|
|
28
|
+
existsSync: vi.fn().mockImplementation((path: string) => path.includes('packages/server-checks')),
|
|
29
|
+
importModule: vi.fn().mockResolvedValue({ COMMANDS: [mockHandler] }),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
33
|
+
const handlers = await loader.loadPlugin('@auto-engineer/server-checks');
|
|
34
|
+
|
|
35
|
+
expect(handlers).toHaveLength(1);
|
|
36
|
+
expect(handlers[0].name).toBe('CheckTests');
|
|
37
|
+
expect(handlers[0].events).toEqual(['TestsCheckPassed', 'TestsCheckFailed']);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should load COMMANDS from node_modules when workspace not found', async () => {
|
|
41
|
+
const mockHandler = createMockHandler('GenerateServer');
|
|
42
|
+
const deps = createMockDeps({
|
|
43
|
+
existsSync: vi.fn().mockImplementation((path: string) => path.includes('node_modules')),
|
|
44
|
+
importModule: vi.fn().mockResolvedValue({ COMMANDS: [mockHandler] }),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
48
|
+
const handlers = await loader.loadPlugin('@auto-engineer/server-generator');
|
|
49
|
+
|
|
50
|
+
expect(handlers).toHaveLength(1);
|
|
51
|
+
expect(handlers[0].name).toBe('GenerateServer');
|
|
52
|
+
expect(deps.existsSync).toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should fallback to direct import when neither workspace nor node_modules found', async () => {
|
|
56
|
+
const mockHandler = createMockHandler('DirectImport');
|
|
57
|
+
const deps = createMockDeps({
|
|
58
|
+
existsSync: vi.fn().mockReturnValue(false),
|
|
59
|
+
importModule: vi.fn().mockResolvedValue({ COMMANDS: [mockHandler] }),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
63
|
+
const handlers = await loader.loadPlugin('some-package');
|
|
64
|
+
|
|
65
|
+
expect(handlers).toHaveLength(1);
|
|
66
|
+
expect(handlers[0].name).toBe('DirectImport');
|
|
67
|
+
expect(deps.importModule).toHaveBeenCalledWith('some-package');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return empty array when import fails', async () => {
|
|
71
|
+
const deps = createMockDeps({
|
|
72
|
+
existsSync: vi.fn().mockReturnValue(false),
|
|
73
|
+
importModule: vi.fn().mockRejectedValue(new Error('Module not found')),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
77
|
+
const handlers = await loader.loadPlugin('@auto-engineer/nonexistent');
|
|
78
|
+
|
|
79
|
+
expect(handlers).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should extract handler metadata', async () => {
|
|
83
|
+
const mockHandler = {
|
|
84
|
+
name: 'CheckTypes',
|
|
85
|
+
alias: 'check:types',
|
|
86
|
+
description: 'Type checking',
|
|
87
|
+
events: ['TypeCheckPassed', 'TypeCheckFailed'],
|
|
88
|
+
fields: { targetDir: { type: 'string', required: true } },
|
|
89
|
+
handle: vi.fn(),
|
|
90
|
+
};
|
|
91
|
+
const deps = createMockDeps({
|
|
92
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
93
|
+
importModule: vi.fn().mockResolvedValue({ COMMANDS: [mockHandler] }),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
97
|
+
const handlers = await loader.loadPlugin('@auto-engineer/server-checks');
|
|
98
|
+
|
|
99
|
+
expect(handlers[0]).toEqual({
|
|
100
|
+
name: 'CheckTypes',
|
|
101
|
+
alias: 'check:types',
|
|
102
|
+
description: 'Type checking',
|
|
103
|
+
events: ['TypeCheckPassed', 'TypeCheckFailed'],
|
|
104
|
+
fields: { targetDir: { type: 'string', required: true } },
|
|
105
|
+
handle: mockHandler.handle,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should skip invalid handlers in COMMANDS array', async () => {
|
|
110
|
+
const validHandler = createMockHandler('Valid');
|
|
111
|
+
const deps = createMockDeps({
|
|
112
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
113
|
+
importModule: vi.fn().mockResolvedValue({
|
|
114
|
+
COMMANDS: [validHandler, { notAHandler: true }, null, 'string', { name: 'NoHandle' }],
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
119
|
+
const handlers = await loader.loadPlugin('@auto-engineer/test');
|
|
120
|
+
|
|
121
|
+
expect(handlers).toHaveLength(1);
|
|
122
|
+
expect(handlers[0].name).toBe('Valid');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle module that is not an object', async () => {
|
|
126
|
+
const deps = createMockDeps({
|
|
127
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
128
|
+
importModule: vi.fn().mockResolvedValue('not an object'),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
132
|
+
const handlers = await loader.loadPlugin('@auto-engineer/test');
|
|
133
|
+
|
|
134
|
+
expect(handlers).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle null module', async () => {
|
|
138
|
+
const deps = createMockDeps({
|
|
139
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
140
|
+
importModule: vi.fn().mockResolvedValue(null),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
144
|
+
const handlers = await loader.loadPlugin('@auto-engineer/test');
|
|
145
|
+
|
|
146
|
+
expect(handlers).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should extract COMMANDS from default export', async () => {
|
|
150
|
+
const mockHandler = createMockHandler('FromDefault');
|
|
151
|
+
const deps = createMockDeps({
|
|
152
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
153
|
+
importModule: vi.fn().mockResolvedValue({
|
|
154
|
+
default: { COMMANDS: [mockHandler] },
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
159
|
+
const handlers = await loader.loadPlugin('@auto-engineer/test');
|
|
160
|
+
|
|
161
|
+
expect(handlers).toHaveLength(1);
|
|
162
|
+
expect(handlers[0].name).toBe('FromDefault');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should return empty when default export has no COMMANDS', async () => {
|
|
166
|
+
const deps = createMockDeps({
|
|
167
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
168
|
+
importModule: vi.fn().mockResolvedValue({
|
|
169
|
+
default: { somethingElse: true },
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
174
|
+
const handlers = await loader.loadPlugin('@auto-engineer/test');
|
|
175
|
+
|
|
176
|
+
expect(handlers).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should return empty when default export is not an object', async () => {
|
|
180
|
+
const deps = createMockDeps({
|
|
181
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
182
|
+
importModule: vi.fn().mockResolvedValue({
|
|
183
|
+
default: 'not an object',
|
|
184
|
+
}),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
188
|
+
const handlers = await loader.loadPlugin('@auto-engineer/test');
|
|
189
|
+
|
|
190
|
+
expect(handlers).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should return empty when extractCommands throws', async () => {
|
|
194
|
+
const deps = createMockDeps({
|
|
195
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
196
|
+
importModule: vi.fn().mockImplementation(() => {
|
|
197
|
+
throw new Error('Unexpected error');
|
|
198
|
+
}),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
202
|
+
const handlers = await loader.loadPlugin('@auto-engineer/test');
|
|
203
|
+
|
|
204
|
+
expect(handlers).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('loadPlugins', () => {
|
|
209
|
+
it('should load handlers from multiple packages', async () => {
|
|
210
|
+
const handler1 = createMockHandler('Handler1');
|
|
211
|
+
const handler2 = createMockHandler('Handler2');
|
|
212
|
+
const deps = createMockDeps({
|
|
213
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
214
|
+
importModule: vi
|
|
215
|
+
.fn()
|
|
216
|
+
.mockResolvedValueOnce({ COMMANDS: [handler1] })
|
|
217
|
+
.mockResolvedValueOnce({ COMMANDS: [handler2] }),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
221
|
+
const handlers = await loader.loadPlugins(['@auto-engineer/pkg1', '@auto-engineer/pkg2']);
|
|
222
|
+
|
|
223
|
+
expect(handlers).toHaveLength(2);
|
|
224
|
+
expect(handlers.map((h) => h.name)).toEqual(['Handler1', 'Handler2']);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should continue loading even if one package fails', async () => {
|
|
228
|
+
const handler = createMockHandler('Handler');
|
|
229
|
+
const deps = createMockDeps({
|
|
230
|
+
existsSync: vi.fn().mockReturnValue(true),
|
|
231
|
+
importModule: vi
|
|
232
|
+
.fn()
|
|
233
|
+
.mockRejectedValueOnce(new Error('Failed'))
|
|
234
|
+
.mockResolvedValueOnce({ COMMANDS: [handler] }),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const loader = new PluginLoader('/workspace', deps);
|
|
238
|
+
const handlers = await loader.loadPlugins(['@auto-engineer/failing', '@auto-engineer/working']);
|
|
239
|
+
|
|
240
|
+
expect(handlers).toHaveLength(1);
|
|
241
|
+
expect(handlers[0].name).toBe('Handler');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('constructor', () => {
|
|
246
|
+
it('should use default workspace root when not provided', () => {
|
|
247
|
+
const loader = new PluginLoader();
|
|
248
|
+
expect(loader).toBeDefined();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should use custom workspace root when provided', () => {
|
|
252
|
+
const loader = new PluginLoader('/custom/workspace');
|
|
253
|
+
expect(loader).toBeDefined();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('integration', { timeout: 30000 }, () => {
|
|
258
|
+
it('should load real package using default deps', async () => {
|
|
259
|
+
const loader = new PluginLoader();
|
|
260
|
+
const handlers = await loader.loadPlugin('@auto-engineer/server-checks');
|
|
261
|
+
const handlerNames = handlers.map((h) => h.name);
|
|
262
|
+
expect(handlerNames).toContain('CheckTests');
|
|
263
|
+
expect(handlerNames).toContain('CheckTypes');
|
|
264
|
+
expect(handlerNames).toContain('CheckLint');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should load handlers from multiple real packages', async () => {
|
|
268
|
+
const loader = new PluginLoader();
|
|
269
|
+
const handlers = await loader.loadPlugins([
|
|
270
|
+
'@auto-engineer/server-checks',
|
|
271
|
+
'@auto-engineer/server-generator-apollo-emmett',
|
|
272
|
+
]);
|
|
273
|
+
const handlerNames = handlers.map((h) => h.name);
|
|
274
|
+
expect(handlerNames).toContain('CheckTests');
|
|
275
|
+
expect(handlerNames).toContain('CheckTypes');
|
|
276
|
+
expect(handlerNames).toContain('CheckLint');
|
|
277
|
+
expect(handlerNames).toContain('GenerateServer');
|
|
278
|
+
expect(handlers.length).toBeGreaterThan(3);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should load ExportSchema from narrative package node entry', async () => {
|
|
282
|
+
const loader = new PluginLoader();
|
|
283
|
+
const handlers = await loader.loadPlugin('@auto-engineer/narrative');
|
|
284
|
+
const handlerNames = handlers.map((h) => h.name);
|
|
285
|
+
expect(handlerNames).toContain('ExportSchema');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should load GenerateClient from frontend-generator package', async () => {
|
|
289
|
+
const loader = new PluginLoader();
|
|
290
|
+
const handlers = await loader.loadPlugin('@auto-engineer/frontend-generator-react-graphql');
|
|
291
|
+
const handlerNames = handlers.map((h) => h.name);
|
|
292
|
+
expect(handlerNames).toContain('GenerateClient');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { existsSync as fsExistsSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export interface CommandHandlerMetadata {
|
|
6
|
+
name: string;
|
|
7
|
+
alias?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
events?: string[];
|
|
10
|
+
fields?: Record<string, unknown>;
|
|
11
|
+
handle: (command: unknown) => Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UnifiedCommandHandler {
|
|
15
|
+
name: string;
|
|
16
|
+
alias?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
events?: string[];
|
|
19
|
+
fields?: Record<string, unknown>;
|
|
20
|
+
handle: (command: unknown) => Promise<unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isValidHandler(obj: unknown): obj is UnifiedCommandHandler {
|
|
24
|
+
return (
|
|
25
|
+
typeof obj === 'object' &&
|
|
26
|
+
obj !== null &&
|
|
27
|
+
'name' in obj &&
|
|
28
|
+
typeof (obj as UnifiedCommandHandler).name === 'string' &&
|
|
29
|
+
'handle' in obj &&
|
|
30
|
+
typeof (obj as UnifiedCommandHandler).handle === 'function'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isHandlerExportKey(key: string): boolean {
|
|
35
|
+
return key.endsWith('CommandHandler') || key.endsWith('commandHandler');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isHandlerObject(value: unknown): boolean {
|
|
39
|
+
return typeof value === 'object' && value !== null && 'name' in value && 'handle' in value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hasCommandsArray(obj: Record<string, unknown>): boolean {
|
|
43
|
+
return 'COMMANDS' in obj && Array.isArray(obj.COMMANDS);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getDefaultExport(obj: Record<string, unknown>): Record<string, unknown> | null {
|
|
47
|
+
if ('default' in obj && typeof obj.default === 'object' && obj.default !== null) {
|
|
48
|
+
return obj.default as Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PluginLoaderDeps {
|
|
54
|
+
existsSync: (path: string) => boolean;
|
|
55
|
+
importModule: (path: string) => Promise<unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const defaultDeps: PluginLoaderDeps = {
|
|
59
|
+
existsSync: fsExistsSync,
|
|
60
|
+
importModule: (path: string) => import(path),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export class PluginLoader {
|
|
64
|
+
private readonly workspaceRoot: string;
|
|
65
|
+
private readonly deps: PluginLoaderDeps;
|
|
66
|
+
|
|
67
|
+
constructor(workspaceRoot?: string, deps?: PluginLoaderDeps) {
|
|
68
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
69
|
+
this.workspaceRoot = workspaceRoot ?? resolve(currentDir, '../../../../');
|
|
70
|
+
this.deps = deps ?? defaultDeps;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async loadPlugin(packageName: string): Promise<CommandHandlerMetadata[]> {
|
|
74
|
+
const handlers: CommandHandlerMetadata[] = [];
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const module = await this.tryLoadModule(packageName);
|
|
78
|
+
if (module === null) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const commands = this.extractCommands(module);
|
|
83
|
+
for (const cmd of commands) {
|
|
84
|
+
if (isValidHandler(cmd)) {
|
|
85
|
+
handlers.push({
|
|
86
|
+
name: cmd.name,
|
|
87
|
+
alias: cmd.alias,
|
|
88
|
+
description: cmd.description,
|
|
89
|
+
events: cmd.events,
|
|
90
|
+
fields: cmd.fields,
|
|
91
|
+
handle: cmd.handle,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return handlers;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async loadPlugins(packageNames: string[]): Promise<CommandHandlerMetadata[]> {
|
|
103
|
+
const allHandlers: CommandHandlerMetadata[] = [];
|
|
104
|
+
for (const packageName of packageNames) {
|
|
105
|
+
const handlers = await this.loadPlugin(packageName);
|
|
106
|
+
allHandlers.push(...handlers);
|
|
107
|
+
}
|
|
108
|
+
return allHandlers;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async tryLoadModule(packageName: string): Promise<unknown> {
|
|
112
|
+
const shortName = packageName.replace('@auto-engineer/', '');
|
|
113
|
+
const distPaths = ['dist', 'dist/src'];
|
|
114
|
+
const entryPoints = ['index.js', 'node.js'];
|
|
115
|
+
|
|
116
|
+
for (const distPath of distPaths) {
|
|
117
|
+
for (const entry of entryPoints) {
|
|
118
|
+
const workspacePath = resolve(this.workspaceRoot, 'packages', shortName, distPath, entry);
|
|
119
|
+
if (this.deps.existsSync(workspacePath)) {
|
|
120
|
+
const mod = await this.deps.importModule(workspacePath);
|
|
121
|
+
if (this.hasCommands(mod)) {
|
|
122
|
+
return mod;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const distPath of distPaths) {
|
|
129
|
+
for (const entry of entryPoints) {
|
|
130
|
+
const nodeModulesPath = resolve(this.workspaceRoot, 'node_modules', packageName, distPath, entry);
|
|
131
|
+
if (this.deps.existsSync(nodeModulesPath)) {
|
|
132
|
+
const mod = await this.deps.importModule(nodeModulesPath);
|
|
133
|
+
if (this.hasCommands(mod)) {
|
|
134
|
+
return mod;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
return await this.deps.importModule(packageName);
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private hasCommands(mod: unknown): boolean {
|
|
148
|
+
if (typeof mod !== 'object' || mod === null) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const obj = mod as Record<string, unknown>;
|
|
152
|
+
|
|
153
|
+
if (hasCommandsArray(obj)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const defaultMod = getDefaultExport(obj);
|
|
158
|
+
if (defaultMod !== null && hasCommandsArray(defaultMod)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return this.hasNamedHandlerExports(obj);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private hasNamedHandlerExports(obj: Record<string, unknown>): boolean {
|
|
166
|
+
for (const key of Object.keys(obj)) {
|
|
167
|
+
if (isHandlerExportKey(key) && isHandlerObject(obj[key])) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private extractCommands(module: unknown): unknown[] {
|
|
175
|
+
if (typeof module !== 'object' || module === null) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const mod = module as Record<string, unknown>;
|
|
180
|
+
|
|
181
|
+
if (hasCommandsArray(mod)) {
|
|
182
|
+
return mod.COMMANDS as unknown[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const defaultMod = getDefaultExport(mod);
|
|
186
|
+
if (defaultMod !== null && hasCommandsArray(defaultMod)) {
|
|
187
|
+
return defaultMod.COMMANDS as unknown[];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return this.extractNamedHandlers(mod);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private extractNamedHandlers(mod: Record<string, unknown>): unknown[] {
|
|
194
|
+
const handlers: unknown[] = [];
|
|
195
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
196
|
+
if (isHandlerExportKey(key) && isHandlerObject(value)) {
|
|
197
|
+
handlers.push(value);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return handlers;
|
|
201
|
+
}
|
|
202
|
+
}
|