@tagma/sdk 0.4.9 → 0.4.11
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/LICENSE +21 -21
- package/dist/config-ops.js +1 -1
- package/dist/config-ops.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/hooks.js +1 -1
- package/dist/hooks.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +1 -0
- package/dist/logger.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +3 -0
- package/dist/runner.js.map +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/schema.test.js.map +1 -1
- package/dist/triggers/file.d.ts.map +1 -1
- package/dist/triggers/file.js +15 -3
- package/dist/triggers/file.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -6
- package/dist/utils.js.map +1 -1
- package/package.json +5 -3
- package/scripts/preinstall.js +31 -0
- package/src/config-ops.ts +1 -1
- package/src/dag.ts +17 -17
- package/src/engine.ts +2 -2
- package/src/hooks.ts +1 -1
- package/src/logger.ts +1 -0
- package/src/registry.ts +214 -214
- package/src/runner.ts +5 -1
- package/src/schema.test.ts +97 -97
- package/src/schema.ts +2 -2
- package/src/sdk.ts +2 -2
- package/src/triggers/file.ts +145 -129
- package/src/utils.ts +3 -8
- package/dist/templates.d.ts +0 -20
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js +0 -93
- package/dist/templates.js.map +0 -1
package/src/schema.test.ts
CHANGED
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import yaml from 'js-yaml';
|
|
3
|
-
import type { PipelineConfig, RawPipelineConfig } from './types';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
function parsePipelineYaml(content: string): RawPipelineConfig {
|
|
7
|
-
const doc = yaml.load(content) as { pipeline: RawPipelineConfig };
|
|
8
|
-
return doc.pipeline;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe('completion default serialization', () => {
|
|
12
|
-
test('serializePipeline omits default exit_code completions from raw configs', () => {
|
|
13
|
-
const raw: RawPipelineConfig = {
|
|
14
|
-
name: 'Serialize Defaults',
|
|
15
|
-
tracks: [
|
|
16
|
-
{
|
|
17
|
-
id: 'track_a',
|
|
18
|
-
name: 'Track A',
|
|
19
|
-
tasks: [
|
|
20
|
-
{ id: 'task_1', prompt: 'hello', completion: { type: 'exit_code' } },
|
|
21
|
-
{ id: 'task_2', prompt: 'world', completion: { type: 'exit_code', expect: 0 } },
|
|
22
|
-
{ id: 'task_3', prompt: 'keep me', completion: { type: 'exit_code', expect: 2 } },
|
|
23
|
-
],
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const parsed = parsePipelineYaml(serializePipeline(raw));
|
|
29
|
-
expect(parsed.tracks[0].tasks[0].completion).toBeUndefined();
|
|
30
|
-
expect(parsed.tracks[0].tasks[1].completion).toBeUndefined();
|
|
31
|
-
expect(parsed.tracks[0].tasks[2].completion).toEqual({ type: 'exit_code', expect: 2 });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('serializePipeline preserves non-default completion plugins', () => {
|
|
35
|
-
const raw: RawPipelineConfig = {
|
|
36
|
-
name: 'Serialize Explicit',
|
|
37
|
-
tracks: [
|
|
38
|
-
{
|
|
39
|
-
id: 'track_a',
|
|
40
|
-
name: 'Track A',
|
|
41
|
-
tasks: [
|
|
42
|
-
{ id: 'task_1', prompt: 'check file', completion: { type: 'file_exists', path: './out.txt' } },
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
],
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const parsed = parsePipelineYaml(serializePipeline(raw));
|
|
49
|
-
expect(parsed.tracks[0].tasks[0].completion).toEqual({
|
|
50
|
-
type: 'file_exists',
|
|
51
|
-
path: './out.txt',
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('deresolvePipeline also omits the default exit_code completion', () => {
|
|
56
|
-
const resolved: PipelineConfig = {
|
|
57
|
-
name: 'Deresolve Defaults',
|
|
58
|
-
tracks: [
|
|
59
|
-
{
|
|
60
|
-
id: 'track_a',
|
|
61
|
-
name: 'Track A',
|
|
62
|
-
driver: 'claude-code',
|
|
63
|
-
permissions: { read: true, write: false, execute: false },
|
|
64
|
-
on_failure: 'skip_downstream',
|
|
65
|
-
cwd: 'D:/workspace',
|
|
66
|
-
tasks: [
|
|
67
|
-
{
|
|
68
|
-
id: 'task_1',
|
|
69
|
-
name: 'Task 1',
|
|
70
|
-
prompt: 'hello',
|
|
71
|
-
driver: 'claude-code',
|
|
72
|
-
permissions: { read: true, write: false, execute: false },
|
|
73
|
-
cwd: 'D:/workspace',
|
|
74
|
-
completion: { type: 'exit_code', expect: 0 },
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
id: 'task_2',
|
|
78
|
-
name: 'Task 2',
|
|
79
|
-
prompt: 'custom',
|
|
80
|
-
driver: 'claude-code',
|
|
81
|
-
permissions: { read: true, write: false, execute: false },
|
|
82
|
-
cwd: 'D:/workspace',
|
|
83
|
-
completion: { type: 'output_check', check: 'test -f ./done.txt' },
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const raw = deresolvePipeline(resolved, 'D:/workspace');
|
|
91
|
-
expect(raw.tracks[0].tasks[0].completion).toBeUndefined();
|
|
92
|
-
expect(raw.tracks[0].tasks[1].completion).toEqual({
|
|
93
|
-
type: 'output_check',
|
|
94
|
-
check: 'test -f ./done.txt',
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
});
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
import type { PipelineConfig, RawPipelineConfig } from './types';
|
|
4
|
+
import { deresolvePipeline, serializePipeline } from './schema';
|
|
5
|
+
|
|
6
|
+
function parsePipelineYaml(content: string): RawPipelineConfig {
|
|
7
|
+
const doc = yaml.load(content) as { pipeline: RawPipelineConfig };
|
|
8
|
+
return doc.pipeline;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('completion default serialization', () => {
|
|
12
|
+
test('serializePipeline omits default exit_code completions from raw configs', () => {
|
|
13
|
+
const raw: RawPipelineConfig = {
|
|
14
|
+
name: 'Serialize Defaults',
|
|
15
|
+
tracks: [
|
|
16
|
+
{
|
|
17
|
+
id: 'track_a',
|
|
18
|
+
name: 'Track A',
|
|
19
|
+
tasks: [
|
|
20
|
+
{ id: 'task_1', prompt: 'hello', completion: { type: 'exit_code' } },
|
|
21
|
+
{ id: 'task_2', prompt: 'world', completion: { type: 'exit_code', expect: 0 } },
|
|
22
|
+
{ id: 'task_3', prompt: 'keep me', completion: { type: 'exit_code', expect: 2 } },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const parsed = parsePipelineYaml(serializePipeline(raw));
|
|
29
|
+
expect(parsed.tracks[0].tasks[0].completion).toBeUndefined();
|
|
30
|
+
expect(parsed.tracks[0].tasks[1].completion).toBeUndefined();
|
|
31
|
+
expect(parsed.tracks[0].tasks[2].completion).toEqual({ type: 'exit_code', expect: 2 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('serializePipeline preserves non-default completion plugins', () => {
|
|
35
|
+
const raw: RawPipelineConfig = {
|
|
36
|
+
name: 'Serialize Explicit',
|
|
37
|
+
tracks: [
|
|
38
|
+
{
|
|
39
|
+
id: 'track_a',
|
|
40
|
+
name: 'Track A',
|
|
41
|
+
tasks: [
|
|
42
|
+
{ id: 'task_1', prompt: 'check file', completion: { type: 'file_exists', path: './out.txt' } },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const parsed = parsePipelineYaml(serializePipeline(raw));
|
|
49
|
+
expect(parsed.tracks[0].tasks[0].completion).toEqual({
|
|
50
|
+
type: 'file_exists',
|
|
51
|
+
path: './out.txt',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('deresolvePipeline also omits the default exit_code completion', () => {
|
|
56
|
+
const resolved: PipelineConfig = {
|
|
57
|
+
name: 'Deresolve Defaults',
|
|
58
|
+
tracks: [
|
|
59
|
+
{
|
|
60
|
+
id: 'track_a',
|
|
61
|
+
name: 'Track A',
|
|
62
|
+
driver: 'claude-code',
|
|
63
|
+
permissions: { read: true, write: false, execute: false },
|
|
64
|
+
on_failure: 'skip_downstream',
|
|
65
|
+
cwd: 'D:/workspace',
|
|
66
|
+
tasks: [
|
|
67
|
+
{
|
|
68
|
+
id: 'task_1',
|
|
69
|
+
name: 'Task 1',
|
|
70
|
+
prompt: 'hello',
|
|
71
|
+
driver: 'claude-code',
|
|
72
|
+
permissions: { read: true, write: false, execute: false },
|
|
73
|
+
cwd: 'D:/workspace',
|
|
74
|
+
completion: { type: 'exit_code', expect: 0 },
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'task_2',
|
|
78
|
+
name: 'Task 2',
|
|
79
|
+
prompt: 'custom',
|
|
80
|
+
driver: 'claude-code',
|
|
81
|
+
permissions: { read: true, write: false, execute: false },
|
|
82
|
+
cwd: 'D:/workspace',
|
|
83
|
+
completion: { type: 'output_check', check: 'test -f ./done.txt' },
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const raw = deresolvePipeline(resolved, 'D:/workspace');
|
|
91
|
+
expect(raw.tracks[0].tasks[0].completion).toBeUndefined();
|
|
92
|
+
expect(raw.tracks[0].tasks[1].completion).toEqual({
|
|
93
|
+
type: 'output_check',
|
|
94
|
+
check: 'test -f ./done.txt',
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
package/src/schema.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import yaml from 'js-yaml';
|
|
2
|
-
import {
|
|
2
|
+
import { relative } from 'path';
|
|
3
3
|
import type {
|
|
4
4
|
PipelineConfig, RawPipelineConfig, RawTrackConfig, RawTaskConfig,
|
|
5
|
-
TrackConfig, TaskConfig, Permissions,
|
|
5
|
+
TrackConfig, TaskConfig, Permissions, CompletionConfig,
|
|
6
6
|
} from './types';
|
|
7
7
|
import { truncateForName, validatePath } from './utils';
|
|
8
8
|
import { DEFAULT_PERMISSIONS } from './types';
|
package/src/sdk.ts
CHANGED
|
@@ -29,8 +29,8 @@ export {
|
|
|
29
29
|
export { validateRaw } from './validate-raw';
|
|
30
30
|
export type { ValidationError } from './validate-raw';
|
|
31
31
|
|
|
32
|
-
// ── Schema: parse / resolve / load / serialize / validate ──
|
|
33
|
-
export { parseYaml, resolveConfig, loadPipeline, serializePipeline, deresolvePipeline, validateConfig } from './schema';
|
|
32
|
+
// ── Schema: parse / resolve / load / serialize / validate ──
|
|
33
|
+
export { parseYaml, resolveConfig, loadPipeline, serializePipeline, deresolvePipeline, validateConfig } from './schema';
|
|
34
34
|
|
|
35
35
|
// ── DAG ──
|
|
36
36
|
export { buildDag, buildRawDag } from './dag';
|
package/src/triggers/file.ts
CHANGED
|
@@ -1,129 +1,145 @@
|
|
|
1
|
-
import { watch } from 'chokidar';
|
|
2
|
-
import { resolve, dirname } from 'path';
|
|
3
|
-
import { mkdir } from 'fs/promises';
|
|
4
|
-
import type { TriggerPlugin, TriggerContext } from '../types';
|
|
5
|
-
import { parseDuration, validatePath } from '../utils';
|
|
6
|
-
import { TriggerTimeoutError } from '../engine';
|
|
7
|
-
|
|
8
|
-
const IS_WINDOWS = process.platform === 'win32';
|
|
9
|
-
|
|
10
|
-
function pathsEqual(a: string, b: string): boolean {
|
|
11
|
-
return IS_WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const FileTrigger: TriggerPlugin = {
|
|
15
|
-
name: 'file',
|
|
16
|
-
schema: {
|
|
17
|
-
description: 'Wait for a file to appear or be modified before the task runs.',
|
|
18
|
-
fields: {
|
|
19
|
-
path: {
|
|
20
|
-
type: 'path',
|
|
21
|
-
required: true,
|
|
22
|
-
description: 'Path to the file to watch (relative to workDir or absolute).',
|
|
23
|
-
placeholder: 'e.g. build/output.json',
|
|
24
|
-
},
|
|
25
|
-
timeout: {
|
|
26
|
-
type: 'duration',
|
|
27
|
-
description: 'Maximum wait time (e.g. 30s, 5m). Omit or 0 to wait indefinitely.',
|
|
28
|
-
placeholder: '30s',
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
watch(config: Record<string, unknown>, ctx: TriggerContext): Promise<unknown> {
|
|
34
|
-
const filePath = config.path as string;
|
|
35
|
-
if (!filePath) throw new Error(`file trigger: "path" is required`);
|
|
36
|
-
|
|
37
|
-
const safePath = validatePath(filePath, ctx.workDir);
|
|
38
|
-
const timeoutMs = config.timeout != null ? parseDuration(String(config.timeout)) : 0;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
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
|
-
|
|
1
|
+
import { watch } from 'chokidar';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { mkdir } from 'fs/promises';
|
|
4
|
+
import type { TriggerPlugin, TriggerContext } from '../types';
|
|
5
|
+
import { parseDuration, validatePath } from '../utils';
|
|
6
|
+
import { TriggerTimeoutError } from '../engine';
|
|
7
|
+
|
|
8
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
9
|
+
|
|
10
|
+
function pathsEqual(a: string, b: string): boolean {
|
|
11
|
+
return IS_WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const FileTrigger: TriggerPlugin = {
|
|
15
|
+
name: 'file',
|
|
16
|
+
schema: {
|
|
17
|
+
description: 'Wait for a file to appear or be modified before the task runs.',
|
|
18
|
+
fields: {
|
|
19
|
+
path: {
|
|
20
|
+
type: 'path',
|
|
21
|
+
required: true,
|
|
22
|
+
description: 'Path to the file to watch (relative to workDir or absolute).',
|
|
23
|
+
placeholder: 'e.g. build/output.json',
|
|
24
|
+
},
|
|
25
|
+
timeout: {
|
|
26
|
+
type: 'duration',
|
|
27
|
+
description: 'Maximum wait time (e.g. 30s, 5m). Omit or 0 to wait indefinitely.',
|
|
28
|
+
placeholder: '30s',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
watch(config: Record<string, unknown>, ctx: TriggerContext): Promise<unknown> {
|
|
34
|
+
const filePath = config.path as string;
|
|
35
|
+
if (!filePath) throw new Error(`file trigger: "path" is required`);
|
|
36
|
+
|
|
37
|
+
const safePath = validatePath(filePath, ctx.workDir);
|
|
38
|
+
const timeoutMs = config.timeout != null ? parseDuration(String(config.timeout)) : 0;
|
|
39
|
+
|
|
40
|
+
// Hoist the async work into a named async function so the Promise
|
|
41
|
+
// constructor itself is synchronous — avoids the no-async-promise-executor
|
|
42
|
+
// lint error and ensures exceptions are always propagated via reject().
|
|
43
|
+
async function start(
|
|
44
|
+
resolve_p: (value: unknown) => void,
|
|
45
|
+
reject: (reason?: unknown) => void,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
if (ctx.signal.aborted) {
|
|
48
|
+
reject(new Error('Pipeline aborted'));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let settled = false;
|
|
53
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
54
|
+
|
|
55
|
+
// Ensure the parent directory exists so the watcher doesn't fail
|
|
56
|
+
// with ENOENT for nested paths like `build/output/result.json`.
|
|
57
|
+
const dir = dirname(safePath);
|
|
58
|
+
try {
|
|
59
|
+
await mkdir(dir, { recursive: true });
|
|
60
|
+
} catch { /* best effort — dir may already exist */ }
|
|
61
|
+
|
|
62
|
+
// Pass `cwd: dir` so chokidar resolves paths relative to the watched
|
|
63
|
+
// directory. The 'add'/'change' events will then carry paths relative
|
|
64
|
+
// to `dir`, which we resolve with `resolve(dir, addedPath)` for an
|
|
65
|
+
// accurate absolute comparison — fixing the ambiguous process.cwd()
|
|
66
|
+
// resolution of the previous implementation.
|
|
67
|
+
const watcher = watch(dir, {
|
|
68
|
+
ignoreInitial: true,
|
|
69
|
+
depth: 0,
|
|
70
|
+
cwd: dir,
|
|
71
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const cleanup = () => {
|
|
75
|
+
if (settled) return;
|
|
76
|
+
settled = true;
|
|
77
|
+
watcher.close().catch(() => { /* ignore */ });
|
|
78
|
+
if (timer) clearTimeout(timer);
|
|
79
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const onAbort = () => {
|
|
83
|
+
cleanup();
|
|
84
|
+
reject(new Error('Pipeline aborted'));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
watcher.on('add', (addedPath: string) => {
|
|
88
|
+
if (settled) return;
|
|
89
|
+
if (pathsEqual(resolve(dir, addedPath), safePath)) {
|
|
90
|
+
cleanup();
|
|
91
|
+
resolve_p({ path: safePath });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Also fire on 'change' so that overwriting an existing file is detected.
|
|
96
|
+
// Without this, upstream tasks that truncate-and-rewrite a file emit only
|
|
97
|
+
// a 'change' event and the downstream trigger would never resolve.
|
|
98
|
+
watcher.on('change', (changedPath: string) => {
|
|
99
|
+
if (settled) return;
|
|
100
|
+
if (pathsEqual(resolve(dir, changedPath), safePath)) {
|
|
101
|
+
cleanup();
|
|
102
|
+
resolve_p({ path: safePath });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
watcher.on('error', (err: unknown) => {
|
|
107
|
+
if (settled) return;
|
|
108
|
+
cleanup();
|
|
109
|
+
reject(new Error(`file trigger watch error: ${err instanceof Error ? err.message : String(err)}`));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// After the watcher finishes its initial scan, check if the file already exists.
|
|
113
|
+
// Doing this inside 'ready' eliminates the race window between existence check
|
|
114
|
+
// and watcher startup, so we neither miss events nor double-resolve.
|
|
115
|
+
watcher.on('ready', () => {
|
|
116
|
+
if (settled) return;
|
|
117
|
+
Bun.file(safePath).exists().then((exists) => {
|
|
118
|
+
if (settled) return;
|
|
119
|
+
if (exists) {
|
|
120
|
+
cleanup();
|
|
121
|
+
resolve_p({ path: safePath });
|
|
122
|
+
}
|
|
123
|
+
}).catch((err: unknown) => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
cleanup();
|
|
126
|
+
reject(new Error(`file trigger existence check failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (timeoutMs > 0) {
|
|
131
|
+
timer = setTimeout(() => {
|
|
132
|
+
if (settled) return;
|
|
133
|
+
cleanup();
|
|
134
|
+
reject(new TriggerTimeoutError(`file trigger timeout: ${filePath} did not appear within ${config.timeout}`));
|
|
135
|
+
}, timeoutMs);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
ctx.signal.addEventListener('abort', onAbort);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return new Promise((resolve_p, reject) => {
|
|
142
|
+
start(resolve_p, reject).catch(reject);
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
};
|
package/src/utils.ts
CHANGED
|
@@ -33,16 +33,11 @@ export function validatePath(filePath: string, projectRoot: string): string {
|
|
|
33
33
|
return resolved;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
let runCounter = 0;
|
|
37
|
-
|
|
38
36
|
export function generateRunId(): string {
|
|
39
37
|
const ts = Date.now().toString(36);
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const rand = randomBytes(3).toString('hex');
|
|
44
|
-
return `run_${ts}_${seq}_${rand}`;
|
|
45
|
-
}
|
|
38
|
+
const rand = randomBytes(6).toString('hex');
|
|
39
|
+
return `run_${ts}_${rand}`;
|
|
40
|
+
}
|
|
46
41
|
|
|
47
42
|
export function truncateForName(text: string, maxLen = 40): string {
|
|
48
43
|
const first = text.split('\n')[0]!.trim();
|
package/dist/templates.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { TemplateConfig } from './types';
|
|
2
|
-
export interface TemplateManifest extends TemplateConfig {
|
|
3
|
-
/** The package ref as it would appear in `task.use`, e.g. `@tagma/template-review`. */
|
|
4
|
-
readonly ref: string;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Scan the workspace's `node_modules/@tagma/*` for packages whose name starts
|
|
8
|
-
* with `template-` and load each one's manifest. Packages without a valid
|
|
9
|
-
* `template.yaml` (or that fail to parse) are silently skipped.
|
|
10
|
-
*
|
|
11
|
-
* Returns an empty array when `workDir` doesn't exist or has no such packages.
|
|
12
|
-
*/
|
|
13
|
-
export declare function discoverTemplates(workDir: string): TemplateManifest[];
|
|
14
|
-
/**
|
|
15
|
-
* Load a single template's manifest by its ref (e.g. `@tagma/template-review`)
|
|
16
|
-
* from the given workspace's `node_modules`. Returns `null` if the package
|
|
17
|
-
* isn't installed or its manifest can't be parsed.
|
|
18
|
-
*/
|
|
19
|
-
export declare function loadTemplateManifest(ref: string, workDir: string): TemplateManifest | null;
|
|
20
|
-
//# sourceMappingURL=templates.d.ts.map
|
package/dist/templates.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,uFAAuF;IACvF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CA8BrE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAO1F"}
|
package/dist/templates.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
// ═══ Template Discovery (F1) ═══
|
|
2
|
-
//
|
|
3
|
-
// Public helpers so editors / UIs can enumerate installed `@tagma/template-*`
|
|
4
|
-
// packages in a workspace and read each template's declarative metadata
|
|
5
|
-
// (name, description, params) without actually expanding the template.
|
|
6
|
-
//
|
|
7
|
-
// The legacy private `loadTemplate` in schema.ts uses Bun-specific APIs
|
|
8
|
-
// (Bun.file, require.resolve). These helpers are Node-compatible because
|
|
9
|
-
// the editor server runs on Node, not Bun.
|
|
10
|
-
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
11
|
-
import { join } from 'path';
|
|
12
|
-
import yaml from 'js-yaml';
|
|
13
|
-
/**
|
|
14
|
-
* Scan the workspace's `node_modules/@tagma/*` for packages whose name starts
|
|
15
|
-
* with `template-` and load each one's manifest. Packages without a valid
|
|
16
|
-
* `template.yaml` (or that fail to parse) are silently skipped.
|
|
17
|
-
*
|
|
18
|
-
* Returns an empty array when `workDir` doesn't exist or has no such packages.
|
|
19
|
-
*/
|
|
20
|
-
export function discoverTemplates(workDir) {
|
|
21
|
-
const out = [];
|
|
22
|
-
const scopeDir = join(workDir, 'node_modules', '@tagma');
|
|
23
|
-
if (!existsSync(scopeDir))
|
|
24
|
-
return out;
|
|
25
|
-
let entries = [];
|
|
26
|
-
try {
|
|
27
|
-
entries = readdirSync(scopeDir);
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
|
-
for (const entry of entries) {
|
|
33
|
-
if (!entry.startsWith('template-'))
|
|
34
|
-
continue;
|
|
35
|
-
const pkgDir = join(scopeDir, entry);
|
|
36
|
-
try {
|
|
37
|
-
const st = statSync(pkgDir);
|
|
38
|
-
if (!st.isDirectory())
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
const ref = `@tagma/${entry}`;
|
|
45
|
-
const manifest = loadTemplateManifestFromDir(pkgDir, ref);
|
|
46
|
-
if (manifest)
|
|
47
|
-
out.push(manifest);
|
|
48
|
-
}
|
|
49
|
-
// Sort alphabetically for deterministic UI rendering.
|
|
50
|
-
out.sort((a, b) => a.ref.localeCompare(b.ref));
|
|
51
|
-
return out;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Load a single template's manifest by its ref (e.g. `@tagma/template-review`)
|
|
55
|
-
* from the given workspace's `node_modules`. Returns `null` if the package
|
|
56
|
-
* isn't installed or its manifest can't be parsed.
|
|
57
|
-
*/
|
|
58
|
-
export function loadTemplateManifest(ref, workDir) {
|
|
59
|
-
// Only @tagma/template-* refs are supported (matches SDK validateTemplateRef).
|
|
60
|
-
const stripped = ref.replace(/@v\d+$/, '');
|
|
61
|
-
if (!stripped.startsWith('@tagma/template-'))
|
|
62
|
-
return null;
|
|
63
|
-
const pkgDir = join(workDir, 'node_modules', stripped);
|
|
64
|
-
if (!existsSync(pkgDir))
|
|
65
|
-
return null;
|
|
66
|
-
return loadTemplateManifestFromDir(pkgDir, stripped);
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Resolve a template manifest from an absolute package directory. Tries
|
|
70
|
-
* `template.yaml` first (the documented convention), then a `template` export
|
|
71
|
-
* from `package.json`'s `main`. Returns `null` on any failure so discovery
|
|
72
|
-
* stays robust against malformed packages.
|
|
73
|
-
*/
|
|
74
|
-
function loadTemplateManifestFromDir(pkgDir, ref) {
|
|
75
|
-
const yamlPath = join(pkgDir, 'template.yaml');
|
|
76
|
-
if (existsSync(yamlPath)) {
|
|
77
|
-
try {
|
|
78
|
-
const content = readFileSync(yamlPath, 'utf-8');
|
|
79
|
-
const doc = yaml.load(content);
|
|
80
|
-
const tpl = (doc && typeof doc === 'object' && 'template' in doc
|
|
81
|
-
? doc.template
|
|
82
|
-
: doc);
|
|
83
|
-
if (tpl && typeof tpl === 'object' && tpl.name && Array.isArray(tpl.tasks)) {
|
|
84
|
-
return { ...tpl, ref };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
//# sourceMappingURL=templates.js.map
|
package/dist/templates.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"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"}
|