@tagma/sdk 0.6.12 → 0.7.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/README.md +56 -15
- package/dist/bootstrap.d.ts +6 -6
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +5 -6
- package/dist/bootstrap.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -0
- package/dist/core/dataflow.d.ts +23 -0
- package/dist/core/dataflow.d.ts.map +1 -0
- package/dist/core/dataflow.js +99 -0
- package/dist/core/dataflow.js.map +1 -0
- package/dist/core/log-prune.d.ts +16 -0
- package/dist/core/log-prune.d.ts.map +1 -0
- package/dist/core/log-prune.js +34 -0
- package/dist/core/log-prune.js.map +1 -0
- package/dist/core/preflight.d.ts +13 -0
- package/dist/core/preflight.d.ts.map +1 -0
- package/dist/core/preflight.js +61 -0
- package/dist/core/preflight.js.map +1 -0
- package/dist/core/run-context.d.ts +52 -0
- package/dist/core/run-context.d.ts.map +1 -0
- package/dist/core/run-context.js +156 -0
- package/dist/core/run-context.js.map +1 -0
- package/dist/core/run-state.d.ts +25 -0
- package/dist/core/run-state.d.ts.map +1 -0
- package/dist/core/run-state.js +93 -0
- package/dist/core/run-state.js.map +1 -0
- package/dist/core/scheduler.d.ts +13 -0
- package/dist/core/scheduler.d.ts.map +1 -0
- package/dist/core/scheduler.js +35 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/task-executor.d.ts +13 -0
- package/dist/core/task-executor.d.ts.map +1 -0
- package/dist/core/task-executor.js +623 -0
- package/dist/core/task-executor.js.map +1 -0
- package/dist/core/trigger-errors.d.ts +9 -0
- package/dist/core/trigger-errors.d.ts.map +1 -0
- package/dist/core/trigger-errors.js +15 -0
- package/dist/core/trigger-errors.js.map +1 -0
- package/dist/engine.d.ts +6 -14
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +68 -1035
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline-definition.d.ts +3 -0
- package/dist/pipeline-definition.d.ts.map +1 -0
- package/dist/pipeline-definition.js +4 -0
- package/dist/pipeline-definition.js.map +1 -0
- package/dist/pipeline-runner.d.ts +2 -1
- package/dist/pipeline-runner.d.ts.map +1 -1
- package/dist/pipeline-runner.js +2 -2
- package/dist/pipeline-runner.js.map +1 -1
- package/dist/plugins.d.ts +5 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +3 -0
- package/dist/plugins.js.map +1 -0
- 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 +3 -19
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +7 -35
- package/dist/registry.js.map +1 -1
- package/dist/tagma.d.ts +24 -0
- package/dist/tagma.d.ts.map +1 -0
- package/dist/tagma.js +23 -0
- package/dist/tagma.js.map +1 -0
- package/dist/utils-api.d.ts +2 -0
- package/dist/utils-api.d.ts.map +1 -0
- package/dist/utils-api.js +2 -0
- package/dist/utils-api.js.map +1 -0
- package/dist/validate-raw.d.ts +4 -4
- package/dist/validate-raw.js +91 -132
- package/dist/validate-raw.js.map +1 -1
- package/dist/yaml.d.ts +4 -0
- package/dist/yaml.d.ts.map +1 -0
- package/dist/yaml.js +3 -0
- package/dist/yaml.js.map +1 -0
- package/package.json +53 -8
- package/src/bootstrap.ts +6 -6
- package/src/config.ts +26 -0
- package/src/core/dataflow.test.ts +166 -0
- package/src/core/dataflow.ts +161 -0
- package/src/core/log-prune.test.ts +58 -0
- package/src/core/log-prune.ts +43 -0
- package/src/core/preflight.test.ts +49 -0
- package/src/core/preflight.ts +89 -0
- package/src/core/run-context.test.ts +244 -0
- package/src/core/run-context.ts +207 -0
- package/src/core/run-state.test.ts +98 -0
- package/src/core/run-state.ts +122 -0
- package/src/core/scheduler.test.ts +83 -0
- package/src/core/scheduler.ts +42 -0
- package/src/core/task-executor.ts +769 -0
- package/src/core/trigger-errors.ts +15 -0
- package/src/engine-ports-mixed.test.ts +68 -411
- package/src/engine-ports.test.ts +37 -341
- package/src/engine.ts +80 -1248
- package/src/index.ts +28 -0
- package/src/pipeline-definition.ts +5 -0
- package/src/pipeline-runner.test.ts +5 -9
- package/src/pipeline-runner.ts +3 -2
- package/src/plugin-registry.test.ts +7 -10
- package/src/plugins.ts +18 -0
- package/src/ports.test.ts +80 -0
- package/src/ports.ts +36 -4
- package/src/registry.ts +7 -49
- package/src/schema-ports.test.ts +41 -214
- package/src/tagma.test.ts +84 -0
- package/src/tagma.ts +47 -0
- package/src/utils-api.ts +8 -0
- package/src/validate-raw-ports.test.ts +80 -393
- package/src/validate-raw.ts +93 -137
- package/src/yaml.ts +11 -0
- package/dist/sdk.d.ts +0 -32
- package/dist/sdk.d.ts.map +0 -1
- package/dist/sdk.js +0 -41
- package/dist/sdk.js.map +0 -1
- package/src/sdk.ts +0 -151
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,103 +41,49 @@ 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 raw: RawPipelineConfig = {
|
|
145
|
-
name: 'p',
|
|
146
|
-
tracks: [
|
|
147
|
-
{
|
|
148
|
-
id: 't',
|
|
149
|
-
name: 'T',
|
|
150
|
-
tasks: [
|
|
151
|
-
{
|
|
152
|
-
id: 'a',
|
|
153
|
-
prompt: 'hi',
|
|
154
|
-
ports: {
|
|
155
|
-
inputs: [{ name: 'city', type: 'string', required: true }],
|
|
156
|
-
outputs: [{ name: 'temp', type: 'number' }],
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
},
|
|
161
|
-
],
|
|
162
|
-
};
|
|
163
|
-
const resolved = resolveConfig(raw, WORK_DIR);
|
|
164
|
-
const back = deresolvePipeline(resolved, WORK_DIR);
|
|
165
|
-
expect(back.tracks[0]!.tasks[0]!.ports).toEqual(raw.tracks[0]!.tasks[0]!.ports!);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('ports with only outputs round-trip', () => {
|
|
169
|
-
const raw: RawPipelineConfig = {
|
|
170
|
-
name: 'p',
|
|
171
|
-
tracks: [
|
|
172
|
-
{
|
|
173
|
-
id: 't',
|
|
174
|
-
name: 'T',
|
|
175
|
-
tasks: [
|
|
176
|
-
{
|
|
177
|
-
id: 'a',
|
|
178
|
-
command: 'echo hi',
|
|
179
|
-
ports: { outputs: [{ name: 'x', type: 'string' }] },
|
|
180
|
-
},
|
|
181
|
-
],
|
|
182
|
-
},
|
|
183
|
-
],
|
|
184
|
-
};
|
|
185
|
-
const resolved = resolveConfig(raw, WORK_DIR);
|
|
186
|
-
const back = deresolvePipeline(resolved, WORK_DIR);
|
|
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.
|
|
62
|
+
test('empty binding maps are dropped on deresolve', () => {
|
|
196
63
|
const resolved: PipelineConfig = {
|
|
197
64
|
name: 'p',
|
|
198
65
|
tracks: [
|
|
199
66
|
{
|
|
200
67
|
id: 't',
|
|
201
68
|
name: 'T',
|
|
202
|
-
driver: 'opencode',
|
|
203
|
-
permissions: { read: true, write: false, execute: false },
|
|
204
|
-
on_failure: 'skip_downstream',
|
|
205
69
|
tasks: [
|
|
206
70
|
{
|
|
207
71
|
id: 'a',
|
|
208
72
|
name: 'a',
|
|
209
73
|
prompt: 'hi',
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
ports: {},
|
|
74
|
+
inputs: {},
|
|
75
|
+
outputs: {},
|
|
213
76
|
},
|
|
214
77
|
],
|
|
215
78
|
},
|
|
216
79
|
],
|
|
217
80
|
};
|
|
218
81
|
const back = deresolvePipeline(resolved, WORK_DIR);
|
|
219
|
-
expect(back.tracks[0]!.tasks[0]!.
|
|
82
|
+
expect(back.tracks[0]!.tasks[0]!.inputs).toBeUndefined();
|
|
83
|
+
expect(back.tracks[0]!.tasks[0]!.outputs).toBeUndefined();
|
|
220
84
|
});
|
|
221
85
|
|
|
222
|
-
test('YAML round-trip
|
|
86
|
+
test('YAML round-trip preserves typed unified binding shape', () => {
|
|
223
87
|
const raw: RawPipelineConfig = {
|
|
224
88
|
name: 'p',
|
|
225
89
|
tracks: [
|
|
@@ -230,18 +94,13 @@ describe('deresolvePipeline — ports round-trip', () => {
|
|
|
230
94
|
{
|
|
231
95
|
id: 'classify',
|
|
232
96
|
prompt: 'pick a bucket',
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
type: 'enum',
|
|
241
|
-
enum: ['spam', 'ham'],
|
|
242
|
-
description: 'Classification',
|
|
243
|
-
},
|
|
244
|
-
],
|
|
97
|
+
inputs: { doc: { type: 'string', required: true, description: 'Full text' } },
|
|
98
|
+
outputs: {
|
|
99
|
+
bucket: {
|
|
100
|
+
type: 'enum',
|
|
101
|
+
enum: ['spam', 'ham'],
|
|
102
|
+
description: 'Classification',
|
|
103
|
+
},
|
|
245
104
|
},
|
|
246
105
|
},
|
|
247
106
|
],
|
|
@@ -250,14 +109,11 @@ describe('deresolvePipeline — ports round-trip', () => {
|
|
|
250
109
|
};
|
|
251
110
|
const yamlText = serializePipeline(raw);
|
|
252
111
|
const parsed = (yaml.load(yamlText) as { pipeline: RawPipelineConfig }).pipeline;
|
|
253
|
-
expect(parsed.tracks[0]!.tasks[0]!.
|
|
112
|
+
expect(parsed.tracks[0]!.tasks[0]!.inputs).toEqual(raw.tracks[0]!.tasks[0]!.inputs!);
|
|
113
|
+
expect(parsed.tracks[0]!.tasks[0]!.outputs).toEqual(raw.tracks[0]!.tasks[0]!.outputs!);
|
|
254
114
|
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// ─── parseYaml accepts ports ─────────────────────────────────────────
|
|
258
115
|
|
|
259
|
-
|
|
260
|
-
test('real-world YAML with lightweight bindings parses cleanly', () => {
|
|
116
|
+
test('real-world YAML with typed bindings parses cleanly', () => {
|
|
261
117
|
const text = `pipeline:
|
|
262
118
|
name: demo
|
|
263
119
|
tracks:
|
|
@@ -267,56 +123,27 @@ describe('parseYaml — accepts ports declarations', () => {
|
|
|
267
123
|
- id: build
|
|
268
124
|
command: bun run build
|
|
269
125
|
outputs:
|
|
270
|
-
bundlePath:
|
|
126
|
+
bundlePath:
|
|
127
|
+
from: json.bundlePath
|
|
128
|
+
type: string
|
|
271
129
|
- id: test
|
|
272
130
|
depends_on: [build]
|
|
273
131
|
command: 'bun test "{{inputs.bundlePath}}"'
|
|
274
132
|
inputs:
|
|
275
133
|
bundlePath:
|
|
276
134
|
from: t.build.outputs.bundlePath
|
|
135
|
+
type: string
|
|
277
136
|
required: true
|
|
278
137
|
`;
|
|
279
138
|
const config = parseYaml(text);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
139
|
+
expect(config.tracks[0]!.tasks[0]!.outputs!.bundlePath).toEqual({
|
|
140
|
+
from: 'json.bundlePath',
|
|
141
|
+
type: 'string',
|
|
142
|
+
});
|
|
143
|
+
expect(config.tracks[0]!.tasks[1]!.inputs!.bundlePath).toEqual({
|
|
284
144
|
from: 't.build.outputs.bundlePath',
|
|
145
|
+
type: 'string',
|
|
285
146
|
required: true,
|
|
286
147
|
});
|
|
287
148
|
});
|
|
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
149
|
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createTagma } from './tagma';
|
|
6
|
+
import type { DriverPlugin, PipelineConfig } from './types';
|
|
7
|
+
|
|
8
|
+
function makeDir(prefix: string): string {
|
|
9
|
+
return mkdtempSync(join(tmpdir(), prefix));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function makeDriver(name: string, marker: string[]): DriverPlugin {
|
|
13
|
+
return {
|
|
14
|
+
name,
|
|
15
|
+
capabilities: { sessionResume: false, systemPrompt: false, outputFormat: false },
|
|
16
|
+
async buildCommand() {
|
|
17
|
+
marker.push(name);
|
|
18
|
+
return { args: ['echo', name] };
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('createTagma', () => {
|
|
24
|
+
test('instances own isolated plugin registries', () => {
|
|
25
|
+
const seenA: string[] = [];
|
|
26
|
+
const seenB: string[] = [];
|
|
27
|
+
const tagmaA = createTagma({ builtins: false });
|
|
28
|
+
const tagmaB = createTagma({ builtins: false });
|
|
29
|
+
|
|
30
|
+
tagmaA.registry.registerPlugin('drivers', 'mock', makeDriver('driver-a', seenA));
|
|
31
|
+
tagmaB.registry.registerPlugin('drivers', 'mock', makeDriver('driver-b', seenB));
|
|
32
|
+
|
|
33
|
+
expect(tagmaA.registry.getHandler<DriverPlugin>('drivers', 'mock').name).toBe('driver-a');
|
|
34
|
+
expect(tagmaB.registry.getHandler<DriverPlugin>('drivers', 'mock').name).toBe('driver-b');
|
|
35
|
+
expect(seenA).toEqual([]);
|
|
36
|
+
expect(seenB).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('run uses only the instance registry', async () => {
|
|
40
|
+
const tagma = createTagma({ builtins: false });
|
|
41
|
+
const dir = makeDir('tagma-instance-run-');
|
|
42
|
+
try {
|
|
43
|
+
await expect(
|
|
44
|
+
tagma.run(
|
|
45
|
+
{
|
|
46
|
+
name: 'instance-run',
|
|
47
|
+
tracks: [
|
|
48
|
+
{
|
|
49
|
+
id: 't',
|
|
50
|
+
name: 'T',
|
|
51
|
+
tasks: [{ id: 'prompt', name: 'prompt', prompt: 'hello' }],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
cwd: dir,
|
|
57
|
+
skipPluginLoading: true,
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
).rejects.toThrow(/driver "opencode" not registered/);
|
|
61
|
+
} finally {
|
|
62
|
+
rmSync(dir, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('validate returns structural pipeline errors without running tasks', () => {
|
|
67
|
+
const tagma = createTagma({ builtins: false });
|
|
68
|
+
|
|
69
|
+
expect(
|
|
70
|
+
tagma.validate({
|
|
71
|
+
name: 'invalid',
|
|
72
|
+
tracks: [
|
|
73
|
+
{
|
|
74
|
+
id: 't',
|
|
75
|
+
name: 'T',
|
|
76
|
+
tasks: [
|
|
77
|
+
{ id: 'a', name: 'A', command: 'echo a', depends_on: ['missing'] },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
}),
|
|
82
|
+
).toEqual(['Task reference "missing" not found']);
|
|
83
|
+
});
|
|
84
|
+
});
|
package/src/tagma.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { runPipeline, type EngineResult, type RunPipelineOptions } from './engine';
|
|
2
|
+
import { bootstrapBuiltins } from './bootstrap';
|
|
3
|
+
import { PluginRegistry } from './registry';
|
|
4
|
+
import { validateConfig } from './schema';
|
|
5
|
+
import type { PipelineConfig } from './types';
|
|
6
|
+
|
|
7
|
+
export interface CreateTagmaOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Registry used by this SDK instance. Omit to create an isolated registry.
|
|
10
|
+
*/
|
|
11
|
+
readonly registry?: PluginRegistry;
|
|
12
|
+
/**
|
|
13
|
+
* Register built-in drivers/triggers/completions/middlewares into the
|
|
14
|
+
* instance registry. Defaults to true.
|
|
15
|
+
*/
|
|
16
|
+
readonly builtins?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TagmaRunOptions extends Omit<RunPipelineOptions, 'registry'> {
|
|
20
|
+
readonly cwd: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Tagma {
|
|
24
|
+
readonly registry: PluginRegistry;
|
|
25
|
+
run(config: PipelineConfig, options: TagmaRunOptions): Promise<EngineResult>;
|
|
26
|
+
validate(config: PipelineConfig): readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createTagma(options: CreateTagmaOptions = {}): Tagma {
|
|
30
|
+
const registry = options.registry ?? new PluginRegistry();
|
|
31
|
+
if (options.builtins !== false) {
|
|
32
|
+
bootstrapBuiltins(registry);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
registry,
|
|
37
|
+
run(config, { cwd, ...runOptions }) {
|
|
38
|
+
return runPipeline(config, cwd, {
|
|
39
|
+
...runOptions,
|
|
40
|
+
registry,
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
validate(config) {
|
|
44
|
+
return validateConfig(config);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|