@tagma/sdk 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/README.md +85 -57
  2. package/dist/approval.d.ts +2 -12
  3. package/dist/approval.d.ts.map +1 -1
  4. package/dist/approval.js +1 -90
  5. package/dist/approval.js.map +1 -1
  6. package/dist/bootstrap.d.ts +1 -1
  7. package/dist/bootstrap.d.ts.map +1 -1
  8. package/dist/completions/file-exists.js +1 -1
  9. package/dist/completions/file-exists.js.map +1 -1
  10. package/dist/completions/output-check.d.ts.map +1 -1
  11. package/dist/completions/output-check.js +17 -4
  12. package/dist/completions/output-check.js.map +1 -1
  13. package/dist/config.d.ts +4 -4
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +2 -2
  16. package/dist/config.js.map +1 -1
  17. package/dist/dataflow.d.ts +3 -0
  18. package/dist/dataflow.d.ts.map +1 -0
  19. package/dist/dataflow.js +2 -0
  20. package/dist/dataflow.js.map +1 -0
  21. package/dist/drivers/opencode.d.ts.map +1 -1
  22. package/dist/drivers/opencode.js +23 -71
  23. package/dist/drivers/opencode.js.map +1 -1
  24. package/dist/engine.d.ts +5 -56
  25. package/dist/engine.d.ts.map +1 -1
  26. package/dist/engine.js +7 -297
  27. package/dist/engine.js.map +1 -1
  28. package/dist/index.d.ts +4 -6
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/logger.d.ts +2 -60
  33. package/dist/logger.d.ts.map +1 -1
  34. package/dist/logger.js +1 -153
  35. package/dist/logger.js.map +1 -1
  36. package/dist/middlewares/static-context.d.ts.map +1 -1
  37. package/dist/middlewares/static-context.js +1 -2
  38. package/dist/middlewares/static-context.js.map +1 -1
  39. package/dist/pipeline-runner.d.ts.map +1 -1
  40. package/dist/pipeline-runner.js +2 -2
  41. package/dist/pipeline-runner.js.map +1 -1
  42. package/dist/plugins.d.ts +2 -2
  43. package/dist/plugins.d.ts.map +1 -1
  44. package/dist/plugins.js +1 -1
  45. package/dist/plugins.js.map +1 -1
  46. package/dist/runner.d.ts +1 -35
  47. package/dist/runner.d.ts.map +1 -1
  48. package/dist/runner.js +1 -610
  49. package/dist/runner.js.map +1 -1
  50. package/dist/runtime/adapters/stdin-approval.d.ts +2 -0
  51. package/dist/runtime/adapters/stdin-approval.d.ts.map +1 -0
  52. package/dist/runtime/adapters/stdin-approval.js +2 -0
  53. package/dist/runtime/adapters/stdin-approval.js.map +1 -0
  54. package/dist/runtime/adapters/websocket-approval.d.ts +2 -0
  55. package/dist/runtime/adapters/websocket-approval.d.ts.map +1 -0
  56. package/dist/runtime/adapters/websocket-approval.js +2 -0
  57. package/dist/runtime/adapters/websocket-approval.js.map +1 -0
  58. package/dist/runtime/bun-process-runner.d.ts +2 -0
  59. package/dist/runtime/bun-process-runner.d.ts.map +1 -0
  60. package/dist/runtime/bun-process-runner.js +2 -0
  61. package/dist/runtime/bun-process-runner.js.map +1 -0
  62. package/dist/runtime.d.ts +2 -8
  63. package/dist/runtime.d.ts.map +1 -1
  64. package/dist/runtime.js +1 -7
  65. package/dist/runtime.js.map +1 -1
  66. package/dist/schema.d.ts.map +1 -1
  67. package/dist/schema.js +3 -4
  68. package/dist/schema.js.map +1 -1
  69. package/dist/tagma.d.ts +3 -4
  70. package/dist/tagma.d.ts.map +1 -1
  71. package/dist/tagma.js +2 -3
  72. package/dist/tagma.js.map +1 -1
  73. package/dist/triggers/file.d.ts.map +1 -1
  74. package/dist/triggers/file.js +74 -108
  75. package/dist/triggers/file.js.map +1 -1
  76. package/dist/triggers/manual.d.ts.map +1 -1
  77. package/dist/triggers/manual.js +1 -2
  78. package/dist/triggers/manual.js.map +1 -1
  79. package/dist/types.d.ts +1 -2
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/types.js +1 -12
  82. package/dist/types.js.map +1 -1
  83. package/dist/utils-api.d.ts +1 -1
  84. package/dist/utils-api.d.ts.map +1 -1
  85. package/dist/utils-api.js +1 -1
  86. package/dist/utils-api.js.map +1 -1
  87. package/dist/validate-raw.d.ts.map +1 -1
  88. package/dist/validate-raw.js +5 -12
  89. package/dist/validate-raw.js.map +1 -1
  90. package/package.json +20 -22
  91. package/dist/adapters/stdin-approval.d.ts +0 -6
  92. package/dist/adapters/stdin-approval.d.ts.map +0 -1
  93. package/dist/adapters/stdin-approval.js +0 -90
  94. package/dist/adapters/stdin-approval.js.map +0 -1
  95. package/dist/adapters/websocket-approval.d.ts +0 -28
  96. package/dist/adapters/websocket-approval.d.ts.map +0 -1
  97. package/dist/adapters/websocket-approval.js +0 -147
  98. package/dist/adapters/websocket-approval.js.map +0 -1
  99. package/dist/core/dataflow.d.ts +0 -23
  100. package/dist/core/dataflow.d.ts.map +0 -1
  101. package/dist/core/dataflow.js +0 -99
  102. package/dist/core/dataflow.js.map +0 -1
  103. package/dist/core/log-prune.d.ts +0 -16
  104. package/dist/core/log-prune.d.ts.map +0 -1
  105. package/dist/core/log-prune.js +0 -34
  106. package/dist/core/log-prune.js.map +0 -1
  107. package/dist/core/preflight.d.ts +0 -13
  108. package/dist/core/preflight.d.ts.map +0 -1
  109. package/dist/core/preflight.js +0 -61
  110. package/dist/core/preflight.js.map +0 -1
  111. package/dist/core/run-context.d.ts +0 -55
  112. package/dist/core/run-context.d.ts.map +0 -1
  113. package/dist/core/run-context.js +0 -158
  114. package/dist/core/run-context.js.map +0 -1
  115. package/dist/core/run-state.d.ts +0 -25
  116. package/dist/core/run-state.d.ts.map +0 -1
  117. package/dist/core/run-state.js +0 -93
  118. package/dist/core/run-state.js.map +0 -1
  119. package/dist/core/scheduler.d.ts +0 -13
  120. package/dist/core/scheduler.d.ts.map +0 -1
  121. package/dist/core/scheduler.js +0 -35
  122. package/dist/core/scheduler.js.map +0 -1
  123. package/dist/core/task-executor.d.ts +0 -13
  124. package/dist/core/task-executor.d.ts.map +0 -1
  125. package/dist/core/task-executor.js +0 -601
  126. package/dist/core/task-executor.js.map +0 -1
  127. package/dist/core/trigger-errors.d.ts +0 -9
  128. package/dist/core/trigger-errors.d.ts.map +0 -1
  129. package/dist/core/trigger-errors.js +0 -15
  130. package/dist/core/trigger-errors.js.map +0 -1
  131. package/dist/dag.d.ts +0 -45
  132. package/dist/dag.d.ts.map +0 -1
  133. package/dist/dag.js +0 -177
  134. package/dist/dag.js.map +0 -1
  135. package/dist/hooks.d.ts +0 -73
  136. package/dist/hooks.d.ts.map +0 -1
  137. package/dist/hooks.js +0 -106
  138. package/dist/hooks.js.map +0 -1
  139. package/dist/pipeline-definition.d.ts +0 -3
  140. package/dist/pipeline-definition.d.ts.map +0 -1
  141. package/dist/pipeline-definition.js +0 -4
  142. package/dist/pipeline-definition.js.map +0 -1
  143. package/dist/ports.d.ts +0 -196
  144. package/dist/ports.d.ts.map +0 -1
  145. package/dist/ports.js +0 -688
  146. package/dist/ports.js.map +0 -1
  147. package/dist/prompt-doc.d.ts +0 -70
  148. package/dist/prompt-doc.d.ts.map +0 -1
  149. package/dist/prompt-doc.js +0 -154
  150. package/dist/prompt-doc.js.map +0 -1
  151. package/dist/registry.d.ts +0 -67
  152. package/dist/registry.d.ts.map +0 -1
  153. package/dist/registry.js +0 -293
  154. package/dist/registry.js.map +0 -1
  155. package/dist/task-ref.d.ts +0 -55
  156. package/dist/task-ref.d.ts.map +0 -1
  157. package/dist/task-ref.js +0 -103
  158. package/dist/task-ref.js.map +0 -1
  159. package/dist/utils.d.ts +0 -13
  160. package/dist/utils.d.ts.map +0 -1
  161. package/dist/utils.js +0 -177
  162. package/dist/utils.js.map +0 -1
  163. package/src/adapters/stdin-approval.ts +0 -106
  164. package/src/adapters/websocket-approval.ts +0 -224
  165. package/src/approval.ts +0 -131
  166. package/src/bootstrap.ts +0 -55
  167. package/src/completions/exit-code.ts +0 -34
  168. package/src/completions/file-exists.ts +0 -66
  169. package/src/completions/output-check.test.ts +0 -50
  170. package/src/completions/output-check.ts +0 -92
  171. package/src/config-ops.test.ts +0 -70
  172. package/src/config-ops.ts +0 -328
  173. package/src/config.ts +0 -26
  174. package/src/core/dataflow.test.ts +0 -166
  175. package/src/core/dataflow.ts +0 -161
  176. package/src/core/log-prune.test.ts +0 -58
  177. package/src/core/log-prune.ts +0 -43
  178. package/src/core/preflight.test.ts +0 -49
  179. package/src/core/preflight.ts +0 -89
  180. package/src/core/run-context.test.ts +0 -256
  181. package/src/core/run-context.ts +0 -211
  182. package/src/core/run-state.test.ts +0 -98
  183. package/src/core/run-state.ts +0 -122
  184. package/src/core/scheduler.test.ts +0 -83
  185. package/src/core/scheduler.ts +0 -42
  186. package/src/core/task-executor.ts +0 -743
  187. package/src/core/trigger-errors.ts +0 -15
  188. package/src/dag.test.ts +0 -56
  189. package/src/dag.ts +0 -245
  190. package/src/drivers/opencode.ts +0 -410
  191. package/src/engine-ports-mixed.test.ts +0 -156
  192. package/src/engine-ports.test.ts +0 -166
  193. package/src/engine-task-type.test.ts +0 -56
  194. package/src/engine.ts +0 -458
  195. package/src/hooks.ts +0 -193
  196. package/src/index.ts +0 -33
  197. package/src/logger.ts +0 -182
  198. package/src/middlewares/static-context.ts +0 -49
  199. package/src/pipeline-definition.ts +0 -5
  200. package/src/pipeline-runner.test.ts +0 -91
  201. package/src/pipeline-runner.ts +0 -194
  202. package/src/plugin-registry.test.ts +0 -382
  203. package/src/plugins.ts +0 -21
  204. package/src/ports.test.ts +0 -678
  205. package/src/ports.ts +0 -925
  206. package/src/prompt-doc.test.ts +0 -174
  207. package/src/prompt-doc.ts +0 -169
  208. package/src/registry.ts +0 -353
  209. package/src/runner.test.ts +0 -142
  210. package/src/runner.ts +0 -666
  211. package/src/runtime.ts +0 -20
  212. package/src/schema-ports.test.ts +0 -172
  213. package/src/schema.test.ts +0 -213
  214. package/src/schema.ts +0 -379
  215. package/src/tagma.test.ts +0 -155
  216. package/src/tagma.ts +0 -62
  217. package/src/task-ref.test.ts +0 -401
  218. package/src/task-ref.ts +0 -121
  219. package/src/triggers/file.ts +0 -164
  220. package/src/triggers/manual.ts +0 -86
  221. package/src/types.ts +0 -18
  222. package/src/utils-api.ts +0 -8
  223. package/src/utils.test.ts +0 -28
  224. package/src/utils.ts +0 -203
  225. package/src/validate-raw-plugin-types.test.ts +0 -60
  226. package/src/validate-raw-ports.test.ts +0 -136
  227. package/src/validate-raw.ts +0 -852
  228. package/src/yaml-compiler.test.ts +0 -108
  229. package/src/yaml-compiler.ts +0 -110
  230. package/src/yaml.ts +0 -11
