@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.
- package/LICENSE +21 -21
- package/README.md +569 -569
- package/dist/dag.d.ts.map +1 -1
- package/dist/dag.js +58 -69
- package/dist/dag.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +63 -37
- package/dist/engine.js.map +1 -1
- package/dist/middlewares/static-context.d.ts.map +1 -1
- package/dist/middlewares/static-context.js +7 -3
- package/dist/middlewares/static-context.js.map +1 -1
- package/dist/prompt-doc.d.ts +36 -0
- package/dist/prompt-doc.d.ts.map +1 -0
- package/dist/prompt-doc.js +44 -0
- package/dist/prompt-doc.js.map +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +11 -0
- package/dist/registry.js.map +1 -1
- package/dist/sdk.d.ts +3 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +4 -0
- package/dist/sdk.js.map +1 -1
- package/dist/task-ref.d.ts +55 -0
- package/dist/task-ref.d.ts.map +1 -0
- package/dist/task-ref.js +101 -0
- package/dist/task-ref.js.map +1 -0
- package/dist/task-ref.test.d.ts +2 -0
- package/dist/task-ref.test.d.ts.map +1 -0
- package/dist/task-ref.test.js +364 -0
- package/dist/task-ref.test.js.map +1 -0
- package/dist/templates.d.ts +20 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +93 -0
- package/dist/templates.js.map +1 -0
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +27 -53
- package/dist/validate-raw.js.map +1 -1
- package/package.json +2 -2
- package/scripts/preinstall.js +31 -31
- package/src/adapters/stdin-approval.ts +106 -106
- package/src/adapters/websocket-approval.ts +224 -224
- package/src/approval.ts +131 -131
- package/src/bootstrap.ts +37 -37
- package/src/completions/exit-code.ts +34 -34
- package/src/completions/file-exists.ts +66 -66
- package/src/completions/output-check.ts +86 -86
- package/src/config-ops.ts +307 -307
- package/src/dag.ts +61 -67
- package/src/drivers/claude-code.ts +250 -250
- package/src/engine.ts +1137 -1098
- package/src/hooks.ts +187 -187
- package/src/logger.ts +182 -182
- package/src/middlewares/static-context.ts +49 -45
- package/src/pipeline-runner.ts +156 -156
- package/src/prompt-doc.ts +49 -0
- package/src/registry.ts +255 -242
- package/src/runner.ts +395 -395
- package/src/schema.test.ts +101 -101
- package/src/schema.ts +338 -338
- package/src/sdk.ts +111 -92
- package/src/task-ref.test.ts +401 -0
- package/src/task-ref.ts +120 -0
- package/src/triggers/file.ts +164 -164
- package/src/triggers/manual.ts +86 -86
- package/src/types.ts +18 -18
- package/src/utils.ts +203 -203
- package/src/validate-raw.ts +412 -442
package/src/approval.ts
CHANGED
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
import { randomUUID } from 'crypto';
|
|
2
|
-
import { nowISO } from './utils';
|
|
3
|
-
|
|
4
|
-
// Approval types (ApprovalRequest, ApprovalDecision, ApprovalOutcome,
|
|
5
|
-
// ApprovalEvent, ApprovalListener, ApprovalGateway) live in the shared
|
|
6
|
-
// @tagma/types package so trigger plugins can import them without
|
|
7
|
-
// depending on the engine's runtime implementation. This module keeps
|
|
8
|
-
// only the in-memory implementation. Internal SDK imports go through
|
|
9
|
-
// ./types (the engine-side re-export) for consistency with the rest of
|
|
10
|
-
// the SDK source.
|
|
11
|
-
import type {
|
|
12
|
-
ApprovalRequest,
|
|
13
|
-
ApprovalDecision,
|
|
14
|
-
ApprovalEvent,
|
|
15
|
-
ApprovalListener,
|
|
16
|
-
ApprovalGateway,
|
|
17
|
-
} from './types';
|
|
18
|
-
|
|
19
|
-
// Re-export for existing engine-side consumers that import from this file.
|
|
20
|
-
export type {
|
|
21
|
-
ApprovalRequest,
|
|
22
|
-
ApprovalDecision,
|
|
23
|
-
ApprovalOutcome,
|
|
24
|
-
ApprovalEvent,
|
|
25
|
-
ApprovalListener,
|
|
26
|
-
ApprovalGateway,
|
|
27
|
-
} from './types';
|
|
28
|
-
|
|
29
|
-
// ═══ Default In-Memory Implementation ═══
|
|
30
|
-
|
|
31
|
-
interface PendingEntry {
|
|
32
|
-
readonly request: ApprovalRequest;
|
|
33
|
-
readonly settle: (decision: ApprovalDecision) => void;
|
|
34
|
-
readonly timer: ReturnType<typeof setTimeout> | null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export class InMemoryApprovalGateway implements ApprovalGateway {
|
|
38
|
-
private readonly pendingMap = new Map<string, PendingEntry>();
|
|
39
|
-
private readonly listeners = new Set<ApprovalListener>();
|
|
40
|
-
|
|
41
|
-
request(req: Omit<ApprovalRequest, 'id' | 'createdAt'>): Promise<ApprovalDecision> {
|
|
42
|
-
const full: ApprovalRequest = {
|
|
43
|
-
id: randomUUID(),
|
|
44
|
-
createdAt: nowISO(),
|
|
45
|
-
taskId: req.taskId,
|
|
46
|
-
trackId: req.trackId,
|
|
47
|
-
message: req.message,
|
|
48
|
-
timeoutMs: req.timeoutMs,
|
|
49
|
-
metadata: req.metadata,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
return new Promise<ApprovalDecision>((resolvePromise) => {
|
|
53
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
54
|
-
if (full.timeoutMs > 0) {
|
|
55
|
-
timer = setTimeout(() => {
|
|
56
|
-
const entry = this.pendingMap.get(full.id);
|
|
57
|
-
if (!entry) return;
|
|
58
|
-
this.pendingMap.delete(full.id);
|
|
59
|
-
const decision: ApprovalDecision = {
|
|
60
|
-
approvalId: full.id,
|
|
61
|
-
outcome: 'timeout',
|
|
62
|
-
reason: `Approval timed out after ${full.timeoutMs}ms`,
|
|
63
|
-
decidedAt: nowISO(),
|
|
64
|
-
};
|
|
65
|
-
this.emit({ type: 'expired', request: full });
|
|
66
|
-
resolvePromise(decision);
|
|
67
|
-
}, full.timeoutMs);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
this.pendingMap.set(full.id, { request: full, settle: resolvePromise, timer });
|
|
71
|
-
this.emit({ type: 'requested', request: full });
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
resolve(
|
|
76
|
-
approvalId: string,
|
|
77
|
-
decision: Omit<ApprovalDecision, 'approvalId' | 'decidedAt'>,
|
|
78
|
-
): boolean {
|
|
79
|
-
const entry = this.pendingMap.get(approvalId);
|
|
80
|
-
if (!entry) return false;
|
|
81
|
-
this.pendingMap.delete(approvalId);
|
|
82
|
-
if (entry.timer) clearTimeout(entry.timer);
|
|
83
|
-
|
|
84
|
-
const full: ApprovalDecision = {
|
|
85
|
-
approvalId,
|
|
86
|
-
outcome: decision.outcome,
|
|
87
|
-
actor: decision.actor,
|
|
88
|
-
reason: decision.reason,
|
|
89
|
-
decidedAt: nowISO(),
|
|
90
|
-
};
|
|
91
|
-
this.emit({ type: 'resolved', request: entry.request, decision: full });
|
|
92
|
-
entry.settle(full);
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
pending(): readonly ApprovalRequest[] {
|
|
97
|
-
return Array.from(this.pendingMap.values()).map((e) => e.request);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
subscribe(listener: ApprovalListener): () => void {
|
|
101
|
-
this.listeners.add(listener);
|
|
102
|
-
return () => {
|
|
103
|
-
this.listeners.delete(listener);
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
abortAll(reason: string): void {
|
|
108
|
-
const entries = Array.from(this.pendingMap.entries());
|
|
109
|
-
this.pendingMap.clear();
|
|
110
|
-
for (const [id, entry] of entries) {
|
|
111
|
-
if (entry.timer) clearTimeout(entry.timer);
|
|
112
|
-
this.emit({ type: 'aborted', request: entry.request, reason });
|
|
113
|
-
entry.settle({
|
|
114
|
-
approvalId: id,
|
|
115
|
-
outcome: 'aborted',
|
|
116
|
-
reason,
|
|
117
|
-
decidedAt: nowISO(),
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private emit(event: ApprovalEvent): void {
|
|
123
|
-
for (const listener of this.listeners) {
|
|
124
|
-
try {
|
|
125
|
-
listener(event);
|
|
126
|
-
} catch (err) {
|
|
127
|
-
console.error('[approval gateway] listener error:', err);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { nowISO } from './utils';
|
|
3
|
+
|
|
4
|
+
// Approval types (ApprovalRequest, ApprovalDecision, ApprovalOutcome,
|
|
5
|
+
// ApprovalEvent, ApprovalListener, ApprovalGateway) live in the shared
|
|
6
|
+
// @tagma/types package so trigger plugins can import them without
|
|
7
|
+
// depending on the engine's runtime implementation. This module keeps
|
|
8
|
+
// only the in-memory implementation. Internal SDK imports go through
|
|
9
|
+
// ./types (the engine-side re-export) for consistency with the rest of
|
|
10
|
+
// the SDK source.
|
|
11
|
+
import type {
|
|
12
|
+
ApprovalRequest,
|
|
13
|
+
ApprovalDecision,
|
|
14
|
+
ApprovalEvent,
|
|
15
|
+
ApprovalListener,
|
|
16
|
+
ApprovalGateway,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
// Re-export for existing engine-side consumers that import from this file.
|
|
20
|
+
export type {
|
|
21
|
+
ApprovalRequest,
|
|
22
|
+
ApprovalDecision,
|
|
23
|
+
ApprovalOutcome,
|
|
24
|
+
ApprovalEvent,
|
|
25
|
+
ApprovalListener,
|
|
26
|
+
ApprovalGateway,
|
|
27
|
+
} from './types';
|
|
28
|
+
|
|
29
|
+
// ═══ Default In-Memory Implementation ═══
|
|
30
|
+
|
|
31
|
+
interface PendingEntry {
|
|
32
|
+
readonly request: ApprovalRequest;
|
|
33
|
+
readonly settle: (decision: ApprovalDecision) => void;
|
|
34
|
+
readonly timer: ReturnType<typeof setTimeout> | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class InMemoryApprovalGateway implements ApprovalGateway {
|
|
38
|
+
private readonly pendingMap = new Map<string, PendingEntry>();
|
|
39
|
+
private readonly listeners = new Set<ApprovalListener>();
|
|
40
|
+
|
|
41
|
+
request(req: Omit<ApprovalRequest, 'id' | 'createdAt'>): Promise<ApprovalDecision> {
|
|
42
|
+
const full: ApprovalRequest = {
|
|
43
|
+
id: randomUUID(),
|
|
44
|
+
createdAt: nowISO(),
|
|
45
|
+
taskId: req.taskId,
|
|
46
|
+
trackId: req.trackId,
|
|
47
|
+
message: req.message,
|
|
48
|
+
timeoutMs: req.timeoutMs,
|
|
49
|
+
metadata: req.metadata,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return new Promise<ApprovalDecision>((resolvePromise) => {
|
|
53
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
54
|
+
if (full.timeoutMs > 0) {
|
|
55
|
+
timer = setTimeout(() => {
|
|
56
|
+
const entry = this.pendingMap.get(full.id);
|
|
57
|
+
if (!entry) return;
|
|
58
|
+
this.pendingMap.delete(full.id);
|
|
59
|
+
const decision: ApprovalDecision = {
|
|
60
|
+
approvalId: full.id,
|
|
61
|
+
outcome: 'timeout',
|
|
62
|
+
reason: `Approval timed out after ${full.timeoutMs}ms`,
|
|
63
|
+
decidedAt: nowISO(),
|
|
64
|
+
};
|
|
65
|
+
this.emit({ type: 'expired', request: full });
|
|
66
|
+
resolvePromise(decision);
|
|
67
|
+
}, full.timeoutMs);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.pendingMap.set(full.id, { request: full, settle: resolvePromise, timer });
|
|
71
|
+
this.emit({ type: 'requested', request: full });
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
resolve(
|
|
76
|
+
approvalId: string,
|
|
77
|
+
decision: Omit<ApprovalDecision, 'approvalId' | 'decidedAt'>,
|
|
78
|
+
): boolean {
|
|
79
|
+
const entry = this.pendingMap.get(approvalId);
|
|
80
|
+
if (!entry) return false;
|
|
81
|
+
this.pendingMap.delete(approvalId);
|
|
82
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
83
|
+
|
|
84
|
+
const full: ApprovalDecision = {
|
|
85
|
+
approvalId,
|
|
86
|
+
outcome: decision.outcome,
|
|
87
|
+
actor: decision.actor,
|
|
88
|
+
reason: decision.reason,
|
|
89
|
+
decidedAt: nowISO(),
|
|
90
|
+
};
|
|
91
|
+
this.emit({ type: 'resolved', request: entry.request, decision: full });
|
|
92
|
+
entry.settle(full);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pending(): readonly ApprovalRequest[] {
|
|
97
|
+
return Array.from(this.pendingMap.values()).map((e) => e.request);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
subscribe(listener: ApprovalListener): () => void {
|
|
101
|
+
this.listeners.add(listener);
|
|
102
|
+
return () => {
|
|
103
|
+
this.listeners.delete(listener);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
abortAll(reason: string): void {
|
|
108
|
+
const entries = Array.from(this.pendingMap.entries());
|
|
109
|
+
this.pendingMap.clear();
|
|
110
|
+
for (const [id, entry] of entries) {
|
|
111
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
112
|
+
this.emit({ type: 'aborted', request: entry.request, reason });
|
|
113
|
+
entry.settle({
|
|
114
|
+
approvalId: id,
|
|
115
|
+
outcome: 'aborted',
|
|
116
|
+
reason,
|
|
117
|
+
decidedAt: nowISO(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private emit(event: ApprovalEvent): void {
|
|
123
|
+
for (const listener of this.listeners) {
|
|
124
|
+
try {
|
|
125
|
+
listener(event);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error('[approval gateway] listener error:', err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/bootstrap.ts
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import { registerPlugin } from './registry';
|
|
2
|
-
|
|
3
|
-
// Built-in Drivers
|
|
4
|
-
// Only claude-code is built in. Other drivers (codex, opencode) ship as
|
|
5
|
-
// workspace plugins under plugins/ and must be declared in pipeline.yaml
|
|
6
|
-
// via the `plugins` field, e.g.:
|
|
7
|
-
// plugins: ["@tagma/driver-codex", "@tagma/driver-opencode"]
|
|
8
|
-
import { ClaudeCodeDriver } from './drivers/claude-code';
|
|
9
|
-
|
|
10
|
-
// Built-in Triggers
|
|
11
|
-
import { FileTrigger } from './triggers/file';
|
|
12
|
-
import { ManualTrigger } from './triggers/manual';
|
|
13
|
-
|
|
14
|
-
// Built-in Completions
|
|
15
|
-
import { ExitCodeCompletion } from './completions/exit-code';
|
|
16
|
-
import { FileExistsCompletion } from './completions/file-exists';
|
|
17
|
-
import { OutputCheckCompletion } from './completions/output-check';
|
|
18
|
-
|
|
19
|
-
// Built-in Middleware
|
|
20
|
-
import { StaticContextMiddleware } from './middlewares/static-context';
|
|
21
|
-
|
|
22
|
-
export function bootstrapBuiltins(): void {
|
|
23
|
-
// Drivers
|
|
24
|
-
registerPlugin('drivers', 'claude-code', ClaudeCodeDriver);
|
|
25
|
-
|
|
26
|
-
// Triggers
|
|
27
|
-
registerPlugin('triggers', 'file', FileTrigger);
|
|
28
|
-
registerPlugin('triggers', 'manual', ManualTrigger);
|
|
29
|
-
|
|
30
|
-
// Completions
|
|
31
|
-
registerPlugin('completions', 'exit_code', ExitCodeCompletion);
|
|
32
|
-
registerPlugin('completions', 'file_exists', FileExistsCompletion);
|
|
33
|
-
registerPlugin('completions', 'output_check', OutputCheckCompletion);
|
|
34
|
-
|
|
35
|
-
// Middlewares
|
|
36
|
-
registerPlugin('middlewares', 'static_context', StaticContextMiddleware);
|
|
37
|
-
}
|
|
1
|
+
import { registerPlugin } from './registry';
|
|
2
|
+
|
|
3
|
+
// Built-in Drivers
|
|
4
|
+
// Only claude-code is built in. Other drivers (codex, opencode) ship as
|
|
5
|
+
// workspace plugins under plugins/ and must be declared in pipeline.yaml
|
|
6
|
+
// via the `plugins` field, e.g.:
|
|
7
|
+
// plugins: ["@tagma/driver-codex", "@tagma/driver-opencode"]
|
|
8
|
+
import { ClaudeCodeDriver } from './drivers/claude-code';
|
|
9
|
+
|
|
10
|
+
// Built-in Triggers
|
|
11
|
+
import { FileTrigger } from './triggers/file';
|
|
12
|
+
import { ManualTrigger } from './triggers/manual';
|
|
13
|
+
|
|
14
|
+
// Built-in Completions
|
|
15
|
+
import { ExitCodeCompletion } from './completions/exit-code';
|
|
16
|
+
import { FileExistsCompletion } from './completions/file-exists';
|
|
17
|
+
import { OutputCheckCompletion } from './completions/output-check';
|
|
18
|
+
|
|
19
|
+
// Built-in Middleware
|
|
20
|
+
import { StaticContextMiddleware } from './middlewares/static-context';
|
|
21
|
+
|
|
22
|
+
export function bootstrapBuiltins(): void {
|
|
23
|
+
// Drivers
|
|
24
|
+
registerPlugin('drivers', 'claude-code', ClaudeCodeDriver);
|
|
25
|
+
|
|
26
|
+
// Triggers
|
|
27
|
+
registerPlugin('triggers', 'file', FileTrigger);
|
|
28
|
+
registerPlugin('triggers', 'manual', ManualTrigger);
|
|
29
|
+
|
|
30
|
+
// Completions
|
|
31
|
+
registerPlugin('completions', 'exit_code', ExitCodeCompletion);
|
|
32
|
+
registerPlugin('completions', 'file_exists', FileExistsCompletion);
|
|
33
|
+
registerPlugin('completions', 'output_check', OutputCheckCompletion);
|
|
34
|
+
|
|
35
|
+
// Middlewares
|
|
36
|
+
registerPlugin('middlewares', 'static_context', StaticContextMiddleware);
|
|
37
|
+
}
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import type { CompletionPlugin, CompletionContext, TaskResult } from '../types';
|
|
2
|
-
|
|
3
|
-
export const ExitCodeCompletion: CompletionPlugin = {
|
|
4
|
-
name: 'exit_code',
|
|
5
|
-
schema: {
|
|
6
|
-
description: 'Mark the task successful when the exit code matches.',
|
|
7
|
-
fields: {
|
|
8
|
-
expect: {
|
|
9
|
-
type: 'number-or-list',
|
|
10
|
-
default: 0,
|
|
11
|
-
description: 'Expected exit code, or list of acceptable codes (e.g. 0 or [0, 2]).',
|
|
12
|
-
placeholder: '0',
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
async check(
|
|
18
|
-
config: Record<string, unknown>,
|
|
19
|
-
result: TaskResult,
|
|
20
|
-
_ctx: CompletionContext,
|
|
21
|
-
): Promise<boolean> {
|
|
22
|
-
const expected = config.expect ?? 0;
|
|
23
|
-
|
|
24
|
-
if (typeof expected === 'number') {
|
|
25
|
-
return result.exitCode === expected;
|
|
26
|
-
}
|
|
27
|
-
if (Array.isArray(expected) && expected.every((v) => typeof v === 'number')) {
|
|
28
|
-
return expected.includes(result.exitCode);
|
|
29
|
-
}
|
|
30
|
-
throw new Error(
|
|
31
|
-
`exit_code completion: "expect" must be a number or number[], got ${typeof expected}`,
|
|
32
|
-
);
|
|
33
|
-
},
|
|
34
|
-
};
|
|
1
|
+
import type { CompletionPlugin, CompletionContext, TaskResult } from '../types';
|
|
2
|
+
|
|
3
|
+
export const ExitCodeCompletion: CompletionPlugin = {
|
|
4
|
+
name: 'exit_code',
|
|
5
|
+
schema: {
|
|
6
|
+
description: 'Mark the task successful when the exit code matches.',
|
|
7
|
+
fields: {
|
|
8
|
+
expect: {
|
|
9
|
+
type: 'number-or-list',
|
|
10
|
+
default: 0,
|
|
11
|
+
description: 'Expected exit code, or list of acceptable codes (e.g. 0 or [0, 2]).',
|
|
12
|
+
placeholder: '0',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
async check(
|
|
18
|
+
config: Record<string, unknown>,
|
|
19
|
+
result: TaskResult,
|
|
20
|
+
_ctx: CompletionContext,
|
|
21
|
+
): Promise<boolean> {
|
|
22
|
+
const expected = config.expect ?? 0;
|
|
23
|
+
|
|
24
|
+
if (typeof expected === 'number') {
|
|
25
|
+
return result.exitCode === expected;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(expected) && expected.every((v) => typeof v === 'number')) {
|
|
28
|
+
return expected.includes(result.exitCode);
|
|
29
|
+
}
|
|
30
|
+
throw new Error(
|
|
31
|
+
`exit_code completion: "expect" must be a number or number[], got ${typeof expected}`,
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
import { stat } from 'node:fs/promises';
|
|
2
|
-
import type { CompletionPlugin, CompletionContext, TaskResult } from '../types';
|
|
3
|
-
import { validatePath } from '../utils';
|
|
4
|
-
|
|
5
|
-
type Kind = 'file' | 'dir' | 'any';
|
|
6
|
-
|
|
7
|
-
export const FileExistsCompletion: CompletionPlugin = {
|
|
8
|
-
name: 'file_exists',
|
|
9
|
-
schema: {
|
|
10
|
-
description: 'Mark the task successful when a target file or directory exists.',
|
|
11
|
-
fields: {
|
|
12
|
-
path: {
|
|
13
|
-
type: 'path',
|
|
14
|
-
required: true,
|
|
15
|
-
description: 'Path to check (relative to workDir or absolute).',
|
|
16
|
-
},
|
|
17
|
-
kind: {
|
|
18
|
-
type: 'enum',
|
|
19
|
-
enum: ['file', 'dir', 'any'],
|
|
20
|
-
default: 'any',
|
|
21
|
-
description: 'Restrict to a file, directory, or accept either.',
|
|
22
|
-
},
|
|
23
|
-
min_size: {
|
|
24
|
-
type: 'number',
|
|
25
|
-
min: 0,
|
|
26
|
-
description: 'Optional minimum size in bytes (files only).',
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
async check(
|
|
32
|
-
config: Record<string, unknown>,
|
|
33
|
-
_result: TaskResult,
|
|
34
|
-
ctx: CompletionContext,
|
|
35
|
-
): Promise<boolean> {
|
|
36
|
-
const filePath = config.path as string;
|
|
37
|
-
if (!filePath) throw new Error('file_exists completion: "path" is required');
|
|
38
|
-
|
|
39
|
-
const safePath = validatePath(filePath, ctx.workDir);
|
|
40
|
-
|
|
41
|
-
const kind = (config.kind as Kind | undefined) ?? 'any';
|
|
42
|
-
if (kind !== 'file' && kind !== 'dir' && kind !== 'any') {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`file_exists completion: "kind" must be "file" | "dir" | "any", got "${kind}"`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const minSize = config.min_size;
|
|
49
|
-
if (minSize != null && (typeof minSize !== 'number' || minSize < 0)) {
|
|
50
|
-
throw new Error(`file_exists completion: "min_size" must be a non-negative number`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const st = await stat(safePath);
|
|
55
|
-
if (kind === 'file' && !st.isFile()) return false;
|
|
56
|
-
if (kind === 'dir' && !st.isDirectory()) return false;
|
|
57
|
-
if (typeof minSize === 'number' && st.isFile() && st.size < minSize) return false;
|
|
58
|
-
return true;
|
|
59
|
-
} catch (err: unknown) {
|
|
60
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
61
|
-
if (code === 'ENOENT' || code === 'ENOTDIR') return false;
|
|
62
|
-
// Permission / IO errors should surface, not silently mean "missing"
|
|
63
|
-
throw err;
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
};
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import type { CompletionPlugin, CompletionContext, TaskResult } from '../types';
|
|
3
|
+
import { validatePath } from '../utils';
|
|
4
|
+
|
|
5
|
+
type Kind = 'file' | 'dir' | 'any';
|
|
6
|
+
|
|
7
|
+
export const FileExistsCompletion: CompletionPlugin = {
|
|
8
|
+
name: 'file_exists',
|
|
9
|
+
schema: {
|
|
10
|
+
description: 'Mark the task successful when a target file or directory exists.',
|
|
11
|
+
fields: {
|
|
12
|
+
path: {
|
|
13
|
+
type: 'path',
|
|
14
|
+
required: true,
|
|
15
|
+
description: 'Path to check (relative to workDir or absolute).',
|
|
16
|
+
},
|
|
17
|
+
kind: {
|
|
18
|
+
type: 'enum',
|
|
19
|
+
enum: ['file', 'dir', 'any'],
|
|
20
|
+
default: 'any',
|
|
21
|
+
description: 'Restrict to a file, directory, or accept either.',
|
|
22
|
+
},
|
|
23
|
+
min_size: {
|
|
24
|
+
type: 'number',
|
|
25
|
+
min: 0,
|
|
26
|
+
description: 'Optional minimum size in bytes (files only).',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
async check(
|
|
32
|
+
config: Record<string, unknown>,
|
|
33
|
+
_result: TaskResult,
|
|
34
|
+
ctx: CompletionContext,
|
|
35
|
+
): Promise<boolean> {
|
|
36
|
+
const filePath = config.path as string;
|
|
37
|
+
if (!filePath) throw new Error('file_exists completion: "path" is required');
|
|
38
|
+
|
|
39
|
+
const safePath = validatePath(filePath, ctx.workDir);
|
|
40
|
+
|
|
41
|
+
const kind = (config.kind as Kind | undefined) ?? 'any';
|
|
42
|
+
if (kind !== 'file' && kind !== 'dir' && kind !== 'any') {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`file_exists completion: "kind" must be "file" | "dir" | "any", got "${kind}"`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const minSize = config.min_size;
|
|
49
|
+
if (minSize != null && (typeof minSize !== 'number' || minSize < 0)) {
|
|
50
|
+
throw new Error(`file_exists completion: "min_size" must be a non-negative number`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const st = await stat(safePath);
|
|
55
|
+
if (kind === 'file' && !st.isFile()) return false;
|
|
56
|
+
if (kind === 'dir' && !st.isDirectory()) return false;
|
|
57
|
+
if (typeof minSize === 'number' && st.isFile() && st.size < minSize) return false;
|
|
58
|
+
return true;
|
|
59
|
+
} catch (err: unknown) {
|
|
60
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
61
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') return false;
|
|
62
|
+
// Permission / IO errors should surface, not silently mean "missing"
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|