@tagma/sdk 0.4.11 → 0.4.13
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/README.md +572 -566
- package/dist/adapters/websocket-approval.d.ts.map +1 -1
- package/dist/adapters/websocket-approval.js +3 -1
- package/dist/adapters/websocket-approval.js.map +1 -1
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js.map +1 -1
- package/dist/completions/exit-code.d.ts.map +1 -1
- package/dist/completions/exit-code.js.map +1 -1
- package/dist/completions/file-exists.d.ts.map +1 -1
- package/dist/completions/file-exists.js.map +1 -1
- package/dist/completions/output-check.js +2 -7
- package/dist/completions/output-check.js.map +1 -1
- package/dist/config-ops.d.ts.map +1 -1
- package/dist/config-ops.js +24 -26
- package/dist/config-ops.js.map +1 -1
- package/dist/dag.d.ts.map +1 -1
- package/dist/dag.js +1 -1
- package/dist/dag.js.map +1 -1
- package/dist/drivers/claude-code.d.ts.map +1 -1
- package/dist/drivers/claude-code.js +10 -5
- package/dist/drivers/claude-code.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +63 -29
- package/dist/engine.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +1 -3
- package/dist/hooks.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -2
- package/dist/logger.js.map +1 -1
- package/dist/pipeline-runner.d.ts.map +1 -1
- package/dist/pipeline-runner.js +10 -4
- package/dist/pipeline-runner.js.map +1 -1
- package/dist/registry.d.ts +11 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +28 -3
- package/dist/registry.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +18 -13
- package/dist/runner.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +39 -14
- package/dist/schema.js.map +1 -1
- package/dist/schema.test.js +5 -1
- package/dist/schema.test.js.map +1 -1
- package/dist/sdk.d.ts +2 -2
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +1 -1
- package/dist/sdk.js.map +1 -1
- package/dist/triggers/file.d.ts.map +1 -1
- package/dist/triggers/file.js +11 -4
- package/dist/triggers/file.js.map +1 -1
- package/dist/triggers/manual.d.ts.map +1 -1
- package/dist/triggers/manual.js +2 -1
- package/dist/triggers/manual.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +63 -8
- package/dist/utils.js.map +1 -1
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +60 -11
- package/dist/validate-raw.js.map +1 -1
- package/package.json +2 -2
- package/scripts/preinstall.js +1 -1
- package/src/adapters/stdin-approval.ts +106 -106
- package/src/adapters/websocket-approval.ts +224 -220
- package/src/approval.ts +131 -125
- package/src/bootstrap.ts +37 -37
- package/src/completions/exit-code.ts +34 -30
- package/src/completions/file-exists.ts +66 -60
- package/src/completions/output-check.ts +86 -86
- package/src/config-ops.ts +307 -322
- package/src/dag.ts +234 -228
- package/src/drivers/claude-code.ts +250 -240
- package/src/engine.ts +1098 -928
- package/src/hooks.ts +187 -179
- package/src/logger.ts +182 -178
- package/src/middlewares/static-context.ts +45 -45
- package/src/pipeline-runner.ts +156 -150
- package/src/registry.ts +51 -23
- package/src/runner.ts +395 -397
- package/src/schema.test.ts +5 -1
- package/src/schema.ts +338 -298
- package/src/sdk.ts +91 -81
- package/src/triggers/file.ts +33 -14
- package/src/triggers/manual.ts +86 -81
- package/src/types.ts +18 -18
- package/src/utils.ts +202 -140
- package/src/validate-raw.ts +442 -389
package/src/pipeline-runner.ts
CHANGED
|
@@ -1,150 +1,156 @@
|
|
|
1
|
-
// ═══ PipelineRunner ═══
|
|
2
|
-
//
|
|
3
|
-
// Wraps runPipeline in a lifecycle object suited for multi-pipeline management
|
|
4
|
-
// in sidecar / Tauri IPC scenarios. Each instance controls one pipeline run.
|
|
5
|
-
//
|
|
6
|
-
// Typical sidecar usage:
|
|
7
|
-
//
|
|
8
|
-
// const runners = new Map<string, PipelineRunner>();
|
|
9
|
-
//
|
|
10
|
-
// const runner = new PipelineRunner(config, workDir);
|
|
11
|
-
// runner.subscribe(event => ipcEmit('pipeline_event', event));
|
|
12
|
-
// runner.start();
|
|
13
|
-
// runners.set(runner.instanceId, runner);
|
|
14
|
-
//
|
|
15
|
-
// // Later, from IPC:
|
|
16
|
-
// runners.get(id)?.abort();
|
|
17
|
-
|
|
18
|
-
import { runPipeline } from './engine';
|
|
19
|
-
import type { EngineResult, PipelineEvent, RunPipelineOptions } from './engine';
|
|
20
|
-
import type { PipelineConfig, TaskState } from './types';
|
|
21
|
-
import { generateRunId } from './utils';
|
|
22
|
-
|
|
23
|
-
export type { PipelineEvent, EngineResult };
|
|
24
|
-
|
|
25
|
-
export type PipelineRunnerStatus = 'idle' | 'running' | 'done' | 'aborted';
|
|
26
|
-
|
|
27
|
-
export class PipelineRunner {
|
|
28
|
-
/**
|
|
29
|
-
* Stable ID assigned before start() — safe to use as a Map key in the sidecar
|
|
30
|
-
* before the engine-assigned runId becomes available.
|
|
31
|
-
*/
|
|
32
|
-
readonly instanceId: string;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The runId generated by the engine. Available after the first 'pipeline_start'
|
|
36
|
-
* event fires (i.e. effectively immediately after start() is called).
|
|
37
|
-
* null until then.
|
|
38
|
-
*/
|
|
39
|
-
private _runId: string | null = null;
|
|
40
|
-
private _status: PipelineRunnerStatus = 'idle';
|
|
41
|
-
private _result: Promise<EngineResult> | null = null;
|
|
42
|
-
private _abortController = new AbortController();
|
|
43
|
-
private _handlers = new Set<(event: PipelineEvent) => void>();
|
|
44
|
-
private _states: ReadonlyMap<string, TaskState> | null = null;
|
|
45
|
-
private _statesMirror = new Map<string, TaskState>();
|
|
46
|
-
|
|
47
|
-
constructor(
|
|
48
|
-
private readonly config: PipelineConfig,
|
|
49
|
-
private readonly workDir: string,
|
|
50
|
-
private readonly opts: Omit<RunPipelineOptions, 'signal' | 'onEvent'> = {},
|
|
51
|
-
) {
|
|
52
|
-
this.instanceId = generateRunId();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
get runId(): string | null {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
if (event.type === '
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
1
|
+
// ═══ PipelineRunner ═══
|
|
2
|
+
//
|
|
3
|
+
// Wraps runPipeline in a lifecycle object suited for multi-pipeline management
|
|
4
|
+
// in sidecar / Tauri IPC scenarios. Each instance controls one pipeline run.
|
|
5
|
+
//
|
|
6
|
+
// Typical sidecar usage:
|
|
7
|
+
//
|
|
8
|
+
// const runners = new Map<string, PipelineRunner>();
|
|
9
|
+
//
|
|
10
|
+
// const runner = new PipelineRunner(config, workDir);
|
|
11
|
+
// runner.subscribe(event => ipcEmit('pipeline_event', event));
|
|
12
|
+
// runner.start();
|
|
13
|
+
// runners.set(runner.instanceId, runner);
|
|
14
|
+
//
|
|
15
|
+
// // Later, from IPC:
|
|
16
|
+
// runners.get(id)?.abort();
|
|
17
|
+
|
|
18
|
+
import { runPipeline } from './engine';
|
|
19
|
+
import type { EngineResult, PipelineEvent, RunPipelineOptions } from './engine';
|
|
20
|
+
import type { PipelineConfig, TaskState } from './types';
|
|
21
|
+
import { generateRunId } from './utils';
|
|
22
|
+
|
|
23
|
+
export type { PipelineEvent, EngineResult };
|
|
24
|
+
|
|
25
|
+
export type PipelineRunnerStatus = 'idle' | 'running' | 'done' | 'aborted';
|
|
26
|
+
|
|
27
|
+
export class PipelineRunner {
|
|
28
|
+
/**
|
|
29
|
+
* Stable ID assigned before start() — safe to use as a Map key in the sidecar
|
|
30
|
+
* before the engine-assigned runId becomes available.
|
|
31
|
+
*/
|
|
32
|
+
readonly instanceId: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The runId generated by the engine. Available after the first 'pipeline_start'
|
|
36
|
+
* event fires (i.e. effectively immediately after start() is called).
|
|
37
|
+
* null until then.
|
|
38
|
+
*/
|
|
39
|
+
private _runId: string | null = null;
|
|
40
|
+
private _status: PipelineRunnerStatus = 'idle';
|
|
41
|
+
private _result: Promise<EngineResult> | null = null;
|
|
42
|
+
private _abortController = new AbortController();
|
|
43
|
+
private _handlers = new Set<(event: PipelineEvent) => void>();
|
|
44
|
+
private _states: ReadonlyMap<string, TaskState> | null = null;
|
|
45
|
+
private _statesMirror = new Map<string, TaskState>();
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly config: PipelineConfig,
|
|
49
|
+
private readonly workDir: string,
|
|
50
|
+
private readonly opts: Omit<RunPipelineOptions, 'signal' | 'onEvent'> = {},
|
|
51
|
+
) {
|
|
52
|
+
this.instanceId = generateRunId();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get runId(): string | null {
|
|
56
|
+
return this._runId;
|
|
57
|
+
}
|
|
58
|
+
get status(): PipelineRunnerStatus {
|
|
59
|
+
return this._status;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Start the pipeline. Calling start() more than once returns the same Promise.
|
|
64
|
+
*/
|
|
65
|
+
start(): Promise<EngineResult> {
|
|
66
|
+
if (this._result) return this._result;
|
|
67
|
+
|
|
68
|
+
// Guard: if abort() was called before start(), the signal is already
|
|
69
|
+
// aborted. Create a fresh controller so the pipeline doesn't terminate
|
|
70
|
+
// immediately. If users truly want pre-abort semantics, they call
|
|
71
|
+
// abort() after start().
|
|
72
|
+
if (this._abortController.signal.aborted) {
|
|
73
|
+
this._abortController = new AbortController();
|
|
74
|
+
this._status = 'idle';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this._status = 'running';
|
|
78
|
+
this._result = runPipeline(this.config, this.workDir, {
|
|
79
|
+
...this.opts,
|
|
80
|
+
signal: this._abortController.signal,
|
|
81
|
+
onEvent: (event) => {
|
|
82
|
+
if (event.type === 'pipeline_start') {
|
|
83
|
+
this._runId = event.runId;
|
|
84
|
+
// Initialize the live mirror with the full initial state snapshot
|
|
85
|
+
for (const [id, state] of event.states) {
|
|
86
|
+
this._statesMirror.set(id, { ...state });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (event.type === 'task_status_change') {
|
|
90
|
+
// Keep the mirror up to date so getStates() works during the run
|
|
91
|
+
this._statesMirror.set(event.taskId, event.state);
|
|
92
|
+
}
|
|
93
|
+
if (event.type === 'pipeline_end') {
|
|
94
|
+
this._status = this._abortController.signal.aborted ? 'aborted' : 'done';
|
|
95
|
+
}
|
|
96
|
+
for (const h of this._handlers) h(event);
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
.then((result) => {
|
|
100
|
+
this._states = result.states;
|
|
101
|
+
if (this._status === 'running') this._status = 'done';
|
|
102
|
+
return result;
|
|
103
|
+
})
|
|
104
|
+
.catch((err) => {
|
|
105
|
+
this._status = 'aborted';
|
|
106
|
+
throw err;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return this._result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Cancel the running pipeline. Safe to call multiple times or before start().
|
|
114
|
+
*/
|
|
115
|
+
abort(reason?: string): void {
|
|
116
|
+
this._status = 'aborted';
|
|
117
|
+
this._abortController.abort(reason);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Live snapshot of task states. Available from the first pipeline_start event onward
|
|
122
|
+
* (i.e. as soon as start() is called) and remains accessible after the run completes.
|
|
123
|
+
* Returns null only if the pipeline has never started.
|
|
124
|
+
*/
|
|
125
|
+
getStates(): ReadonlyMap<string, TaskState> | null {
|
|
126
|
+
if (this._states) return snapshotStates(this._states);
|
|
127
|
+
if (this._statesMirror.size > 0) return snapshotStates(this._statesMirror);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Subscribe to pipeline/task events. Returns an unsubscribe function.
|
|
133
|
+
* Events are emitted synchronously in the engine's event loop, so keep
|
|
134
|
+
* handlers non-blocking (e.g. queue to IPC, do not await inside).
|
|
135
|
+
*/
|
|
136
|
+
subscribe(handler: (event: PipelineEvent) => void): () => void {
|
|
137
|
+
this._handlers.add(handler);
|
|
138
|
+
return () => this._handlers.delete(handler);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Deep-copy a states map so callers cannot mutate SDK internals. */
|
|
143
|
+
function snapshotStates(src: ReadonlyMap<string, TaskState>): ReadonlyMap<string, TaskState> {
|
|
144
|
+
const copy = new Map<string, TaskState>();
|
|
145
|
+
for (const [id, s] of src) {
|
|
146
|
+
copy.set(id, {
|
|
147
|
+
config: { ...s.config },
|
|
148
|
+
trackConfig: { ...s.trackConfig },
|
|
149
|
+
status: s.status,
|
|
150
|
+
result: s.result ? { ...s.result } : null,
|
|
151
|
+
startedAt: s.startedAt,
|
|
152
|
+
finishedAt: s.finishedAt,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return copy;
|
|
156
|
+
}
|
package/src/registry.ts
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
1
3
|
import type {
|
|
2
|
-
PluginCategory,
|
|
3
|
-
|
|
4
|
+
PluginCategory,
|
|
5
|
+
DriverPlugin,
|
|
6
|
+
TriggerPlugin,
|
|
7
|
+
CompletionPlugin,
|
|
8
|
+
MiddlewarePlugin,
|
|
9
|
+
PluginManifest,
|
|
4
10
|
} from './types';
|
|
5
11
|
|
|
6
12
|
type PluginType = DriverPlugin | TriggerPlugin | CompletionPlugin | MiddlewarePlugin;
|
|
7
13
|
|
|
8
14
|
const VALID_CATEGORIES: ReadonlySet<PluginCategory> = new Set([
|
|
9
|
-
'drivers',
|
|
15
|
+
'drivers',
|
|
16
|
+
'triggers',
|
|
17
|
+
'completions',
|
|
18
|
+
'middlewares',
|
|
10
19
|
]);
|
|
11
20
|
|
|
12
21
|
const registries = {
|
|
13
|
-
drivers:
|
|
14
|
-
triggers:
|
|
22
|
+
drivers: new Map<string, DriverPlugin>(),
|
|
23
|
+
triggers: new Map<string, TriggerPlugin>(),
|
|
15
24
|
completions: new Map<string, CompletionPlugin>(),
|
|
16
25
|
middlewares: new Map<string, MiddlewarePlugin>(),
|
|
17
26
|
};
|
|
@@ -47,7 +56,7 @@ function validateContract(category: PluginCategory, handler: unknown): void {
|
|
|
47
56
|
} catch (err) {
|
|
48
57
|
throw new Error(
|
|
49
58
|
`drivers plugin "${h.name}" capabilities accessor threw: ` +
|
|
50
|
-
|
|
59
|
+
(err instanceof Error ? err.message : String(err)),
|
|
51
60
|
);
|
|
52
61
|
}
|
|
53
62
|
if (!caps || typeof caps !== 'object') {
|
|
@@ -57,16 +66,14 @@ function validateContract(category: PluginCategory, handler: unknown): void {
|
|
|
57
66
|
for (const field of ['sessionResume', 'systemPrompt', 'outputFormat'] as const) {
|
|
58
67
|
if (typeof c[field] !== 'boolean') {
|
|
59
68
|
throw new Error(
|
|
60
|
-
`drivers plugin "${h.name}".capabilities.${field} must be a boolean (got ${typeof c[field]})
|
|
69
|
+
`drivers plugin "${h.name}".capabilities.${field} must be a boolean (got ${typeof c[field]})`,
|
|
61
70
|
);
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
// Optional methods, but if present must be functions.
|
|
65
74
|
for (const opt of ['parseResult', 'resolveModel', 'resolveTools'] as const) {
|
|
66
75
|
if (h[opt] !== undefined && typeof h[opt] !== 'function') {
|
|
67
|
-
throw new Error(
|
|
68
|
-
`drivers plugin "${h.name}".${opt} must be a function or undefined`
|
|
69
|
-
);
|
|
76
|
+
throw new Error(`drivers plugin "${h.name}".${opt} must be a function or undefined`);
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
break;
|
|
@@ -101,7 +108,9 @@ export type RegisterResult = 'registered' | 'replaced' | 'unchanged';
|
|
|
101
108
|
* minimum interface contract for the category.
|
|
102
109
|
*/
|
|
103
110
|
export function registerPlugin<T extends PluginType>(
|
|
104
|
-
category: PluginCategory,
|
|
111
|
+
category: PluginCategory,
|
|
112
|
+
type: string,
|
|
113
|
+
handler: T,
|
|
105
114
|
): RegisterResult {
|
|
106
115
|
if (!VALID_CATEGORIES.has(category)) {
|
|
107
116
|
throw new Error(`Unknown plugin category "${category}"`);
|
|
@@ -129,14 +138,12 @@ export function unregisterPlugin(category: PluginCategory, type: string): boolea
|
|
|
129
138
|
return registries[category].delete(type);
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
export function getHandler<T extends PluginType>(
|
|
133
|
-
category: PluginCategory, type: string,
|
|
134
|
-
): T {
|
|
141
|
+
export function getHandler<T extends PluginType>(category: PluginCategory, type: string): T {
|
|
135
142
|
const handler = registries[category].get(type);
|
|
136
143
|
if (!handler) {
|
|
137
144
|
throw new Error(
|
|
138
145
|
`${category} type "${type}" not registered.\n` +
|
|
139
|
-
|
|
146
|
+
`Install the plugin: bun add @tagma/${category.replace(/s$/, '')}-${type}`,
|
|
140
147
|
);
|
|
141
148
|
}
|
|
142
149
|
return handler as T;
|
|
@@ -181,7 +188,7 @@ export function readPluginManifest(pkgJson: unknown): PluginManifest | null {
|
|
|
181
188
|
const type = m.type;
|
|
182
189
|
if (typeof category !== 'string' || !VALID_CATEGORIES.has(category as PluginCategory)) {
|
|
183
190
|
throw new Error(
|
|
184
|
-
`tagmaPlugin.category must be one of ${[...VALID_CATEGORIES].join(', ')}, got ${JSON.stringify(category)}
|
|
191
|
+
`tagmaPlugin.category must be one of ${[...VALID_CATEGORIES].join(', ')}, got ${JSON.stringify(category)}`,
|
|
185
192
|
);
|
|
186
193
|
}
|
|
187
194
|
if (typeof type !== 'string' || type.length === 0) {
|
|
@@ -190,20 +197,41 @@ export function readPluginManifest(pkgJson: unknown): PluginManifest | null {
|
|
|
190
197
|
return { category: category as PluginCategory, type };
|
|
191
198
|
}
|
|
192
199
|
|
|
193
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Load and register a list of plugin packages.
|
|
202
|
+
*
|
|
203
|
+
* @param pluginNames - Validated npm package names to load.
|
|
204
|
+
* @param resolveFrom - Optional absolute path to resolve plugins from (e.g. the
|
|
205
|
+
* workspace's working directory). When omitted, the default ESM resolution
|
|
206
|
+
* uses the SDK's own `node_modules`, which will fail for plugins installed
|
|
207
|
+
* only in the user's workspace. CLI callers should pass `process.cwd()` or
|
|
208
|
+
* the workspace root so that workspace-local plugins resolve correctly.
|
|
209
|
+
*/
|
|
210
|
+
export async function loadPlugins(
|
|
211
|
+
pluginNames: readonly string[],
|
|
212
|
+
resolveFrom?: string,
|
|
213
|
+
): Promise<void> {
|
|
194
214
|
for (const name of pluginNames) {
|
|
195
215
|
if (!isValidPluginName(name)) {
|
|
196
216
|
throw new Error(
|
|
197
217
|
`Plugin "${name}" rejected: plugin names must be scoped npm packages ` +
|
|
198
|
-
|
|
199
|
-
|
|
218
|
+
`(e.g. @tagma/trigger-xyz) or tagma-plugin-* packages. ` +
|
|
219
|
+
`Relative/absolute paths are not allowed.`,
|
|
200
220
|
);
|
|
201
221
|
}
|
|
202
|
-
|
|
222
|
+
let moduleUrl: string = name;
|
|
223
|
+
if (resolveFrom) {
|
|
224
|
+
// Resolve the package entry point relative to the caller's directory so
|
|
225
|
+
// plugins installed in the workspace's node_modules are found even when
|
|
226
|
+
// the SDK itself lives elsewhere (e.g. a global install or a monorepo
|
|
227
|
+
// sibling package).
|
|
228
|
+
const req = createRequire(resolveFrom.endsWith('/') ? resolveFrom : resolveFrom + '/');
|
|
229
|
+
const resolved = req.resolve(name);
|
|
230
|
+
moduleUrl = pathToFileURL(resolved).href;
|
|
231
|
+
}
|
|
232
|
+
const mod = await import(moduleUrl);
|
|
203
233
|
if (!mod.pluginCategory || !mod.pluginType || !mod.default) {
|
|
204
|
-
throw new Error(
|
|
205
|
-
`Plugin "${name}" must export pluginCategory, pluginType, and default`
|
|
206
|
-
);
|
|
234
|
+
throw new Error(`Plugin "${name}" must export pluginCategory, pluginType, and default`);
|
|
207
235
|
}
|
|
208
236
|
registerPlugin(mod.pluginCategory, mod.pluginType, mod.default);
|
|
209
237
|
}
|