@herdctl/core 1.3.0 → 2.0.0
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/dist/config/__tests__/agent.test.js +12 -12
- package/dist/config/__tests__/agent.test.js.map +1 -1
- package/dist/config/__tests__/loader.test.js +201 -4
- package/dist/config/__tests__/loader.test.js.map +1 -1
- package/dist/config/__tests__/merge.test.js +29 -4
- package/dist/config/__tests__/merge.test.js.map +1 -1
- package/dist/config/__tests__/parser.test.js +13 -13
- package/dist/config/__tests__/parser.test.js.map +1 -1
- package/dist/config/__tests__/schema.test.js +10 -10
- package/dist/config/__tests__/schema.test.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -2
- package/dist/config/index.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +71 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/merge.d.ts +4 -1
- package/dist/config/merge.d.ts.map +1 -1
- package/dist/config/merge.js +16 -0
- package/dist/config/merge.js.map +1 -1
- package/dist/config/schema.d.ts +906 -89
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +109 -7
- package/dist/config/schema.js.map +1 -1
- package/dist/fleet-manager/__tests__/coverage.test.js +25 -24
- package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/discord-manager.test.js +9 -2
- package/dist/fleet-manager/__tests__/discord-manager.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/integration.test.js +27 -0
- package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/job-control.test.js +66 -0
- package/dist/fleet-manager/__tests__/job-control.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/status-queries.test.js +12 -11
- package/dist/fleet-manager/__tests__/status-queries.test.js.map +1 -1
- package/dist/fleet-manager/config-reload.js +9 -9
- package/dist/fleet-manager/config-reload.js.map +1 -1
- package/dist/fleet-manager/discord-manager.d.ts.map +1 -1
- package/dist/fleet-manager/discord-manager.js +27 -4
- package/dist/fleet-manager/discord-manager.js.map +1 -1
- package/dist/fleet-manager/fleet-manager.d.ts +11 -0
- package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
- package/dist/fleet-manager/fleet-manager.js +27 -0
- package/dist/fleet-manager/fleet-manager.js.map +1 -1
- package/dist/fleet-manager/job-control.d.ts +1 -1
- package/dist/fleet-manager/job-control.d.ts.map +1 -1
- package/dist/fleet-manager/job-control.js +36 -14
- package/dist/fleet-manager/job-control.js.map +1 -1
- package/dist/fleet-manager/schedule-executor.d.ts +1 -1
- package/dist/fleet-manager/schedule-executor.d.ts.map +1 -1
- package/dist/fleet-manager/schedule-executor.js +11 -14
- package/dist/fleet-manager/schedule-executor.js.map +1 -1
- package/dist/fleet-manager/status-queries.js +7 -7
- package/dist/fleet-manager/status-queries.js.map +1 -1
- package/dist/fleet-manager/types.d.ts +10 -2
- package/dist/fleet-manager/types.d.ts.map +1 -1
- package/dist/fleet-manager/working-directory-helper.d.ts +29 -0
- package/dist/fleet-manager/working-directory-helper.d.ts.map +1 -0
- package/dist/fleet-manager/working-directory-helper.js +36 -0
- package/dist/fleet-manager/working-directory-helper.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/runner/__tests__/job-executor.test.js +449 -118
- package/dist/runner/__tests__/job-executor.test.js.map +1 -1
- package/dist/runner/__tests__/sdk-adapter.test.js +147 -23
- package/dist/runner/__tests__/sdk-adapter.test.js.map +1 -1
- package/dist/runner/index.d.ts +2 -0
- package/dist/runner/index.d.ts.map +1 -1
- package/dist/runner/index.js +1 -0
- package/dist/runner/index.js.map +1 -1
- package/dist/runner/job-executor.d.ts +12 -8
- package/dist/runner/job-executor.d.ts.map +1 -1
- package/dist/runner/job-executor.js +257 -126
- package/dist/runner/job-executor.js.map +1 -1
- package/dist/runner/runtime/__tests__/cli-session-path.test.d.ts +2 -0
- package/dist/runner/runtime/__tests__/cli-session-path.test.d.ts.map +1 -0
- package/dist/runner/runtime/__tests__/cli-session-path.test.js +150 -0
- package/dist/runner/runtime/__tests__/cli-session-path.test.js.map +1 -0
- package/dist/runner/runtime/__tests__/docker-config.test.d.ts +2 -0
- package/dist/runner/runtime/__tests__/docker-config.test.d.ts.map +1 -0
- package/dist/runner/runtime/__tests__/docker-config.test.js +352 -0
- package/dist/runner/runtime/__tests__/docker-config.test.js.map +1 -0
- package/dist/runner/runtime/__tests__/docker-security.test.d.ts +2 -0
- package/dist/runner/runtime/__tests__/docker-security.test.d.ts.map +1 -0
- package/dist/runner/runtime/__tests__/docker-security.test.js +384 -0
- package/dist/runner/runtime/__tests__/docker-security.test.js.map +1 -0
- package/dist/runner/runtime/__tests__/factory.test.d.ts +2 -0
- package/dist/runner/runtime/__tests__/factory.test.d.ts.map +1 -0
- package/dist/runner/runtime/__tests__/factory.test.js +149 -0
- package/dist/runner/runtime/__tests__/factory.test.js.map +1 -0
- package/dist/runner/runtime/__tests__/integration.test.d.ts +2 -0
- package/dist/runner/runtime/__tests__/integration.test.d.ts.map +1 -0
- package/dist/runner/runtime/__tests__/integration.test.js +274 -0
- package/dist/runner/runtime/__tests__/integration.test.js.map +1 -0
- package/dist/runner/runtime/cli-runtime.d.ts +107 -0
- package/dist/runner/runtime/cli-runtime.d.ts.map +1 -0
- package/dist/runner/runtime/cli-runtime.js +335 -0
- package/dist/runner/runtime/cli-runtime.js.map +1 -0
- package/dist/runner/runtime/cli-session-path.d.ts +108 -0
- package/dist/runner/runtime/cli-session-path.d.ts.map +1 -0
- package/dist/runner/runtime/cli-session-path.js +173 -0
- package/dist/runner/runtime/cli-session-path.js.map +1 -0
- package/dist/runner/runtime/cli-session-watcher.d.ts +55 -0
- package/dist/runner/runtime/cli-session-watcher.d.ts.map +1 -0
- package/dist/runner/runtime/cli-session-watcher.js +187 -0
- package/dist/runner/runtime/cli-session-watcher.js.map +1 -0
- package/dist/runner/runtime/container-manager.d.ts +76 -0
- package/dist/runner/runtime/container-manager.d.ts.map +1 -0
- package/dist/runner/runtime/container-manager.js +229 -0
- package/dist/runner/runtime/container-manager.js.map +1 -0
- package/dist/runner/runtime/container-runner.d.ts +62 -0
- package/dist/runner/runtime/container-runner.d.ts.map +1 -0
- package/dist/runner/runtime/container-runner.js +235 -0
- package/dist/runner/runtime/container-runner.js.map +1 -0
- package/dist/runner/runtime/docker-config.d.ts +100 -0
- package/dist/runner/runtime/docker-config.d.ts.map +1 -0
- package/dist/runner/runtime/docker-config.js +98 -0
- package/dist/runner/runtime/docker-config.js.map +1 -0
- package/dist/runner/runtime/factory.d.ts +63 -0
- package/dist/runner/runtime/factory.d.ts.map +1 -0
- package/dist/runner/runtime/factory.js +68 -0
- package/dist/runner/runtime/factory.js.map +1 -0
- package/dist/runner/runtime/index.d.ts +20 -0
- package/dist/runner/runtime/index.d.ts.map +1 -0
- package/dist/runner/runtime/index.js +21 -0
- package/dist/runner/runtime/index.js.map +1 -0
- package/dist/runner/runtime/interface.d.ts +59 -0
- package/dist/runner/runtime/interface.d.ts.map +1 -0
- package/dist/runner/runtime/interface.js +12 -0
- package/dist/runner/runtime/interface.js.map +1 -0
- package/dist/runner/runtime/sdk-runtime.d.ts +46 -0
- package/dist/runner/runtime/sdk-runtime.d.ts.map +1 -0
- package/dist/runner/runtime/sdk-runtime.js +63 -0
- package/dist/runner/runtime/sdk-runtime.js.map +1 -0
- package/dist/runner/sdk-adapter.d.ts +4 -0
- package/dist/runner/sdk-adapter.d.ts.map +1 -1
- package/dist/runner/sdk-adapter.js +35 -16
- package/dist/runner/sdk-adapter.js.map +1 -1
- package/dist/runner/types.d.ts +12 -6
- package/dist/runner/types.d.ts.map +1 -1
- package/dist/scheduler/__tests__/schedule-runner.test.js +61 -50
- package/dist/scheduler/__tests__/schedule-runner.test.js.map +1 -1
- package/dist/scheduler/schedule-runner.d.ts +1 -4
- package/dist/scheduler/schedule-runner.d.ts.map +1 -1
- package/dist/scheduler/schedule-runner.js +40 -8
- package/dist/scheduler/schedule-runner.js.map +1 -1
- package/dist/state/__tests__/session-schema.test.js +4 -0
- package/dist/state/__tests__/session-schema.test.js.map +1 -1
- package/dist/state/__tests__/session-validation.test.d.ts +2 -0
- package/dist/state/__tests__/session-validation.test.d.ts.map +1 -0
- package/dist/state/__tests__/session-validation.test.js +446 -0
- package/dist/state/__tests__/session-validation.test.js.map +1 -0
- package/dist/state/__tests__/session.test.js +68 -0
- package/dist/state/__tests__/session.test.js.map +1 -1
- package/dist/state/__tests__/working-directory-validation.test.d.ts +5 -0
- package/dist/state/__tests__/working-directory-validation.test.d.ts.map +1 -0
- package/dist/state/__tests__/working-directory-validation.test.js +101 -0
- package/dist/state/__tests__/working-directory-validation.test.js.map +1 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +4 -0
- package/dist/state/index.js.map +1 -1
- package/dist/state/schemas/session-info.d.ts +32 -0
- package/dist/state/schemas/session-info.d.ts.map +1 -1
- package/dist/state/schemas/session-info.js +22 -0
- package/dist/state/schemas/session-info.js.map +1 -1
- package/dist/state/session-validation.d.ts +202 -0
- package/dist/state/session-validation.d.ts.map +1 -0
- package/dist/state/session-validation.js +407 -0
- package/dist/state/session-validation.js.map +1 -0
- package/dist/state/session.d.ts +23 -3
- package/dist/state/session.d.ts.map +1 -1
- package/dist/state/session.js +41 -6
- package/dist/state/session.js.map +1 -1
- package/dist/state/working-directory-validation.d.ts +52 -0
- package/dist/state/working-directory-validation.d.ts.map +1 -0
- package/dist/state/working-directory-validation.js +81 -0
- package/dist/state/working-directory-validation.js.map +1 -0
- package/package.json +7 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI session file watcher - monitors CLI session files for new messages
|
|
3
|
+
*
|
|
4
|
+
* Event-driven watcher that yields messages as they're written to the session file.
|
|
5
|
+
* No polling, no timeouts - just clean async/await.
|
|
6
|
+
*/
|
|
7
|
+
import type { SDKMessage } from "../types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Watches a CLI session file and yields new messages as they're written
|
|
10
|
+
*/
|
|
11
|
+
export declare class CLISessionWatcher {
|
|
12
|
+
private watcher;
|
|
13
|
+
private lastLineCount;
|
|
14
|
+
private sessionFilePath;
|
|
15
|
+
private messageQueue;
|
|
16
|
+
private pendingMessageResolve;
|
|
17
|
+
private stopped;
|
|
18
|
+
constructor(sessionFilePath: string);
|
|
19
|
+
/**
|
|
20
|
+
* Initialize watcher by reading existing file content
|
|
21
|
+
*
|
|
22
|
+
* When resuming a session, call this before watch() to skip existing
|
|
23
|
+
* messages and only process new content appended after this point.
|
|
24
|
+
*/
|
|
25
|
+
initialize(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Process file and queue any new messages
|
|
28
|
+
*/
|
|
29
|
+
private processFile;
|
|
30
|
+
/**
|
|
31
|
+
* Watch session file and yield messages as they arrive
|
|
32
|
+
*
|
|
33
|
+
* This is event-driven - it waits (blocks) until messages are available.
|
|
34
|
+
* No polling, no timeouts.
|
|
35
|
+
*/
|
|
36
|
+
watch(): AsyncIterable<SDKMessage>;
|
|
37
|
+
/**
|
|
38
|
+
* Process any remaining messages in the file
|
|
39
|
+
*
|
|
40
|
+
* Called when the CLI process exits to ensure we don't miss final messages
|
|
41
|
+
* that haven't triggered a chokidar event yet.
|
|
42
|
+
*
|
|
43
|
+
* @returns Array of any remaining messages found
|
|
44
|
+
*/
|
|
45
|
+
flushRemainingMessages(): Promise<SDKMessage[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Stop watching
|
|
48
|
+
*/
|
|
49
|
+
stop(): void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Convenience function to watch a session file
|
|
53
|
+
*/
|
|
54
|
+
export declare function watchSessionFile(sessionFilePath: string, signal?: AbortSignal): AsyncIterable<SDKMessage>;
|
|
55
|
+
//# sourceMappingURL=cli-session-watcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-session-watcher.d.ts","sourceRoot":"","sources":["../../../src/runner/runtime/cli-session-watcher.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;GAEG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAAkD;IACjE,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,qBAAqB,CAA6B;IAC1D,OAAO,CAAC,OAAO,CAAS;gBAEZ,eAAe,EAAE,MAAM;IAInC;;;;;OAKG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBjC;;OAEG;YACW,WAAW;IAwCzB;;;;;OAKG;IACI,KAAK,IAAI,aAAa,CAAC,UAAU,CAAC;IA0DzC;;;;;;;OAOG;IACG,sBAAsB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAYrD;;OAEG;IACH,IAAI,IAAI,IAAI;CAab;AAED;;GAEG;AACH,wBAAuB,gBAAgB,CACrC,eAAe,EAAE,MAAM,EACvB,MAAM,CAAC,EAAE,WAAW,GACnB,aAAa,CAAC,UAAU,CAAC,CAa3B"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI session file watcher - monitors CLI session files for new messages
|
|
3
|
+
*
|
|
4
|
+
* Event-driven watcher that yields messages as they're written to the session file.
|
|
5
|
+
* No polling, no timeouts - just clean async/await.
|
|
6
|
+
*/
|
|
7
|
+
import chokidar from "chokidar";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
/**
|
|
10
|
+
* Watches a CLI session file and yields new messages as they're written
|
|
11
|
+
*/
|
|
12
|
+
export class CLISessionWatcher {
|
|
13
|
+
watcher = null;
|
|
14
|
+
lastLineCount = 0;
|
|
15
|
+
sessionFilePath;
|
|
16
|
+
messageQueue = [];
|
|
17
|
+
pendingMessageResolve = null;
|
|
18
|
+
stopped = false;
|
|
19
|
+
constructor(sessionFilePath) {
|
|
20
|
+
this.sessionFilePath = sessionFilePath;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Initialize watcher by reading existing file content
|
|
24
|
+
*
|
|
25
|
+
* When resuming a session, call this before watch() to skip existing
|
|
26
|
+
* messages and only process new content appended after this point.
|
|
27
|
+
*/
|
|
28
|
+
async initialize() {
|
|
29
|
+
try {
|
|
30
|
+
const content = await readFile(this.sessionFilePath, "utf-8");
|
|
31
|
+
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
32
|
+
this.lastLineCount = lines.length;
|
|
33
|
+
console.log(`[CLISessionWatcher] Initialized at line ${this.lastLineCount}, will skip existing content`);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === "ENOENT") {
|
|
37
|
+
// File doesn't exist yet - that's fine for new sessions
|
|
38
|
+
console.log(`[CLISessionWatcher] File doesn't exist yet, starting from line 0`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.warn(`[CLISessionWatcher] Failed to initialize: ${error instanceof Error ? error.message : String(error)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Process file and queue any new messages
|
|
47
|
+
*/
|
|
48
|
+
async processFile() {
|
|
49
|
+
try {
|
|
50
|
+
const content = await readFile(this.sessionFilePath, "utf-8");
|
|
51
|
+
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
52
|
+
// Process only new lines since last read
|
|
53
|
+
const newLines = lines.slice(this.lastLineCount);
|
|
54
|
+
this.lastLineCount = lines.length;
|
|
55
|
+
console.log(`[CLISessionWatcher] Processing ${newLines.length} new lines`);
|
|
56
|
+
// Parse and queue valid messages
|
|
57
|
+
for (const line of newLines) {
|
|
58
|
+
try {
|
|
59
|
+
const message = JSON.parse(line);
|
|
60
|
+
console.log(`[CLISessionWatcher] Queued message type: ${message.type}`);
|
|
61
|
+
this.messageQueue.push(message);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// Skip invalid JSON lines (CLI may output non-JSON)
|
|
65
|
+
console.warn(`[CLISessionWatcher] Failed to parse line: ${error instanceof Error ? error.message : String(error)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// If someone is waiting for a message and we have one, wake them up
|
|
69
|
+
if (this.pendingMessageResolve && this.messageQueue.length > 0) {
|
|
70
|
+
console.log(`[CLISessionWatcher] Waking up waiting iterator`);
|
|
71
|
+
this.pendingMessageResolve();
|
|
72
|
+
this.pendingMessageResolve = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.code !== "ENOENT") {
|
|
77
|
+
console.warn(`[CLISessionWatcher] Error reading session file: ${error instanceof Error ? error.message : String(error)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Watch session file and yield messages as they arrive
|
|
83
|
+
*
|
|
84
|
+
* This is event-driven - it waits (blocks) until messages are available.
|
|
85
|
+
* No polling, no timeouts.
|
|
86
|
+
*/
|
|
87
|
+
async *watch() {
|
|
88
|
+
// Configure chokidar
|
|
89
|
+
this.watcher = chokidar.watch(this.sessionFilePath, {
|
|
90
|
+
awaitWriteFinish: {
|
|
91
|
+
stabilityThreshold: 500,
|
|
92
|
+
pollInterval: 100,
|
|
93
|
+
},
|
|
94
|
+
ignoreInitial: false,
|
|
95
|
+
});
|
|
96
|
+
this.watcher.on("add", async () => {
|
|
97
|
+
console.log("[CLISessionWatcher] File 'add' event");
|
|
98
|
+
await this.processFile();
|
|
99
|
+
});
|
|
100
|
+
this.watcher.on("change", async () => {
|
|
101
|
+
console.log("[CLISessionWatcher] File 'change' event");
|
|
102
|
+
await this.processFile();
|
|
103
|
+
});
|
|
104
|
+
this.watcher.on("error", (error) => {
|
|
105
|
+
console.error(`[CLISessionWatcher] Watcher error: ${error instanceof Error ? error.message : String(error)}`);
|
|
106
|
+
});
|
|
107
|
+
try {
|
|
108
|
+
while (!this.stopped) {
|
|
109
|
+
// If we have queued messages, yield them
|
|
110
|
+
while (this.messageQueue.length > 0) {
|
|
111
|
+
const message = this.messageQueue.shift();
|
|
112
|
+
console.log(`[CLISessionWatcher] Yielding message type: ${message.type}`);
|
|
113
|
+
yield message;
|
|
114
|
+
}
|
|
115
|
+
// No messages - wait for chokidar to add one
|
|
116
|
+
console.log(`[CLISessionWatcher] No messages, waiting for chokidar event`);
|
|
117
|
+
await new Promise((resolve) => {
|
|
118
|
+
this.pendingMessageResolve = resolve;
|
|
119
|
+
// Also wake up if stopped
|
|
120
|
+
if (this.stopped) {
|
|
121
|
+
resolve();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
console.log(`[CLISessionWatcher] Woke up (stopped: ${this.stopped}, queue: ${this.messageQueue.length})`);
|
|
125
|
+
// Check if we should exit
|
|
126
|
+
if (this.stopped) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
console.log(`[CLISessionWatcher] Generator exiting`);
|
|
133
|
+
this.stop();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Process any remaining messages in the file
|
|
138
|
+
*
|
|
139
|
+
* Called when the CLI process exits to ensure we don't miss final messages
|
|
140
|
+
* that haven't triggered a chokidar event yet.
|
|
141
|
+
*
|
|
142
|
+
* @returns Array of any remaining messages found
|
|
143
|
+
*/
|
|
144
|
+
async flushRemainingMessages() {
|
|
145
|
+
console.log(`[CLISessionWatcher] Flushing remaining messages from file`);
|
|
146
|
+
await this.processFile();
|
|
147
|
+
// Return all queued messages
|
|
148
|
+
const messages = [...this.messageQueue];
|
|
149
|
+
this.messageQueue = [];
|
|
150
|
+
console.log(`[CLISessionWatcher] Flushed ${messages.length} remaining message(s)`);
|
|
151
|
+
return messages;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Stop watching
|
|
155
|
+
*/
|
|
156
|
+
stop() {
|
|
157
|
+
console.log(`[CLISessionWatcher] stop() called`);
|
|
158
|
+
this.stopped = true;
|
|
159
|
+
if (this.watcher) {
|
|
160
|
+
this.watcher.close();
|
|
161
|
+
this.watcher = null;
|
|
162
|
+
}
|
|
163
|
+
// Wake up any waiting iterator
|
|
164
|
+
if (this.pendingMessageResolve) {
|
|
165
|
+
this.pendingMessageResolve();
|
|
166
|
+
this.pendingMessageResolve = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Convenience function to watch a session file
|
|
172
|
+
*/
|
|
173
|
+
export async function* watchSessionFile(sessionFilePath, signal) {
|
|
174
|
+
const watcher = new CLISessionWatcher(sessionFilePath);
|
|
175
|
+
try {
|
|
176
|
+
for await (const message of watcher.watch()) {
|
|
177
|
+
if (signal?.aborted) {
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
yield message;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
watcher.stop();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=cli-session-watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-session-watcher.js","sourceRoot":"","sources":["../../../src/runner/runtime/cli-session-watcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker container lifecycle management
|
|
3
|
+
*
|
|
4
|
+
* Handles container creation, security configuration, and cleanup.
|
|
5
|
+
* Uses dockerode for Docker API communication.
|
|
6
|
+
*/
|
|
7
|
+
import type { Container, Exec } from "dockerode";
|
|
8
|
+
import type { DockerConfig, PathMapping } from "./docker-config.js";
|
|
9
|
+
import type { ResolvedAgent } from "../../config/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* Container manager for herdctl Docker execution
|
|
12
|
+
*/
|
|
13
|
+
export declare class ContainerManager {
|
|
14
|
+
private docker;
|
|
15
|
+
private runningContainers;
|
|
16
|
+
constructor(docker?: import("dockerode"));
|
|
17
|
+
/**
|
|
18
|
+
* Get or create a container for an agent
|
|
19
|
+
*
|
|
20
|
+
* For persistent containers (ephemeral: false), reuses existing running container.
|
|
21
|
+
* For ephemeral containers, always creates a new container with AutoRemove.
|
|
22
|
+
*
|
|
23
|
+
* @param agentName - Name of the agent
|
|
24
|
+
* @param config - Docker configuration
|
|
25
|
+
* @param mounts - Volume mounts
|
|
26
|
+
* @param env - Environment variables
|
|
27
|
+
* @returns Docker container
|
|
28
|
+
*/
|
|
29
|
+
getOrCreateContainer(agentName: string, config: DockerConfig, mounts: PathMapping[], env: string[]): Promise<Container>;
|
|
30
|
+
/**
|
|
31
|
+
* Create a new Docker container with security hardening
|
|
32
|
+
*/
|
|
33
|
+
private createContainer;
|
|
34
|
+
/**
|
|
35
|
+
* Execute a command inside a container
|
|
36
|
+
*
|
|
37
|
+
* @param container - Docker container
|
|
38
|
+
* @param command - Command and arguments
|
|
39
|
+
* @param workDir - Working directory inside container
|
|
40
|
+
* @returns Exec instance for stream access
|
|
41
|
+
*/
|
|
42
|
+
execInContainer(container: Container, command: string[], workDir?: string): Promise<Exec>;
|
|
43
|
+
/**
|
|
44
|
+
* Clean up old containers for an agent
|
|
45
|
+
*
|
|
46
|
+
* Removes oldest containers when count exceeds maxContainers.
|
|
47
|
+
*
|
|
48
|
+
* @param agentName - Name of the agent
|
|
49
|
+
* @param maxContainers - Maximum containers to keep
|
|
50
|
+
*/
|
|
51
|
+
cleanupOldContainers(agentName: string, maxContainers: number): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Stop and remove a specific container
|
|
54
|
+
*/
|
|
55
|
+
stopContainer(container: Container): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build volume mounts for container execution
|
|
59
|
+
*
|
|
60
|
+
* Creates mounts for working directory, auth files, and Docker sessions.
|
|
61
|
+
*
|
|
62
|
+
* @param agent - Resolved agent configuration
|
|
63
|
+
* @param dockerConfig - Docker configuration
|
|
64
|
+
* @param stateDir - herdctl state directory (.herdctl/)
|
|
65
|
+
* @returns Array of path mappings
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildContainerMounts(agent: ResolvedAgent, dockerConfig: DockerConfig, stateDir: string): PathMapping[];
|
|
68
|
+
/**
|
|
69
|
+
* Build environment variables for container
|
|
70
|
+
*
|
|
71
|
+
* @param agent - Resolved agent configuration
|
|
72
|
+
* @param config - Docker configuration (for custom env vars)
|
|
73
|
+
* @returns Array of "KEY=value" strings
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildContainerEnv(agent: ResolvedAgent, config?: DockerConfig): string[];
|
|
76
|
+
//# sourceMappingURL=container-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container-manager.d.ts","sourceRoot":"","sources":["../../../src/runner/runtime/container-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAA0B,IAAI,EAAE,MAAM,WAAW,CAAC;AAIzE,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,iBAAiB,CAAgC;gBAE7C,MAAM,CAAC,EAAE,OAAO,WAAW,CAAC;IAIxC;;;;;;;;;;;OAWG;IACG,oBAAoB,CACxB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,WAAW,EAAE,EACrB,GAAG,EAAE,MAAM,EAAE,GACZ,OAAO,CAAC,SAAS,CAAC;IA+BrB;;OAEG;YACW,eAAe;IAoD7B;;;;;;;OAOG;IACG,eAAe,CACnB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,GAAE,MAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;;;OAOG;IACG,oBAAoB,CACxB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC;IAwBhB;;OAEG;IACG,aAAa,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;CAgBzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,aAAa,EACpB,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,MAAM,GACf,WAAW,EAAE,CAiCf;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,aAAa,EACpB,MAAM,CAAC,EAAE,YAAY,GACpB,MAAM,EAAE,CA2BV"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker container lifecycle management
|
|
3
|
+
*
|
|
4
|
+
* Handles container creation, security configuration, and cleanup.
|
|
5
|
+
* Uses dockerode for Docker API communication.
|
|
6
|
+
*/
|
|
7
|
+
import Dockerode from "dockerode";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
/**
|
|
10
|
+
* Container manager for herdctl Docker execution
|
|
11
|
+
*/
|
|
12
|
+
export class ContainerManager {
|
|
13
|
+
docker;
|
|
14
|
+
runningContainers = new Map();
|
|
15
|
+
constructor(docker) {
|
|
16
|
+
this.docker = docker ?? new Dockerode();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get or create a container for an agent
|
|
20
|
+
*
|
|
21
|
+
* For persistent containers (ephemeral: false), reuses existing running container.
|
|
22
|
+
* For ephemeral containers, always creates a new container with AutoRemove.
|
|
23
|
+
*
|
|
24
|
+
* @param agentName - Name of the agent
|
|
25
|
+
* @param config - Docker configuration
|
|
26
|
+
* @param mounts - Volume mounts
|
|
27
|
+
* @param env - Environment variables
|
|
28
|
+
* @returns Docker container
|
|
29
|
+
*/
|
|
30
|
+
async getOrCreateContainer(agentName, config, mounts, env) {
|
|
31
|
+
// For persistent containers, check if already running
|
|
32
|
+
if (!config.ephemeral) {
|
|
33
|
+
const existing = this.runningContainers.get(agentName);
|
|
34
|
+
if (existing) {
|
|
35
|
+
try {
|
|
36
|
+
const info = await existing.inspect();
|
|
37
|
+
if (info.State.Running) {
|
|
38
|
+
return existing;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Container no longer exists, remove from map
|
|
43
|
+
this.runningContainers.delete(agentName);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Create new container
|
|
48
|
+
const container = await this.createContainer(agentName, config, mounts, env);
|
|
49
|
+
// Start the container
|
|
50
|
+
await container.start();
|
|
51
|
+
// Track persistent containers
|
|
52
|
+
if (!config.ephemeral) {
|
|
53
|
+
this.runningContainers.set(agentName, container);
|
|
54
|
+
}
|
|
55
|
+
return container;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Create a new Docker container with security hardening
|
|
59
|
+
*/
|
|
60
|
+
async createContainer(agentName, config, mounts, env) {
|
|
61
|
+
const containerName = `herdctl-${agentName}-${Date.now()}`;
|
|
62
|
+
const createOptions = {
|
|
63
|
+
Image: config.image,
|
|
64
|
+
name: containerName,
|
|
65
|
+
Tty: false,
|
|
66
|
+
OpenStdin: true,
|
|
67
|
+
StdinOnce: false,
|
|
68
|
+
// Keep container running for exec commands
|
|
69
|
+
Cmd: ["sleep", "infinity"],
|
|
70
|
+
WorkingDir: "/workspace",
|
|
71
|
+
Env: env,
|
|
72
|
+
HostConfig: {
|
|
73
|
+
// Resource limits
|
|
74
|
+
Memory: config.memoryBytes,
|
|
75
|
+
MemorySwap: config.memoryBytes, // Same as Memory = no swap
|
|
76
|
+
CpuShares: config.cpuShares, // undefined = no limit (full CPU access)
|
|
77
|
+
// Network isolation
|
|
78
|
+
NetworkMode: config.network,
|
|
79
|
+
// Volume mounts
|
|
80
|
+
Binds: mounts.map((m) => `${m.hostPath}:${m.containerPath}:${m.mode}`),
|
|
81
|
+
// Security hardening
|
|
82
|
+
SecurityOpt: ["no-new-privileges:true"],
|
|
83
|
+
CapDrop: ["ALL"],
|
|
84
|
+
ReadonlyRootfs: false, // Claude needs to write temp files
|
|
85
|
+
// Cleanup
|
|
86
|
+
AutoRemove: config.ephemeral,
|
|
87
|
+
},
|
|
88
|
+
// Non-root user
|
|
89
|
+
User: config.user,
|
|
90
|
+
};
|
|
91
|
+
return this.docker.createContainer(createOptions);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Execute a command inside a container
|
|
95
|
+
*
|
|
96
|
+
* @param container - Docker container
|
|
97
|
+
* @param command - Command and arguments
|
|
98
|
+
* @param workDir - Working directory inside container
|
|
99
|
+
* @returns Exec instance for stream access
|
|
100
|
+
*/
|
|
101
|
+
async execInContainer(container, command, workDir = "/workspace") {
|
|
102
|
+
return container.exec({
|
|
103
|
+
Cmd: command,
|
|
104
|
+
AttachStdout: true,
|
|
105
|
+
AttachStderr: true,
|
|
106
|
+
AttachStdin: false,
|
|
107
|
+
Tty: false,
|
|
108
|
+
WorkingDir: workDir,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Clean up old containers for an agent
|
|
113
|
+
*
|
|
114
|
+
* Removes oldest containers when count exceeds maxContainers.
|
|
115
|
+
*
|
|
116
|
+
* @param agentName - Name of the agent
|
|
117
|
+
* @param maxContainers - Maximum containers to keep
|
|
118
|
+
*/
|
|
119
|
+
async cleanupOldContainers(agentName, maxContainers) {
|
|
120
|
+
const containers = await this.docker.listContainers({
|
|
121
|
+
all: true,
|
|
122
|
+
filters: {
|
|
123
|
+
name: [`herdctl-${agentName}-`],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
// Sort by creation time, oldest first
|
|
127
|
+
const sorted = containers.sort((a, b) => a.Created - b.Created);
|
|
128
|
+
// Remove oldest until under limit
|
|
129
|
+
const toRemove = sorted.slice(0, Math.max(0, sorted.length - maxContainers));
|
|
130
|
+
for (const info of toRemove) {
|
|
131
|
+
const container = this.docker.getContainer(info.Id);
|
|
132
|
+
try {
|
|
133
|
+
await container.remove({ force: true });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Ignore errors for already-removed containers
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Stop and remove a specific container
|
|
142
|
+
*/
|
|
143
|
+
async stopContainer(container) {
|
|
144
|
+
try {
|
|
145
|
+
await container.stop({ t: 5 }); // 5 second timeout
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Container may already be stopped
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const info = await container.inspect();
|
|
152
|
+
if (!info.HostConfig?.AutoRemove) {
|
|
153
|
+
await container.remove({ force: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Container may already be removed
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Build volume mounts for container execution
|
|
163
|
+
*
|
|
164
|
+
* Creates mounts for working directory, auth files, and Docker sessions.
|
|
165
|
+
*
|
|
166
|
+
* @param agent - Resolved agent configuration
|
|
167
|
+
* @param dockerConfig - Docker configuration
|
|
168
|
+
* @param stateDir - herdctl state directory (.herdctl/)
|
|
169
|
+
* @returns Array of path mappings
|
|
170
|
+
*/
|
|
171
|
+
export function buildContainerMounts(agent, dockerConfig, stateDir) {
|
|
172
|
+
const mounts = [];
|
|
173
|
+
// Working directory mount
|
|
174
|
+
const working_directory = agent.working_directory;
|
|
175
|
+
if (working_directory) {
|
|
176
|
+
const working_directoryRoot = typeof working_directory === "string"
|
|
177
|
+
? working_directory
|
|
178
|
+
: working_directory.root;
|
|
179
|
+
mounts.push({
|
|
180
|
+
hostPath: working_directoryRoot,
|
|
181
|
+
containerPath: "/workspace",
|
|
182
|
+
mode: dockerConfig.workspaceMode,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// Docker sessions directory (separate from host sessions)
|
|
186
|
+
// Claude CLI writes sessions to ~/.claude/projects/<encoded-workspace>/
|
|
187
|
+
// Inside container, working dir is /workspace → encoded as "-workspace"
|
|
188
|
+
// Mount docker-sessions to this location so we can watch files from host
|
|
189
|
+
// Note: Authentication uses ANTHROPIC_API_KEY env var, so no auth mount needed
|
|
190
|
+
const dockerSessionsDir = path.join(stateDir, "docker-sessions");
|
|
191
|
+
mounts.push({
|
|
192
|
+
hostPath: dockerSessionsDir,
|
|
193
|
+
containerPath: "/home/claude/.claude/projects/-workspace",
|
|
194
|
+
mode: "rw",
|
|
195
|
+
});
|
|
196
|
+
// Custom volumes from config
|
|
197
|
+
mounts.push(...dockerConfig.volumes);
|
|
198
|
+
return mounts;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Build environment variables for container
|
|
202
|
+
*
|
|
203
|
+
* @param agent - Resolved agent configuration
|
|
204
|
+
* @param config - Docker configuration (for custom env vars)
|
|
205
|
+
* @returns Array of "KEY=value" strings
|
|
206
|
+
*/
|
|
207
|
+
export function buildContainerEnv(agent, config) {
|
|
208
|
+
const env = [];
|
|
209
|
+
// Pass through API key if available (preferred over mounted auth)
|
|
210
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
211
|
+
env.push(`ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`);
|
|
212
|
+
}
|
|
213
|
+
// Pass through OAuth token if available (for Claude Max web authentication)
|
|
214
|
+
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
215
|
+
env.push(`CLAUDE_CODE_OAUTH_TOKEN=${process.env.CLAUDE_CODE_OAUTH_TOKEN}`);
|
|
216
|
+
}
|
|
217
|
+
// Add custom environment variables from docker config
|
|
218
|
+
if (config?.env) {
|
|
219
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
220
|
+
env.push(`${key}=${value}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Terminal support
|
|
224
|
+
env.push("TERM=xterm-256color");
|
|
225
|
+
// HOME directory for claude user
|
|
226
|
+
env.push("HOME=/home/claude");
|
|
227
|
+
return env;
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=container-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container-manager.js","sourceRoot":"","sources":["../../../src/runner/runtime/container-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContainerRunner - Docker container decorator for RuntimeInterface
|
|
3
|
+
*
|
|
4
|
+
* Wraps any runtime (SDK or CLI) and transparently executes inside Docker containers.
|
|
5
|
+
* Handles path translation, mount configuration, and container lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const baseRuntime = new CLIRuntime();
|
|
10
|
+
* const dockerRuntime = new ContainerRunner(baseRuntime, dockerConfig);
|
|
11
|
+
*
|
|
12
|
+
* // Execution happens inside Docker container
|
|
13
|
+
* for await (const message of dockerRuntime.execute(options)) {
|
|
14
|
+
* console.log(message);
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { RuntimeInterface, RuntimeExecuteOptions } from "./interface.js";
|
|
19
|
+
import type { SDKMessage } from "../types.js";
|
|
20
|
+
import type { DockerConfig } from "./docker-config.js";
|
|
21
|
+
/**
|
|
22
|
+
* Container runtime decorator
|
|
23
|
+
*
|
|
24
|
+
* Decorates any RuntimeInterface to execute inside Docker containers.
|
|
25
|
+
* The wrapped runtime's execute logic runs via `docker exec` inside the container.
|
|
26
|
+
*/
|
|
27
|
+
export declare class ContainerRunner implements RuntimeInterface {
|
|
28
|
+
private wrapped;
|
|
29
|
+
private config;
|
|
30
|
+
private manager;
|
|
31
|
+
private stateDir;
|
|
32
|
+
/**
|
|
33
|
+
* Create a new ContainerRunner
|
|
34
|
+
*
|
|
35
|
+
* @param wrapped - The underlying runtime to execute inside containers
|
|
36
|
+
* @param config - Docker configuration
|
|
37
|
+
* @param stateDir - herdctl state directory (.herdctl/)
|
|
38
|
+
* @param docker - Optional Docker client for testing
|
|
39
|
+
*/
|
|
40
|
+
constructor(wrapped: RuntimeInterface, config: DockerConfig, stateDir: string, docker?: import("dockerode"));
|
|
41
|
+
/**
|
|
42
|
+
* Execute agent inside Docker container
|
|
43
|
+
*
|
|
44
|
+
* Creates or reuses container, then executes based on runtime type:
|
|
45
|
+
* - CLI runtime: docker exec claude, watch session files on host
|
|
46
|
+
* - SDK runtime: docker exec wrapper script, stream JSONL from stdout
|
|
47
|
+
*/
|
|
48
|
+
execute(options: RuntimeExecuteOptions): AsyncIterable<SDKMessage>;
|
|
49
|
+
/**
|
|
50
|
+
* Execute CLI runtime inside Docker container
|
|
51
|
+
*
|
|
52
|
+
* Spawns claude CLI via docker exec and watches session files on host.
|
|
53
|
+
*/
|
|
54
|
+
private executeCLIRuntime;
|
|
55
|
+
/**
|
|
56
|
+
* Execute SDK runtime inside Docker container
|
|
57
|
+
*
|
|
58
|
+
* Runs docker-sdk-wrapper.js script which imports SDK and streams messages as JSONL.
|
|
59
|
+
*/
|
|
60
|
+
private executeSDKRuntime;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=container-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container-runner.d.ts","sourceRoot":"","sources":["../../../src/runner/runtime/container-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAWvD;;;;;GAKG;AACH,qBAAa,eAAgB,YAAW,gBAAgB;IAapD,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,MAAM;IAbhB,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;;;;OAOG;gBAEO,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,YAAY,EAC5B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,OAAO,WAAW,CAAC;IAM9B;;;;;;OAMG;IACI,OAAO,CAAC,OAAO,EAAE,qBAAqB,GAAG,aAAa,CAAC,UAAU,CAAC;IAqEzE;;;;OAIG;YACY,iBAAiB;IAkChC;;;;OAIG;YACY,iBAAiB;CAgGjC"}
|