@tagma/sdk 0.4.14 → 0.4.16

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 (67) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +569 -569
  3. package/dist/dag.d.ts.map +1 -1
  4. package/dist/dag.js +58 -69
  5. package/dist/dag.js.map +1 -1
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +63 -37
  8. package/dist/engine.js.map +1 -1
  9. package/dist/middlewares/static-context.d.ts.map +1 -1
  10. package/dist/middlewares/static-context.js +7 -3
  11. package/dist/middlewares/static-context.js.map +1 -1
  12. package/dist/prompt-doc.d.ts +36 -0
  13. package/dist/prompt-doc.d.ts.map +1 -0
  14. package/dist/prompt-doc.js +44 -0
  15. package/dist/prompt-doc.js.map +1 -0
  16. package/dist/registry.d.ts.map +1 -1
  17. package/dist/registry.js +11 -0
  18. package/dist/registry.js.map +1 -1
  19. package/dist/sdk.d.ts +3 -0
  20. package/dist/sdk.d.ts.map +1 -1
  21. package/dist/sdk.js +4 -0
  22. package/dist/sdk.js.map +1 -1
  23. package/dist/task-ref.d.ts +55 -0
  24. package/dist/task-ref.d.ts.map +1 -0
  25. package/dist/task-ref.js +101 -0
  26. package/dist/task-ref.js.map +1 -0
  27. package/dist/task-ref.test.d.ts +2 -0
  28. package/dist/task-ref.test.d.ts.map +1 -0
  29. package/dist/task-ref.test.js +364 -0
  30. package/dist/task-ref.test.js.map +1 -0
  31. package/dist/templates.d.ts +20 -0
  32. package/dist/templates.d.ts.map +1 -0
  33. package/dist/templates.js +93 -0
  34. package/dist/templates.js.map +1 -0
  35. package/dist/validate-raw.d.ts.map +1 -1
  36. package/dist/validate-raw.js +27 -53
  37. package/dist/validate-raw.js.map +1 -1
  38. package/package.json +2 -2
  39. package/scripts/preinstall.js +31 -31
  40. package/src/adapters/stdin-approval.ts +106 -106
  41. package/src/adapters/websocket-approval.ts +224 -224
  42. package/src/approval.ts +131 -131
  43. package/src/bootstrap.ts +37 -37
  44. package/src/completions/exit-code.ts +34 -34
  45. package/src/completions/file-exists.ts +66 -66
  46. package/src/completions/output-check.ts +86 -86
  47. package/src/config-ops.ts +307 -307
  48. package/src/dag.ts +61 -67
  49. package/src/drivers/claude-code.ts +250 -250
  50. package/src/engine.ts +1137 -1098
  51. package/src/hooks.ts +187 -187
  52. package/src/logger.ts +182 -182
  53. package/src/middlewares/static-context.ts +49 -45
  54. package/src/pipeline-runner.ts +156 -156
  55. package/src/prompt-doc.ts +49 -0
  56. package/src/registry.ts +255 -242
  57. package/src/runner.ts +395 -395
  58. package/src/schema.test.ts +101 -101
  59. package/src/schema.ts +338 -338
  60. package/src/sdk.ts +111 -92
  61. package/src/task-ref.test.ts +401 -0
  62. package/src/task-ref.ts +120 -0
  63. package/src/triggers/file.ts +164 -164
  64. package/src/triggers/manual.ts +86 -86
  65. package/src/types.ts +18 -18
  66. package/src/utils.ts +203 -203
  67. package/src/validate-raw.ts +412 -442
