@tagma/sdk 0.7.0 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -44
- package/dist/bootstrap.d.ts +20 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +21 -11
- package/dist/bootstrap.js.map +1 -1
- package/dist/core/dataflow.d.ts.map +1 -1
- package/dist/core/dataflow.js +45 -9
- package/dist/core/dataflow.js.map +1 -1
- package/dist/core/run-context.d.ts +3 -0
- package/dist/core/run-context.d.ts.map +1 -1
- package/dist/core/run-context.js +2 -0
- package/dist/core/run-context.js.map +1 -1
- package/dist/core/task-executor.d.ts.map +1 -1
- package/dist/core/task-executor.js +46 -84
- package/dist/core/task-executor.js.map +1 -1
- package/dist/engine.d.ts +6 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +3 -0
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins.d.ts +2 -2
- package/dist/plugins.d.ts.map +1 -1
- package/dist/ports.d.ts +4 -0
- package/dist/ports.d.ts.map +1 -1
- package/dist/ports.js +27 -4
- package/dist/ports.js.map +1 -1
- package/dist/registry.d.ts +10 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +64 -25
- package/dist/registry.js.map +1 -1
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +8 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -7
- package/dist/schema.js.map +1 -1
- package/dist/tagma.d.ts +11 -1
- package/dist/tagma.d.ts.map +1 -1
- package/dist/tagma.js +6 -0
- package/dist/tagma.js.map +1 -1
- package/dist/validate-raw.d.ts +4 -4
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +89 -230
- package/dist/validate-raw.js.map +1 -1
- package/package.json +2 -2
- package/src/bootstrap.ts +23 -14
- package/src/core/dataflow.test.ts +8 -9
- package/src/core/dataflow.ts +57 -14
- package/src/core/run-context.test.ts +12 -0
- package/src/core/run-context.ts +4 -0
- package/src/core/task-executor.ts +75 -135
- package/src/engine-ports-mixed.test.ts +68 -411
- package/src/engine-ports.test.ts +37 -341
- package/src/engine.ts +8 -0
- package/src/index.ts +5 -0
- package/src/pipeline-runner.test.ts +5 -9
- package/src/plugin-registry.test.ts +138 -1
- package/src/plugins.ts +5 -2
- package/src/ports.test.ts +80 -0
- package/src/ports.ts +36 -4
- package/src/registry.ts +81 -26
- package/src/runtime.ts +20 -0
- package/src/schema-ports.test.ts +47 -197
- package/src/schema.ts +1 -7
- package/src/tagma.test.ts +72 -1
- package/src/tagma.ts +16 -1
- package/src/validate-raw-ports.test.ts +80 -393
- package/src/validate-raw.ts +90 -250
package/src/schema-ports.test.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
import yaml from 'js-yaml';
|
|
3
3
|
import type { PipelineConfig, RawPipelineConfig } from './types';
|
|
4
|
-
import {
|
|
5
|
-
deresolvePipeline,
|
|
6
|
-
parseYaml,
|
|
7
|
-
resolveConfig,
|
|
8
|
-
serializePipeline,
|
|
9
|
-
} from './schema';
|
|
4
|
+
import { deresolvePipeline, parseYaml, resolveConfig, serializePipeline } from './schema';
|
|
10
5
|
|
|
11
6
|
const WORK_DIR = process.platform === 'win32' ? 'D:\\fake-work' : '/fake-work';
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
describe('resolveConfig — ports passthrough', () => {
|
|
16
|
-
test('raw lightweight bindings survive onto the resolved task', () => {
|
|
8
|
+
describe('schema — unified bindings passthrough', () => {
|
|
9
|
+
test('typed inputs and outputs survive onto the resolved task', () => {
|
|
17
10
|
const raw: RawPipelineConfig = {
|
|
18
11
|
name: 'p',
|
|
19
12
|
tracks: [
|
|
@@ -24,94 +17,19 @@ describe('resolveConfig — ports passthrough', () => {
|
|
|
24
17
|
{
|
|
25
18
|
id: 'a',
|
|
26
19
|
command: 'echo "{{inputs.city}}"',
|
|
27
|
-
inputs: {
|
|
28
|
-
|
|
29
|
-
},
|
|
30
|
-
outputs: {
|
|
31
|
-
report: { from: 'json.reportPath' },
|
|
32
|
-
},
|
|
20
|
+
inputs: { city: { from: 't.plan.outputs.city', type: 'string', required: true } },
|
|
21
|
+
outputs: { report: { from: 'json.reportPath', type: 'string' } },
|
|
33
22
|
},
|
|
34
23
|
],
|
|
35
24
|
},
|
|
36
25
|
],
|
|
37
26
|
};
|
|
38
|
-
const
|
|
39
|
-
const task = resolved.tracks[0]!.tasks[0]!;
|
|
27
|
+
const task = resolveConfig(raw, WORK_DIR).tracks[0]!.tasks[0]!;
|
|
40
28
|
expect(task.inputs).toEqual(raw.tracks[0]!.tasks[0]!.inputs!);
|
|
41
29
|
expect(task.outputs).toEqual(raw.tracks[0]!.tasks[0]!.outputs!);
|
|
42
30
|
});
|
|
43
31
|
|
|
44
|
-
test('
|
|
45
|
-
const raw: RawPipelineConfig = {
|
|
46
|
-
name: 'p',
|
|
47
|
-
tracks: [
|
|
48
|
-
{
|
|
49
|
-
id: 't',
|
|
50
|
-
name: 'T',
|
|
51
|
-
tasks: [
|
|
52
|
-
{
|
|
53
|
-
id: 'a',
|
|
54
|
-
prompt: 'do it',
|
|
55
|
-
ports: {
|
|
56
|
-
inputs: [{ name: 'city', type: 'string', required: true }],
|
|
57
|
-
outputs: [{ name: 'temp', type: 'number', description: 'Celsius' }],
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
};
|
|
64
|
-
const resolved = resolveConfig(raw, WORK_DIR);
|
|
65
|
-
const task = resolved.tracks[0]!.tasks[0]!;
|
|
66
|
-
expect(task.ports).toBeDefined();
|
|
67
|
-
expect(task.ports!.inputs).toEqual([
|
|
68
|
-
{ name: 'city', type: 'string', required: true },
|
|
69
|
-
]);
|
|
70
|
-
expect(task.ports!.outputs).toEqual([
|
|
71
|
-
{ name: 'temp', type: 'number', description: 'Celsius' },
|
|
72
|
-
]);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('tasks without ports still resolve with ports === undefined', () => {
|
|
76
|
-
const raw: RawPipelineConfig = {
|
|
77
|
-
name: 'p',
|
|
78
|
-
tracks: [
|
|
79
|
-
{ id: 't', name: 'T', tasks: [{ id: 'a', prompt: 'do it' }] },
|
|
80
|
-
],
|
|
81
|
-
};
|
|
82
|
-
const resolved = resolveConfig(raw, WORK_DIR);
|
|
83
|
-
expect(resolved.tracks[0]!.tasks[0]!.ports).toBeUndefined();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('ports is not inherited from track or pipeline', () => {
|
|
87
|
-
// Ports describe a per-task I/O contract. If we accidentally pulled
|
|
88
|
-
// them from track defaults, two tasks in the same track would share
|
|
89
|
-
// input ports and downstream data-flow would be ambiguous. Test that
|
|
90
|
-
// a track with an unrelated `middlewares` default doesn't spread
|
|
91
|
-
// anywhere unexpected — purely a regression guard for the no-inherit
|
|
92
|
-
// invariant.
|
|
93
|
-
const raw: RawPipelineConfig = {
|
|
94
|
-
name: 'p',
|
|
95
|
-
tracks: [
|
|
96
|
-
{
|
|
97
|
-
id: 't',
|
|
98
|
-
name: 'T',
|
|
99
|
-
middlewares: [{ type: 'static_context', file: './x' }],
|
|
100
|
-
tasks: [{ id: 'a', prompt: 'x' }, { id: 'b', prompt: 'y' }],
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
};
|
|
104
|
-
const resolved = resolveConfig(raw, WORK_DIR);
|
|
105
|
-
for (const task of resolved.tracks[0]!.tasks) {
|
|
106
|
-
expect(task.ports).toBeUndefined();
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// ─── deresolvePipeline preserves ports ───────────────────────────────
|
|
112
|
-
|
|
113
|
-
describe('deresolvePipeline — ports round-trip', () => {
|
|
114
|
-
test('lightweight bindings round-trip', () => {
|
|
32
|
+
test('typed inputs and outputs round-trip through deresolve', () => {
|
|
115
33
|
const raw: RawPipelineConfig = {
|
|
116
34
|
name: 'p',
|
|
117
35
|
tracks: [
|
|
@@ -123,25 +41,26 @@ describe('deresolvePipeline — ports round-trip', () => {
|
|
|
123
41
|
id: 'a',
|
|
124
42
|
command: 'echo "{{inputs.city}}"',
|
|
125
43
|
inputs: {
|
|
126
|
-
city: {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
44
|
+
city: {
|
|
45
|
+
from: 't.plan.outputs.city',
|
|
46
|
+
type: 'enum',
|
|
47
|
+
enum: ['Shanghai', 'Paris'],
|
|
48
|
+
required: true,
|
|
49
|
+
},
|
|
131
50
|
},
|
|
51
|
+
outputs: { raw: { from: 'stdout' } },
|
|
132
52
|
},
|
|
133
53
|
],
|
|
134
54
|
},
|
|
135
55
|
],
|
|
136
56
|
};
|
|
137
|
-
const
|
|
138
|
-
const back = deresolvePipeline(resolved, WORK_DIR);
|
|
57
|
+
const back = deresolvePipeline(resolveConfig(raw, WORK_DIR), WORK_DIR);
|
|
139
58
|
expect(back.tracks[0]!.tasks[0]!.inputs).toEqual(raw.tracks[0]!.tasks[0]!.inputs!);
|
|
140
59
|
expect(back.tracks[0]!.tasks[0]!.outputs).toEqual(raw.tracks[0]!.tasks[0]!.outputs!);
|
|
141
60
|
});
|
|
142
61
|
|
|
143
|
-
test('
|
|
144
|
-
const
|
|
62
|
+
test('empty binding maps are dropped on deresolve', () => {
|
|
63
|
+
const resolved: PipelineConfig = {
|
|
145
64
|
name: 'p',
|
|
146
65
|
tracks: [
|
|
147
66
|
{
|
|
@@ -150,22 +69,21 @@ describe('deresolvePipeline — ports round-trip', () => {
|
|
|
150
69
|
tasks: [
|
|
151
70
|
{
|
|
152
71
|
id: 'a',
|
|
72
|
+
name: 'a',
|
|
153
73
|
prompt: 'hi',
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
outputs: [{ name: 'temp', type: 'number' }],
|
|
157
|
-
},
|
|
74
|
+
inputs: {},
|
|
75
|
+
outputs: {},
|
|
158
76
|
},
|
|
159
77
|
],
|
|
160
78
|
},
|
|
161
79
|
],
|
|
162
80
|
};
|
|
163
|
-
const resolved = resolveConfig(raw, WORK_DIR);
|
|
164
81
|
const back = deresolvePipeline(resolved, WORK_DIR);
|
|
165
|
-
expect(back.tracks[0]!.tasks[0]!.
|
|
82
|
+
expect(back.tracks[0]!.tasks[0]!.inputs).toBeUndefined();
|
|
83
|
+
expect(back.tracks[0]!.tasks[0]!.outputs).toBeUndefined();
|
|
166
84
|
});
|
|
167
85
|
|
|
168
|
-
test('ports
|
|
86
|
+
test('legacy ports are not carried through resolve or deresolve', () => {
|
|
169
87
|
const raw: RawPipelineConfig = {
|
|
170
88
|
name: 'p',
|
|
171
89
|
tracks: [
|
|
@@ -175,51 +93,20 @@ describe('deresolvePipeline — ports round-trip', () => {
|
|
|
175
93
|
tasks: [
|
|
176
94
|
{
|
|
177
95
|
id: 'a',
|
|
178
|
-
command: 'echo
|
|
179
|
-
ports: { outputs: [{ name: '
|
|
96
|
+
command: 'echo ok',
|
|
97
|
+
ports: { outputs: [{ name: 'old', type: 'string' }] },
|
|
180
98
|
},
|
|
181
99
|
],
|
|
182
100
|
},
|
|
183
101
|
],
|
|
184
102
|
};
|
|
185
103
|
const resolved = resolveConfig(raw, WORK_DIR);
|
|
186
|
-
|
|
187
|
-
expect(back.tracks[0]!.tasks[0]!.ports).toEqual({
|
|
188
|
-
outputs: [{ name: 'x', type: 'string' }],
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('empty ports ({}) is dropped on deresolve', () => {
|
|
193
|
-
// YAML round-trip prefers field absence over `ports: {}` so a task
|
|
194
|
-
// that once declared a port but had it cleared in the editor
|
|
195
|
-
// doesn't persist a useless empty object in the file.
|
|
196
|
-
const resolved: PipelineConfig = {
|
|
197
|
-
name: 'p',
|
|
198
|
-
tracks: [
|
|
199
|
-
{
|
|
200
|
-
id: 't',
|
|
201
|
-
name: 'T',
|
|
202
|
-
driver: 'opencode',
|
|
203
|
-
permissions: { read: true, write: false, execute: false },
|
|
204
|
-
on_failure: 'skip_downstream',
|
|
205
|
-
tasks: [
|
|
206
|
-
{
|
|
207
|
-
id: 'a',
|
|
208
|
-
name: 'a',
|
|
209
|
-
prompt: 'hi',
|
|
210
|
-
permissions: { read: true, write: false, execute: false },
|
|
211
|
-
driver: 'opencode',
|
|
212
|
-
ports: {},
|
|
213
|
-
},
|
|
214
|
-
],
|
|
215
|
-
},
|
|
216
|
-
],
|
|
217
|
-
};
|
|
104
|
+
expect(resolved.tracks[0]!.tasks[0]!.ports).toBeUndefined();
|
|
218
105
|
const back = deresolvePipeline(resolved, WORK_DIR);
|
|
219
106
|
expect(back.tracks[0]!.tasks[0]!.ports).toBeUndefined();
|
|
220
107
|
});
|
|
221
108
|
|
|
222
|
-
test('YAML round-trip
|
|
109
|
+
test('YAML round-trip preserves typed unified binding shape', () => {
|
|
223
110
|
const raw: RawPipelineConfig = {
|
|
224
111
|
name: 'p',
|
|
225
112
|
tracks: [
|
|
@@ -230,18 +117,13 @@ describe('deresolvePipeline — ports round-trip', () => {
|
|
|
230
117
|
{
|
|
231
118
|
id: 'classify',
|
|
232
119
|
prompt: 'pick a bucket',
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
type: 'enum',
|
|
241
|
-
enum: ['spam', 'ham'],
|
|
242
|
-
description: 'Classification',
|
|
243
|
-
},
|
|
244
|
-
],
|
|
120
|
+
inputs: { doc: { type: 'string', required: true, description: 'Full text' } },
|
|
121
|
+
outputs: {
|
|
122
|
+
bucket: {
|
|
123
|
+
type: 'enum',
|
|
124
|
+
enum: ['spam', 'ham'],
|
|
125
|
+
description: 'Classification',
|
|
126
|
+
},
|
|
245
127
|
},
|
|
246
128
|
},
|
|
247
129
|
],
|
|
@@ -250,14 +132,11 @@ describe('deresolvePipeline — ports round-trip', () => {
|
|
|
250
132
|
};
|
|
251
133
|
const yamlText = serializePipeline(raw);
|
|
252
134
|
const parsed = (yaml.load(yamlText) as { pipeline: RawPipelineConfig }).pipeline;
|
|
253
|
-
expect(parsed.tracks[0]!.tasks[0]!.
|
|
135
|
+
expect(parsed.tracks[0]!.tasks[0]!.inputs).toEqual(raw.tracks[0]!.tasks[0]!.inputs!);
|
|
136
|
+
expect(parsed.tracks[0]!.tasks[0]!.outputs).toEqual(raw.tracks[0]!.tasks[0]!.outputs!);
|
|
254
137
|
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// ─── parseYaml accepts ports ─────────────────────────────────────────
|
|
258
138
|
|
|
259
|
-
|
|
260
|
-
test('real-world YAML with lightweight bindings parses cleanly', () => {
|
|
139
|
+
test('real-world YAML with typed bindings parses cleanly', () => {
|
|
261
140
|
const text = `pipeline:
|
|
262
141
|
name: demo
|
|
263
142
|
tracks:
|
|
@@ -267,56 +146,27 @@ describe('parseYaml — accepts ports declarations', () => {
|
|
|
267
146
|
- id: build
|
|
268
147
|
command: bun run build
|
|
269
148
|
outputs:
|
|
270
|
-
bundlePath:
|
|
149
|
+
bundlePath:
|
|
150
|
+
from: json.bundlePath
|
|
151
|
+
type: string
|
|
271
152
|
- id: test
|
|
272
153
|
depends_on: [build]
|
|
273
154
|
command: 'bun test "{{inputs.bundlePath}}"'
|
|
274
155
|
inputs:
|
|
275
156
|
bundlePath:
|
|
276
157
|
from: t.build.outputs.bundlePath
|
|
158
|
+
type: string
|
|
277
159
|
required: true
|
|
278
160
|
`;
|
|
279
161
|
const config = parseYaml(text);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
162
|
+
expect(config.tracks[0]!.tasks[0]!.outputs!.bundlePath).toEqual({
|
|
163
|
+
from: 'json.bundlePath',
|
|
164
|
+
type: 'string',
|
|
165
|
+
});
|
|
166
|
+
expect(config.tracks[0]!.tasks[1]!.inputs!.bundlePath).toEqual({
|
|
284
167
|
from: 't.build.outputs.bundlePath',
|
|
168
|
+
type: 'string',
|
|
285
169
|
required: true,
|
|
286
170
|
});
|
|
287
171
|
});
|
|
288
|
-
|
|
289
|
-
test('real-world YAML with ports parses cleanly', () => {
|
|
290
|
-
const text = `pipeline:
|
|
291
|
-
name: demo
|
|
292
|
-
tracks:
|
|
293
|
-
- id: t
|
|
294
|
-
name: Main
|
|
295
|
-
tasks:
|
|
296
|
-
- id: plan
|
|
297
|
-
prompt: Pick a city and id
|
|
298
|
-
ports:
|
|
299
|
-
outputs:
|
|
300
|
-
- name: city
|
|
301
|
-
type: string
|
|
302
|
-
description: Target city
|
|
303
|
-
- name: id
|
|
304
|
-
type: number
|
|
305
|
-
- id: fetch
|
|
306
|
-
depends_on: [plan]
|
|
307
|
-
command: 'weather.sh --city "{{inputs.city}}" --id {{inputs.id}}'
|
|
308
|
-
ports:
|
|
309
|
-
inputs:
|
|
310
|
-
- { name: city, type: string, required: true }
|
|
311
|
-
- { name: id, type: number, required: true }
|
|
312
|
-
outputs:
|
|
313
|
-
- { name: temp, type: number }
|
|
314
|
-
`;
|
|
315
|
-
const config = parseYaml(text);
|
|
316
|
-
const plan = config.tracks[0]!.tasks[0]!;
|
|
317
|
-
const fetch = config.tracks[0]!.tasks[1]!;
|
|
318
|
-
expect(plan.ports!.outputs!.map((p) => p.name)).toEqual(['city', 'id']);
|
|
319
|
-
expect(fetch.ports!.inputs!.map((p) => p.name)).toEqual(['city', 'id']);
|
|
320
|
-
expect(fetch.ports!.outputs!.map((p) => p.name)).toEqual(['temp']);
|
|
321
|
-
});
|
|
322
172
|
});
|
package/src/schema.ts
CHANGED
|
@@ -161,11 +161,10 @@ export function resolveConfig(raw: RawPipelineConfig, workDir: string): Pipeline
|
|
|
161
161
|
completion: rawTask.completion,
|
|
162
162
|
agent_profile: rawTask.agent_profile ?? rawTrack.agent_profile,
|
|
163
163
|
cwd: rawTask.cwd ? validatePath(rawTask.cwd, workDir) : trackCwd,
|
|
164
|
-
//
|
|
164
|
+
// Unified bindings have no inheritance; they describe
|
|
165
165
|
// per-task data flow, not cross-task defaults.
|
|
166
166
|
inputs: rawTask.inputs,
|
|
167
167
|
outputs: rawTask.outputs,
|
|
168
|
-
ports: rawTask.ports,
|
|
169
168
|
};
|
|
170
169
|
});
|
|
171
170
|
|
|
@@ -313,11 +312,6 @@ export function deresolvePipeline(config: PipelineConfig, workDir: string): RawP
|
|
|
313
312
|
: {}),
|
|
314
313
|
...(task.inputs && Object.keys(task.inputs).length > 0 ? { inputs: task.inputs } : {}),
|
|
315
314
|
...(task.outputs && Object.keys(task.outputs).length > 0 ? { outputs: task.outputs } : {}),
|
|
316
|
-
...(task.ports &&
|
|
317
|
-
((task.ports.inputs && task.ports.inputs.length > 0) ||
|
|
318
|
-
(task.ports.outputs && task.ports.outputs.length > 0))
|
|
319
|
-
? { ports: task.ports }
|
|
320
|
-
: {}),
|
|
321
315
|
};
|
|
322
316
|
});
|
|
323
317
|
|
package/src/tagma.test.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { createTagma } from './tagma';
|
|
6
|
-
import type { DriverPlugin,
|
|
6
|
+
import type { DriverPlugin, TagmaPlugin, TaskResult } from './types';
|
|
7
|
+
import type { TagmaRuntime } from './runtime';
|
|
7
8
|
|
|
8
9
|
function makeDir(prefix: string): string {
|
|
9
10
|
return mkdtempSync(join(tmpdir(), prefix));
|
|
@@ -21,6 +22,76 @@ function makeDriver(name: string, marker: string[]): DriverPlugin {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
describe('createTagma', () => {
|
|
25
|
+
test('runs command tasks through the configured runtime', async () => {
|
|
26
|
+
const calls: string[] = [];
|
|
27
|
+
const taskResult: TaskResult = {
|
|
28
|
+
exitCode: 0,
|
|
29
|
+
stdout: 'runtime-ok',
|
|
30
|
+
stderr: '',
|
|
31
|
+
stdoutPath: null,
|
|
32
|
+
stderrPath: null,
|
|
33
|
+
stdoutBytes: 10,
|
|
34
|
+
stderrBytes: 0,
|
|
35
|
+
durationMs: 1,
|
|
36
|
+
sessionId: null,
|
|
37
|
+
normalizedOutput: null,
|
|
38
|
+
failureKind: null,
|
|
39
|
+
};
|
|
40
|
+
const runtime: TagmaRuntime = {
|
|
41
|
+
async runCommand(command, cwd) {
|
|
42
|
+
calls.push(`${cwd}:${command}`);
|
|
43
|
+
return taskResult;
|
|
44
|
+
},
|
|
45
|
+
async runSpawn() {
|
|
46
|
+
throw new Error('runSpawn should not be called for command tasks');
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const tagma = createTagma({ builtins: false, runtime });
|
|
50
|
+
const dir = makeDir('tagma-runtime-run-');
|
|
51
|
+
try {
|
|
52
|
+
const result = await tagma.run(
|
|
53
|
+
{
|
|
54
|
+
name: 'runtime-run',
|
|
55
|
+
tracks: [
|
|
56
|
+
{
|
|
57
|
+
id: 't',
|
|
58
|
+
name: 'T',
|
|
59
|
+
tasks: [{ id: 'cmd', name: 'cmd', command: 'fake-only-command' }],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
cwd: dir,
|
|
65
|
+
skipPluginLoading: true,
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(result.success).toBe(true);
|
|
70
|
+
expect(calls).toEqual([`${dir}:fake-only-command`]);
|
|
71
|
+
expect(result.states.get('t.cmd')?.result?.stdout).toBe('runtime-ok');
|
|
72
|
+
} finally {
|
|
73
|
+
rmSync(dir, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('registers capability plugins passed to options', () => {
|
|
78
|
+
const seen: string[] = [];
|
|
79
|
+
const driver = makeDriver('driver-plugin', seen);
|
|
80
|
+
const plugin: TagmaPlugin = {
|
|
81
|
+
name: 'tagma-plugin-local',
|
|
82
|
+
capabilities: {
|
|
83
|
+
drivers: {
|
|
84
|
+
mock: driver,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const tagma = createTagma({ builtins: false, plugins: [plugin] });
|
|
90
|
+
|
|
91
|
+
expect(tagma.registry.getHandler<DriverPlugin>('drivers', 'mock')).toBe(driver);
|
|
92
|
+
expect(seen).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
24
95
|
test('instances own isolated plugin registries', () => {
|
|
25
96
|
const seenA: string[] = [];
|
|
26
97
|
const seenB: string[] = [];
|
package/src/tagma.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { runPipeline, type EngineResult, type RunPipelineOptions } from './engin
|
|
|
2
2
|
import { bootstrapBuiltins } from './bootstrap';
|
|
3
3
|
import { PluginRegistry } from './registry';
|
|
4
4
|
import { validateConfig } from './schema';
|
|
5
|
-
import type
|
|
5
|
+
import { bunRuntime, type TagmaRuntime } from './runtime';
|
|
6
|
+
import type { PipelineConfig, TagmaPlugin } from './types';
|
|
6
7
|
|
|
7
8
|
export interface CreateTagmaOptions {
|
|
8
9
|
/**
|
|
@@ -14,6 +15,15 @@ export interface CreateTagmaOptions {
|
|
|
14
15
|
* instance registry. Defaults to true.
|
|
15
16
|
*/
|
|
16
17
|
readonly builtins?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Package-level capability plugins to register into this SDK instance.
|
|
20
|
+
*/
|
|
21
|
+
readonly plugins?: readonly TagmaPlugin[];
|
|
22
|
+
/**
|
|
23
|
+
* Runtime implementation used for command and driver process execution.
|
|
24
|
+
* Defaults to the SDK's Bun runtime.
|
|
25
|
+
*/
|
|
26
|
+
readonly runtime?: TagmaRuntime;
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
export interface TagmaRunOptions extends Omit<RunPipelineOptions, 'registry'> {
|
|
@@ -28,9 +38,13 @@ export interface Tagma {
|
|
|
28
38
|
|
|
29
39
|
export function createTagma(options: CreateTagmaOptions = {}): Tagma {
|
|
30
40
|
const registry = options.registry ?? new PluginRegistry();
|
|
41
|
+
const runtime = options.runtime ?? bunRuntime();
|
|
31
42
|
if (options.builtins !== false) {
|
|
32
43
|
bootstrapBuiltins(registry);
|
|
33
44
|
}
|
|
45
|
+
for (const plugin of options.plugins ?? []) {
|
|
46
|
+
registry.registerTagmaPlugin(plugin);
|
|
47
|
+
}
|
|
34
48
|
|
|
35
49
|
return {
|
|
36
50
|
registry,
|
|
@@ -38,6 +52,7 @@ export function createTagma(options: CreateTagmaOptions = {}): Tagma {
|
|
|
38
52
|
return runPipeline(config, cwd, {
|
|
39
53
|
...runOptions,
|
|
40
54
|
registry,
|
|
55
|
+
runtime,
|
|
41
56
|
});
|
|
42
57
|
},
|
|
43
58
|
validate(config) {
|