@@ -1,194 +0,0 @@
1
- // ═══ PipelineRunner ═══
2
- //
3
- // Wraps runPipeline in a lifecycle object suited for multi-pipeline
4
- // management in sidecar / Tauri IPC scenarios. Each instance controls
5
- // one pipeline run.
6
- //
7
- // The runner forwards wire-shape `RunEventPayload` values to its
8
- // subscribers — identical to what the editor server broadcasts over SSE —
9
- // so sidecar hosts don't need to know anything about the engine's
10
- // internal TaskState.
11
- //
12
- // Typical sidecar usage:
13
- //
14
- // const runners = new Map<string, PipelineRunner>();
15
- //
16
- // const runner = new PipelineRunner(config, workDir, { registry });
17
- // runner.subscribe(event => ipcEmit('run_event', event));
18
- // runner.start();
19
- // runners.set(runner.instanceId, runner);
20
- //
21
- // // Later, from IPC:
22
- // runners.get(id)?.abort();
23
-
24
- import { runPipeline } from './engine';
25
- import type { EngineResult, RunPipelineOptions } from './engine';
26
- import { TASK_LOG_CAP, type PipelineConfig, type RunEventPayload, type RunTaskState } from './types';
27
- import { generateRunId } from './utils';
28
-
29
- export type { EngineResult };
30
-
31
- export type PipelineRunnerStatus = 'idle' | 'running' | 'done' | 'aborted';
32
- export type PipelineRunnerOptions = Omit<RunPipelineOptions, 'signal' | 'onEvent'>;
33
-
34
- export class PipelineRunner {
35
- /**
36
- * Stable ID assigned before start() — safe to use as a Map key before
37
- * the engine-assigned runId becomes available.
38
- */
39
- readonly instanceId: string;
40
-
41
- /**
42
- * The runId generated by the engine. Set when the first `run_start`
43
- * event arrives on the forwarded event stream. null until then.
44
- */
45
- private _runId: string | null = null;
46
- private _status: PipelineRunnerStatus = 'idle';
47
- private _result: Promise<EngineResult> | null = null;
48
- private _abortController = new AbortController();
49
- private _handlers = new Set<(event: RunEventPayload) => void>();
50
- /**
51
- * Wire-shape task mirror, kept in sync with `run_start` / `task_update`
52
- * events. Exposed through `getTasks()`. Hosts see the same wire
53
- * projection the editor client sees, so there is exactly one task-state
54
- * vocabulary across IPC boundaries.
55
- */
56
- private _tasks = new Map<string, RunTaskState>();
57
-
58
- constructor(
59
- private readonly config: PipelineConfig,
60
- private readonly workDir: string,
61
- private readonly opts: PipelineRunnerOptions,
62
- ) {
63
- this.instanceId = generateRunId();
64
- }
65
-
66
- get runId(): string | null {
67
- return this._runId;
68
- }
69
- get status(): PipelineRunnerStatus {
70
- return this._status;
71
- }
72
-
73
- /**
74
- * Start the pipeline. Calling start() more than once returns the same Promise.
75
- */
76
- start(): Promise<EngineResult> {
77
- if (this._result) return this._result;
78
-
79
- // Guard: if abort() was called before start(), the signal is already
80
- // aborted. Create a fresh controller so the pipeline doesn't terminate
81
- // immediately. If users truly want pre-abort semantics, they call
82
- // abort() after start().
83
- if (this._abortController.signal.aborted) {
84
- this._abortController = new AbortController();
85
- this._status = 'idle';
86
- }
87
-
88
- this._status = 'running';
89
- this._result = runPipeline(this.config, this.workDir, {
90
- ...this.opts,
91
- signal: this._abortController.signal,
92
- onEvent: (event) => {
93
- this._applyEvent(event);
94
- for (const h of this._handlers) h(event);
95
- },
96
- })
97
- .then((result) => {
98
- if (this._status === 'running') this._status = 'done';
99
- return result;
100
- })
101
- .catch((err) => {
102
- this._status = 'aborted';
103
- throw err;
104
- });
105
-
106
- return this._result;
107
- }
108
-
109
- private _applyEvent(event: RunEventPayload): void {
110
- switch (event.type) {
111
- case 'run_start':
112
- this._runId = event.runId;
113
- this._tasks.clear();
114
- for (const t of event.tasks) this._tasks.set(t.taskId, { ...t });
115
- return;
116
- case 'task_update': {
117
- const prev = this._tasks.get(event.taskId);
118
- if (!prev) return;
119
- const pick = <T>(incoming: T | undefined, previous: T): T =>
120
- incoming !== undefined ? incoming : previous;
121
- this._tasks.set(event.taskId, {
122
- ...prev,
123
- status: event.status,
124
- startedAt: pick(event.startedAt, prev.startedAt),
125
- finishedAt: pick(event.finishedAt, prev.finishedAt),
126
- durationMs: pick(event.durationMs, prev.durationMs),
127
- exitCode: pick(event.exitCode, prev.exitCode),
128
- stdout: pick(event.stdout, prev.stdout),
129
- stderr: pick(event.stderr, prev.stderr),
130
- stdoutPath: pick(event.stdoutPath, prev.stdoutPath),
131
- stderrPath: pick(event.stderrPath, prev.stderrPath),
132
- stdoutBytes: pick(event.stdoutBytes, prev.stdoutBytes),
133
- stderrBytes: pick(event.stderrBytes, prev.stderrBytes),
134
- sessionId: pick(event.sessionId, prev.sessionId),
135
- normalizedOutput: pick(event.normalizedOutput, prev.normalizedOutput),
136
- outputs: pick(event.outputs, prev.outputs),
137
- inputs: pick(event.inputs, prev.inputs),
138
- resolvedDriver: pick(event.resolvedDriver, prev.resolvedDriver),
139
- resolvedModel: pick(event.resolvedModel, prev.resolvedModel),
140
- resolvedPermissions: pick(event.resolvedPermissions, prev.resolvedPermissions),
141
- });
142
- return;
143
- }
144
- case 'task_log': {
145
- if (event.taskId === null) return;
146
- const prev = this._tasks.get(event.taskId);
147
- if (!prev) return;
148
- const logs = [
149
- ...prev.logs,
150
- { level: event.level, timestamp: event.timestamp, text: event.text },
151
- ];
152
- this._tasks.set(event.taskId, {
153
- ...prev,
154
- logs: logs.slice(-TASK_LOG_CAP),
155
- totalLogCount: prev.totalLogCount + 1,
156
- });
157
- return;
158
- }
159
- case 'run_end':
160
- this._status = this._abortController.signal.aborted ? 'aborted' : 'done';
161
- return;
162
- default:
163
- return;
164
- }
165
- }
166
-
167
- /**
168
- * Cancel the running pipeline. Safe to call multiple times or before start().
169
- */
170
- abort(reason?: string): void {
171
- this._status = 'aborted';
172
- this._abortController.abort(reason);
173
- }
174
-
175
- /**
176
- * Live snapshot of wire-shape task states. Populated from the first
177
- * `run_start` event onward. Returns an empty map before the run starts.
178
- */
179
- getTasks(): ReadonlyMap<string, RunTaskState> {
180
- const copy = new Map<string, RunTaskState>();
181
- for (const [id, t] of this._tasks) copy.set(id, { ...t });
182
- return copy;
183
- }
184
-
185
- /**
186
- * Subscribe to run events. Returns an unsubscribe function. Events are
187
- * emitted synchronously in the engine's event loop, so keep handlers
188
- * non-blocking (e.g. queue to IPC, do not await inside).
189
- */
190
- subscribe(handler: (event: RunEventPayload) => void): () => void {
191
- this._handlers.add(handler);
192
- return () => this._handlers.delete(handler);
193
- }
194
- }
@@ -1,382 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { PluginRegistry } from './registry';
3
- import { bootstrapBuiltins } from './bootstrap';
4
- import { runPipeline } from './engine';
5
- import type { DriverPlugin, TriggerPlugin, PipelineConfig } from './types';
6
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
7
- import { tmpdir } from 'node:os';
8
- import { join } from 'node:path';
9
- import type { TagmaPlugin } from './types';
10
-
11
- function makeDriver(name: string, marker: string[]): DriverPlugin {
12
- return {
13
- name,
14
- capabilities: { sessionResume: false, systemPrompt: false, outputFormat: false },
15
- async buildCommand() {
16
- marker.push(`buildCommand:${name}`);
17
- return { args: ['echo', 'noop'] };
18
- },
19
- };
20
- }
21
-
22
- function makeTrigger(name: string, marker: string[]): TriggerPlugin {
23
- return {
24
- name,
25
- async watch() {
26
- marker.push(`watch:${name}`);
27
- },
28
- };
29
- }
30
-
31
- describe('PluginRegistry — instance isolation', () => {
32
- test('two registries do not share drivers registered under the same type', () => {
33
- const regA = new PluginRegistry();
34
- const regB = new PluginRegistry();
35
- const markerA: string[] = [];
36
- const markerB: string[] = [];
37
-
38
- regA.registerPlugin('drivers', 'mock', makeDriver('mockA', markerA));
39
- regB.registerPlugin('drivers', 'mock', makeDriver('mockB', markerB));
40
-
41
- expect(regA.getHandler<DriverPlugin>('drivers', 'mock').name).toBe('mockA');
42
- expect(regB.getHandler<DriverPlugin>('drivers', 'mock').name).toBe('mockB');
43
-
44
- expect(regA.hasHandler('drivers', 'mock')).toBe(true);
45
- expect(regB.hasHandler('drivers', 'mock')).toBe(true);
46
- expect(regA.hasHandler('triggers', 'mock')).toBe(false);
47
- });
48
-
49
- test('unregistering in one registry does not affect the other', () => {
50
- const regA = new PluginRegistry();
51
- const regB = new PluginRegistry();
52
- regA.registerPlugin('drivers', 'mock', makeDriver('mockA', []));
53
- regB.registerPlugin('drivers', 'mock', makeDriver('mockB', []));
54
-
55
- expect(regA.unregisterPlugin('drivers', 'mock')).toBe(true);
56
- expect(regA.hasHandler('drivers', 'mock')).toBe(false);
57
- expect(regB.hasHandler('drivers', 'mock')).toBe(true);
58
- });
59
-
60
- test('listRegistered is scoped per instance', () => {
61
- const regA = new PluginRegistry();
62
- const regB = new PluginRegistry();
63
- regA.registerPlugin('triggers', 'a-only', makeTrigger('a-only', []));
64
- regB.registerPlugin('triggers', 'b-only', makeTrigger('b-only', []));
65
-
66
- expect(regA.listRegistered('triggers')).toEqual(['a-only']);
67
- expect(regB.listRegistered('triggers')).toEqual(['b-only']);
68
- });
69
-
70
- test('registering the same instance twice returns unchanged', () => {
71
- const reg = new PluginRegistry();
72
- const driver = makeDriver('same', []);
73
- expect(reg.registerPlugin('drivers', 'mock', driver)).toBe('registered');
74
- expect(reg.registerPlugin('drivers', 'mock', driver)).toBe('unchanged');
75
- });
76
-
77
- test('replacing with a different handler returns replaced', () => {
78
- const reg = new PluginRegistry();
79
- expect(reg.registerPlugin('drivers', 'mock', makeDriver('one', []))).toBe('registered');
80
- expect(reg.registerPlugin('drivers', 'mock', makeDriver('two', []))).toBe('replaced');
81
- expect(reg.getHandler<DriverPlugin>('drivers', 'mock').name).toBe('two');
82
- });
83
-
84
- test('bootstrapBuiltins(target) populates a specific instance', () => {
85
- const fresh = new PluginRegistry();
86
- expect(fresh.hasHandler('drivers', 'opencode')).toBe(false);
87
-
88
- bootstrapBuiltins(fresh);
89
-
90
- expect(fresh.hasHandler('drivers', 'opencode')).toBe(true);
91
- expect(fresh.hasHandler('triggers', 'file')).toBe(true);
92
- expect(fresh.hasHandler('triggers', 'manual')).toBe(true);
93
- expect(fresh.hasHandler('completions', 'exit_code')).toBe(true);
94
- expect(fresh.hasHandler('middlewares', 'static_context')).toBe(true);
95
-
96
- // Default registry's state is independent of `fresh` — if the default
97
- // happens to have opencode (because another test bootstrapped it), that
98
- // is fine; the guarantee is that `fresh.unregister` does not leak.
99
- fresh.unregisterPlugin('drivers', 'opencode');
100
- expect(fresh.hasHandler('drivers', 'opencode')).toBe(false);
101
- });
102
- });
103
-
104
- describe('PluginRegistry — capability plugins', () => {
105
- test('registerTagmaPlugin registers multiple capabilities from one package', () => {
106
- const reg = new PluginRegistry();
107
- const driver = makeDriver('cap-driver', []);
108
- const trigger = makeTrigger('cap-trigger', []);
109
- const plugin: TagmaPlugin = {
110
- name: 'tagma-plugin-multi',
111
- capabilities: {
112
- drivers: { cap_driver: driver },
113
- triggers: { cap_trigger: trigger },
114
- },
115
- };
116
-
117
- expect(reg.registerTagmaPlugin(plugin)).toEqual([
118
- { category: 'drivers', type: 'cap_driver', result: 'registered' },
119
- { category: 'triggers', type: 'cap_trigger', result: 'registered' },
120
- ]);
121
- expect(reg.getHandler<DriverPlugin>('drivers', 'cap_driver')).toBe(driver);
122
- expect(reg.getHandler<TriggerPlugin>('triggers', 'cap_trigger')).toBe(trigger);
123
- });
124
-
125
- test('registerTagmaPlugin keeps replacement warnings from the registry path', () => {
126
- const reg = new PluginRegistry();
127
- const originalWarn = console.warn;
128
- const warnings: string[] = [];
129
- console.warn = (message?: unknown) => {
130
- warnings.push(String(message));
131
- };
132
- try {
133
- reg.registerPlugin('drivers', 'mock', makeDriver('first', []));
134
- const result = reg.registerTagmaPlugin({
135
- name: 'tagma-plugin-replacement',
136
- capabilities: {
137
- drivers: { mock: makeDriver('second', []) },
138
- },
139
- });
140
-
141
- expect(result).toEqual([{ category: 'drivers', type: 'mock', result: 'replaced' }]);
142
- expect(warnings).toContain(
143
- '[tagma-sdk] registerPlugin: replaced existing drivers/mock - check for duplicate plugin packages claiming the same type.',
144
- );
145
- } finally {
146
- console.warn = originalWarn;
147
- }
148
- });
149
-
150
- test('loadPlugins accepts capability plugin default exports', async () => {
151
- const dir = mkdtempSync(join(tmpdir(), 'tagma-capability-plugin-'));
152
- const pluginDir = join(dir, 'node_modules', 'tagma-plugin-capability');
153
- mkdirSync(pluginDir, { recursive: true });
154
- writeFileSync(
155
- join(pluginDir, 'package.json'),
156
- JSON.stringify({ name: 'tagma-plugin-capability', version: '1.0.0', type: 'module', main: './index.js' }),
157
- 'utf-8',
158
- );
159
- writeFileSync(
160
- join(pluginDir, 'index.js'),
161
- [
162
- 'const driver = {',
163
- " name: 'cap-driver',",
164
- ' capabilities: { sessionResume: false, systemPrompt: false, outputFormat: false },',
165
- " async buildCommand() { return { args: ['echo', 'cap'] }; },",
166
- '};',
167
- 'const trigger = {',
168
- " name: 'cap-trigger',",
169
- ' async watch() {}',
170
- '};',
171
- 'export default {',
172
- " name: 'tagma-plugin-capability',",
173
- ' capabilities: {',
174
- ' drivers: { cap_driver: driver },',
175
- ' triggers: { cap_trigger: trigger },',
176
- ' },',
177
- '};',
178
- '',
179
- ].join('\n'),
180
- 'utf-8',
181
- );
182
-
183
- try {
184
- const reg = new PluginRegistry();
185
- await reg.loadPlugins(['tagma-plugin-capability'], dir);
186
- expect(reg.hasHandler('drivers', 'cap_driver')).toBe(true);
187
- expect(reg.hasHandler('triggers', 'cap_trigger')).toBe(true);
188
- } finally {
189
- rmSync(dir, { recursive: true, force: true });
190
- }
191
- });
192
-
193
- test('loadPlugins rejects legacy plugin module exports', async () => {
194
- const dir = mkdtempSync(join(tmpdir(), 'tagma-legacy-plugin-'));
195
- const pluginDir = join(dir, 'node_modules', 'tagma-plugin-legacy');
196
- mkdirSync(pluginDir, { recursive: true });
197
- writeFileSync(
198
- join(pluginDir, 'package.json'),
199
- JSON.stringify({ name: 'tagma-plugin-legacy', version: '1.0.0', type: 'module', main: './index.js' }),
200
- 'utf-8',
201
- );
202
- writeFileSync(
203
- join(pluginDir, 'index.js'),
204
- [
205
- "export const pluginCategory = 'drivers';",
206
- "export const pluginType = 'legacy';",
207
- 'export default {',
208
- " name: 'legacy',",
209
- ' capabilities: { sessionResume: false, systemPrompt: false, outputFormat: false },',
210
- " async buildCommand() { return { args: ['echo', 'legacy'] }; },",
211
- '};',
212
- '',
213
- ].join('\n'),
214
- 'utf-8',
215
- );
216
-
217
- try {
218
- const reg = new PluginRegistry();
219
- await expect(reg.loadPlugins(['tagma-plugin-legacy'], dir)).rejects.toThrow(
220
- /must default-export a TagmaPlugin/,
221
- );
222
- } finally {
223
- rmSync(dir, { recursive: true, force: true });
224
- }
225
- });
226
- });
227
-
228
- describe('PluginRegistry — validation', () => {
229
- test('rejects unknown category', () => {
230
- const reg = new PluginRegistry();
231
- expect(() =>
232
- reg.registerPlugin(
233
- 'nope' as 'drivers',
234
- 'x',
235
- makeDriver('x', []),
236
- ),
237
- ).toThrow(/Unknown plugin category/);
238
- });
239
-
240
- test('rejects driver missing buildCommand', () => {
241
- const reg = new PluginRegistry();
242
- expect(() =>
243
- reg.registerPlugin(
244
- 'drivers',
245
- 'broken',
246
- // deliberately bad: no buildCommand
247
- { name: 'broken', capabilities: { sessionResume: false, systemPrompt: false, outputFormat: false } } as unknown as DriverPlugin,
248
- ),
249
- ).toThrow(/must export buildCommand/);
250
- });
251
-
252
- test('rejects handler with missing name', () => {
253
- const reg = new PluginRegistry();
254
- expect(() =>
255
- reg.registerPlugin(
256
- 'drivers',
257
- 'x',
258
- // deliberately bad: no name
259
- { capabilities: { sessionResume: false, systemPrompt: false, outputFormat: false }, buildCommand: async () => ({ args: [] }) } as unknown as DriverPlugin,
260
- ),
261
- ).toThrow(/non-empty "name"/);
262
- });
263
-
264
- test('rejects plugin type identifiers that are not YAML-safe ids', () => {
265
- const reg = new PluginRegistry();
266
- expect(() =>
267
- reg.registerPlugin(
268
- 'drivers',
269
- '../evil',
270
- makeDriver('evil', []),
271
- ),
272
- ).toThrow(/Plugin type .* must match/);
273
- });
274
-
275
- test('middleware install hint uses singular middleware package name', () => {
276
- const reg = new PluginRegistry();
277
- expect(() => reg.getHandler('middlewares', 'audit')).toThrow(
278
- /bun add @tagma\/middleware-audit/,
279
- );
280
- });
281
-
282
- test('rejects middleware without enhanceDoc', () => {
283
- const reg = new PluginRegistry();
284
- expect(() =>
285
- reg.registerPlugin('middlewares', 'old', {
286
- name: 'old',
287
- async enhance(prompt: string) {
288
- return prompt;
289
- },
290
- } as never),
291
- ).toThrow(/must export enhanceDoc/);
292
- });
293
- });
294
-
295
- describe('runPipeline — options.registry isolation', () => {
296
- test('concurrent runs with different registries see their own drivers', async () => {
297
- const regA = new PluginRegistry();
298
- const regB = new PluginRegistry();
299
- const seenA: string[] = [];
300
- const seenB: string[] = [];
301
-
302
- bootstrapBuiltins(regA);
303
- bootstrapBuiltins(regB);
304
-
305
- regA.registerPlugin('drivers', 'mock', makeDriver('mockA', seenA));
306
- regB.registerPlugin('drivers', 'mock', makeDriver('mockB', seenB));
307
-
308
- // Command-only pipeline exercises the preflight path (which uses the
309
- // registry) plus the run-loop path without requiring a real driver
310
- // invocation. We verify isolation by asserting that preflight with a
311
- // registry missing `mock` rejects, while the matching registry accepts.
312
- const config: PipelineConfig = {
313
- name: 'isolation-test',
314
- tracks: [
315
- {
316
- id: 't',
317
- name: 'T',
318
- tasks: [{ id: 'only', name: 'only', command: 'echo hi' }],
319
- },
320
- ],
321
- };
322
-
323
- const tmpA = mkdtempSync(join(tmpdir(), 'tagma-regA-'));
324
- const tmpB = mkdtempSync(join(tmpdir(), 'tagma-regB-'));
325
- try {
326
- const [resA, resB] = await Promise.all([
327
- runPipeline(config, tmpA, { registry: regA, skipPluginLoading: true }),
328
- runPipeline(config, tmpB, { registry: regB, skipPluginLoading: true }),
329
- ]);
330
- expect(resA.success).toBe(true);
331
- expect(resB.success).toBe(true);
332
- expect(resA.runId).not.toBe(resB.runId);
333
- } finally {
334
- rmSync(tmpA, { recursive: true, force: true });
335
- rmSync(tmpB, { recursive: true, force: true });
336
- }
337
- });
338
-
339
- test('preflight fails when referenced driver is missing from the passed registry', async () => {
340
- const regNoOpencode = new PluginRegistry();
341
- // Deliberately do NOT bootstrap builtins — opencode is not registered.
342
- const config: PipelineConfig = {
343
- name: 'preflight-miss',
344
- tracks: [
345
- {
346
- id: 't',
347
- name: 'T',
348
- tasks: [{ id: 'x', name: 'x', prompt: 'hello' }],
349
- },
350
- ],
351
- };
352
- const tmp = mkdtempSync(join(tmpdir(), 'tagma-miss-'));
353
- try {
354
- await expect(
355
- runPipeline(config, tmp, { registry: regNoOpencode, skipPluginLoading: true }),
356
- ).rejects.toThrow(/driver "opencode" not registered/);
357
- } finally {
358
- rmSync(tmp, { recursive: true, force: true });
359
- }
360
- });
361
-
362
- test('runPipeline rejects missing explicit registry', async () => {
363
- const config: PipelineConfig = {
364
- name: 'missing-registry',
365
- tracks: [
366
- {
367
- id: 't',
368
- name: 'T',
369
- tasks: [{ id: 'only', name: 'only', command: 'echo hi' }],
370
- },
371
- ],
372
- };
373
- const tmp = mkdtempSync(join(tmpdir(), 'tagma-default-'));
374
- try {
375
- await expect(
376
- runPipeline(config, tmp, { skipPluginLoading: true } as never),
377
- ).rejects.toThrow(/requires options\.registry/);
378
- } finally {
379
- rmSync(tmp, { recursive: true, force: true });
380
- }
381
- });
382
- });
package/src/plugins.ts DELETED
@@ -1,21 +0,0 @@
1
- export { bootstrapBuiltins } from './bootstrap';
2
- export {
3
- PluginRegistry,
4
- isValidPluginName,
5
- PLUGIN_NAME_RE,
6
- readPluginManifest,
7
- } from './registry';
8
- export type { RegisteredCapability, RegisterResult } from './registry';
9
- export type {
10
- CapabilityHandler,
11
- PluginCategory,
12
- PluginCapabilities,
13
- PluginModule,
14
- PluginManifest,
15
- PluginSetupContext,
16
- TagmaPlugin,
17
- DriverPlugin,
18
- TriggerPlugin,
19
- CompletionPlugin,
20
- MiddlewarePlugin,
21
- } from './types';