package/src/utils.ts CHANGED
@@ -1,203 +1,203 @@
1
- import { resolve, relative, parse as parsePath } from 'path';
2
- import { realpathSync, lstatSync, existsSync } from 'fs';
3
- import { randomBytes } from 'crypto';
4
-
5
- const DURATION_RE = /^(\d*\.?\d+)\s*(s|m|h|d)$/;
6
-
7
- export function parseDuration(input: string): number {
8
- const match = DURATION_RE.exec(input.trim());
9
- if (!match) {
10
- throw new Error(`Invalid duration format: "${input}". Expected format: <number>(s|m|h|d)`);
11
- }
12
- const value = parseFloat(match[1]);
13
- const unit = match[2];
14
- switch (unit) {
15
- case 's':
16
- return value * 1000;
17
- case 'm':
18
- return value * 60_000;
19
- case 'h':
20
- return value * 3_600_000;
21
- case 'd':
22
- return value * 86_400_000;
23
- default:
24
- throw new Error(`Unknown duration unit: "${unit}"`);
25
- }
26
- }
27
-
28
- export function validatePath(filePath: string, projectRoot: string): string {
29
- const resolved = resolve(projectRoot, filePath);
30
-
31
- // D2: Cross-drive check (Windows) — path.relative('C:\\root', 'D:\\x') returns
32
- // 'D:\\x' which does NOT start with '..', so a pure relative check would wrongly
33
- // allow cross-drive paths. Reject them explicitly before any further comparison.
34
- if (parsePath(projectRoot).root !== parsePath(resolved).root) {
35
- throw new Error(
36
- `Security: path "${filePath}" is on a different drive than the project root "${projectRoot}".`,
37
- );
38
- }
39
-
40
- const rel = relative(projectRoot, resolved);
41
- if (rel.startsWith('..') || rel.startsWith('/')) {
42
- throw new Error(
43
- `Security: path "${filePath}" escapes project root. ` +
44
- `All file references must be within "${projectRoot}".`,
45
- );
46
- }
47
-
48
- // D1: Resolve symlinks and re-validate so a symlink whose string path is
49
- // inside the project root but whose target lies outside is rejected.
50
- // Only resolve if the path exists on disk; at parse time the file may not
51
- // yet exist (e.g. a future output path), so we skip realpath for absent paths.
52
- if (existsSync(resolved)) {
53
- // Reject the entry outright if it is itself a symlink — callers that want
54
- // to allow symlinks within the tree can pass pre-resolved paths.
55
- try {
56
- const stat = lstatSync(resolved);
57
- if (stat.isSymbolicLink()) {
58
- throw new Error(
59
- `Security: path "${filePath}" is a symbolic link. Symbolic links are not allowed within the project root.`,
60
- );
61
- }
62
- } catch (err) {
63
- if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
64
- }
65
-
66
- // Also verify the real (fully resolved) path stays within the project root.
67
- let real: string;
68
- try {
69
- real = realpathSync.native(resolved);
70
- } catch {
71
- real = resolved; // path vanished between existsSync and realpathSync — skip
72
- }
73
- const realRoot = (() => {
74
- try {
75
- return realpathSync.native(projectRoot);
76
- } catch {
77
- return projectRoot;
78
- }
79
- })();
80
- if (parsePath(realRoot).root !== parsePath(real).root) {
81
- throw new Error(
82
- `Security: resolved path "${real}" is on a different drive than the project root "${realRoot}".`,
83
- );
84
- }
85
- const realRel = relative(realRoot, real);
86
- if (realRel.startsWith('..') || realRel.startsWith('/')) {
87
- throw new Error(
88
- `Security: path "${filePath}" resolves via symlink to "${real}" which escapes project root "${realRoot}".`,
89
- );
90
- }
91
- }
92
-
93
- return resolved;
94
- }
95
-
96
- export function generateRunId(): string {
97
- const ts = Date.now().toString(36);
98
- const rand = randomBytes(6).toString('hex');
99
- return `run_${ts}_${rand}`;
100
- }
101
-
102
- export function truncateForName(text: string, maxLen = 40): string {
103
- const first = text.split('\n')[0]!.trim();
104
- // Guard: if the first line is empty (e.g. prompt is all whitespace/newlines),
105
- // fall back to the raw text trimmed rather than silently producing an empty name.
106
- if (!first) return text.trim().slice(0, maxLen) || '...';
107
- return first.length > maxLen ? first.slice(0, maxLen) + '...' : first;
108
- }
109
-
110
- export function nowISO(): string {
111
- return new Date().toISOString();
112
- }
113
-
114
- // ═══ Platform-aware shell ═══
115
- //
116
- // Resolution order:
117
- // 1. Env override: PIPELINE_SHELL="bash" or PIPELINE_SHELL="cmd" etc.
118
- // 2. Windows: prefer sh (Git Bash / MSYS2) if on PATH, fall back to cmd.exe
119
- // 3. Unix: sh
120
- //
121
- // Resolution is cached once on first call to avoid repeated PATH lookups.
122
-
123
- const IS_WINDOWS = process.platform === 'win32';
124
-
125
- type ShellKind = 'sh' | 'bash' | 'cmd' | 'powershell';
126
- let resolvedShell: { kind: ShellKind; path: string } | null = null;
127
-
128
- function detectShell(): { kind: ShellKind; path: string } {
129
- // Env override takes precedence
130
- const override = process.env.PIPELINE_SHELL;
131
- if (override) {
132
- const kind = override as ShellKind;
133
- return { kind, path: override };
134
- }
135
-
136
- if (!IS_WINDOWS) {
137
- return { kind: 'sh', path: 'sh' };
138
- }
139
-
140
- // Windows: probe PATH for sh (bundled with Git for Windows / MSYS2)
141
- const pathEnv = process.env.PATH ?? '';
142
- const pathExt = (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';');
143
- const dirs = pathEnv.split(';').filter(Boolean);
144
-
145
- for (const dir of dirs) {
146
- for (const ext of ['', ...pathExt]) {
147
- const candidate = `${dir}\\sh${ext}`;
148
- try {
149
- if (Bun.file(candidate).size > 0) {
150
- return { kind: 'sh', path: candidate };
151
- }
152
- } catch {
153
- /* ignore */
154
- }
155
- }
156
- }
157
-
158
- // Fallback: cmd.exe (always present on Windows)
159
- const systemRoot = process.env.SystemRoot ?? 'C:\\Windows';
160
- return { kind: 'cmd', path: `${systemRoot}\\System32\\cmd.exe` };
161
- }
162
-
163
- function getShell(): { kind: ShellKind; path: string } {
164
- if (!resolvedShell) resolvedShell = detectShell();
165
- return resolvedShell;
166
- }
167
-
168
- export function shellArgs(command: string): readonly string[] {
169
- const sh = getShell();
170
- if (sh.kind === 'cmd') {
171
- return [sh.path, '/c', command];
172
- }
173
- if (sh.kind === 'powershell') {
174
- return [sh.path, '-Command', command];
175
- }
176
- // sh or bash
177
- return [sh.path, '-c', command];
178
- }
179
-
180
- /** Quote a single argument for inclusion in a shell command string. */
181
- function quoteArg(arg: string): string {
182
- if (!/[\s"'\\<>|&;`$!^%]/.test(arg)) return arg;
183
- if (IS_WINDOWS) {
184
- // On Windows (cmd.exe), double-quote and escape embedded quotes + backslashes
185
- return '"' + arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
186
- }
187
- // On Unix, use single quotes to prevent $variable expansion.
188
- // Escape embedded single quotes via the '\'' idiom.
189
- return "'" + arg.replace(/'/g, "'\\''") + "'";
190
- }
191
-
192
- /**
193
- * Convert an args array to shell-wrapped args suitable for Bun.spawn.
194
- * Each arg is quoted as needed, then joined and passed through shellArgs.
195
- */
196
- export function shellArgsFromArray(args: readonly string[]): readonly string[] {
197
- return shellArgs(args.map(quoteArg).join(' '));
198
- }
199
-
200
- // For tests: allow resetting the cached shell detection
201
- export function _resetShellCache(): void {
202
- resolvedShell = null;
203
- }
1
+ import { resolve, relative, parse as parsePath } from 'path';
2
+ import { realpathSync, lstatSync, existsSync } from 'fs';
3
+ import { randomBytes } from 'crypto';
4
+
5
+ const DURATION_RE = /^(\d*\.?\d+)\s*(s|m|h|d)$/;
6
+
7
+ export function parseDuration(input: string): number {
8
+ const match = DURATION_RE.exec(input.trim());
9
+ if (!match) {
10
+ throw new Error(`Invalid duration format: "${input}". Expected format: <number>(s|m|h|d)`);
11
+ }
12
+ const value = parseFloat(match[1]);
13
+ const unit = match[2];
14
+ switch (unit) {
15
+ case 's':
16
+ return value * 1000;
17
+ case 'm':
18
+ return value * 60_000;
19
+ case 'h':
20
+ return value * 3_600_000;
21
+ case 'd':
22
+ return value * 86_400_000;
23
+ default:
24
+ throw new Error(`Unknown duration unit: "${unit}"`);
25
+ }
26
+ }
27
+
28
+ export function validatePath(filePath: string, projectRoot: string): string {
29
+ const resolved = resolve(projectRoot, filePath);
30
+
31
+ // D2: Cross-drive check (Windows) — path.relative('C:\\root', 'D:\\x') returns
32
+ // 'D:\\x' which does NOT start with '..', so a pure relative check would wrongly
33
+ // allow cross-drive paths. Reject them explicitly before any further comparison.
34
+ if (parsePath(projectRoot).root !== parsePath(resolved).root) {
35
+ throw new Error(
36
+ `Security: path "${filePath}" is on a different drive than the project root "${projectRoot}".`,
37
+ );
38
+ }
39
+
40
+ const rel = relative(projectRoot, resolved);
41
+ if (rel.startsWith('..') || rel.startsWith('/')) {
42
+ throw new Error(
43
+ `Security: path "${filePath}" escapes project root. ` +
44
+ `All file references must be within "${projectRoot}".`,
45
+ );
46
+ }
47
+
48
+ // D1: Resolve symlinks and re-validate so a symlink whose string path is
49
+ // inside the project root but whose target lies outside is rejected.
50
+ // Only resolve if the path exists on disk; at parse time the file may not
51
+ // yet exist (e.g. a future output path), so we skip realpath for absent paths.
52
+ if (existsSync(resolved)) {
53
+ // Reject the entry outright if it is itself a symlink — callers that want
54
+ // to allow symlinks within the tree can pass pre-resolved paths.
55
+ try {
56
+ const stat = lstatSync(resolved);
57
+ if (stat.isSymbolicLink()) {
58
+ throw new Error(
59
+ `Security: path "${filePath}" is a symbolic link. Symbolic links are not allowed within the project root.`,
60
+ );
61
+ }
62
+ } catch (err) {
63
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
64
+ }
65
+
66
+ // Also verify the real (fully resolved) path stays within the project root.
67
+ let real: string;
68
+ try {
69
+ real = realpathSync.native(resolved);
70
+ } catch {
71
+ real = resolved; // path vanished between existsSync and realpathSync — skip
72
+ }
73
+ const realRoot = (() => {
74
+ try {
75
+ return realpathSync.native(projectRoot);
76
+ } catch {
77
+ return projectRoot;
78
+ }
79
+ })();
80
+ if (parsePath(realRoot).root !== parsePath(real).root) {
81
+ throw new Error(
82
+ `Security: resolved path "${real}" is on a different drive than the project root "${realRoot}".`,
83
+ );
84
+ }
85
+ const realRel = relative(realRoot, real);
86
+ if (realRel.startsWith('..') || realRel.startsWith('/')) {
87
+ throw new Error(
88
+ `Security: path "${filePath}" resolves via symlink to "${real}" which escapes project root "${realRoot}".`,
89
+ );
90
+ }
91
+ }
92
+
93
+ return resolved;
94
+ }
95
+
96
+ export function generateRunId(): string {
97
+ const ts = Date.now().toString(36);
98
+ const rand = randomBytes(6).toString('hex');
99
+ return `run_${ts}_${rand}`;
100
+ }
101
+
102
+ export function truncateForName(text: string, maxLen = 40): string {
103
+ const first = text.split('\n')[0]!.trim();
104
+ // Guard: if the first line is empty (e.g. prompt is all whitespace/newlines),
105
+ // fall back to the raw text trimmed rather than silently producing an empty name.
106
+ if (!first) return text.trim().slice(0, maxLen) || '...';
107
+ return first.length > maxLen ? first.slice(0, maxLen) + '...' : first;
108
+ }
109
+
110
+ export function nowISO(): string {
111
+ return new Date().toISOString();
112
+ }
113
+
114
+ // ═══ Platform-aware shell ═══
115
+ //
116
+ // Resolution order:
117
+ // 1. Env override: PIPELINE_SHELL="bash" or PIPELINE_SHELL="cmd" etc.
118
+ // 2. Windows: prefer sh (Git Bash / MSYS2) if on PATH, fall back to cmd.exe
119
+ // 3. Unix: sh
120
+ //
121
+ // Resolution is cached once on first call to avoid repeated PATH lookups.
122
+
123
+ const IS_WINDOWS = process.platform === 'win32';
124
+
125
+ type ShellKind = 'sh' | 'bash' | 'cmd' | 'powershell';
126
+ let resolvedShell: { kind: ShellKind; path: string } | null = null;
127
+
128
+ function detectShell(): { kind: ShellKind; path: string } {
129
+ // Env override takes precedence
130
+ const override = process.env.PIPELINE_SHELL;
131
+ if (override) {
132
+ const kind = override as ShellKind;
133
+ return { kind, path: override };
134
+ }
135
+
136
+ if (!IS_WINDOWS) {
137
+ return { kind: 'sh', path: 'sh' };
138
+ }
139
+
140
+ // Windows: probe PATH for sh (bundled with Git for Windows / MSYS2)
141
+ const pathEnv = process.env.PATH ?? '';
142
+ const pathExt = (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';');
143
+ const dirs = pathEnv.split(';').filter(Boolean);
144
+
145
+ for (const dir of dirs) {
146
+ for (const ext of ['', ...pathExt]) {
147
+ const candidate = `${dir}\\sh${ext}`;
148
+ try {
149
+ if (Bun.file(candidate).size > 0) {
150
+ return { kind: 'sh', path: candidate };
151
+ }
152
+ } catch {
153
+ /* ignore */
154
+ }
155
+ }
156
+ }
157
+
158
+ // Fallback: cmd.exe (always present on Windows)
159
+ const systemRoot = process.env.SystemRoot ?? 'C:\\Windows';
160
+ return { kind: 'cmd', path: `${systemRoot}\\System32\\cmd.exe` };
161
+ }
162
+
163
+ function getShell(): { kind: ShellKind; path: string } {
164
+ if (!resolvedShell) resolvedShell = detectShell();
165
+ return resolvedShell;
166
+ }
167
+
168
+ export function shellArgs(command: string): readonly string[] {
169
+ const sh = getShell();
170
+ if (sh.kind === 'cmd') {
171
+ return [sh.path, '/c', command];
172
+ }
173
+ if (sh.kind === 'powershell') {
174
+ return [sh.path, '-Command', command];
175
+ }
176
+ // sh or bash
177
+ return [sh.path, '-c', command];
178
+ }
179
+
180
+ /** Quote a single argument for inclusion in a shell command string. */
181
+ function quoteArg(arg: string): string {
182
+ if (!/[\s"'\\<>|&;`$!^%]/.test(arg)) return arg;
183
+ if (IS_WINDOWS) {
184
+ // On Windows (cmd.exe), double-quote and escape embedded quotes + backslashes
185
+ return '"' + arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
186
+ }
187
+ // On Unix, use single quotes to prevent $variable expansion.
188
+ // Escape embedded single quotes via the '\'' idiom.
189
+ return "'" + arg.replace(/'/g, "'\\''") + "'";
190
+ }
191
+
192
+ /**
193
+ * Convert an args array to shell-wrapped args suitable for Bun.spawn.
194
+ * Each arg is quoted as needed, then joined and passed through shellArgs.
195
+ */
196
+ export function shellArgsFromArray(args: readonly string[]): readonly string[] {
197
+ return shellArgs(args.map(quoteArg).join(' '));
198
+ }
199
+
200
+ // For tests: allow resetting the cached shell detection
201
+ export function _resetShellCache(): void {
202
+ resolvedShell = null;
203
+ }