@docker-harpoon/core 0.1.0 → 0.1.2
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/api/index.d.ts +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/promise.d.ts +8 -0
- package/dist/api/promise.d.ts.map +1 -1
- package/dist/api/promise.js +109 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/resources/container.d.ts +10 -0
- package/dist/resources/container.d.ts.map +1 -1
- package/dist/resources/container.js +222 -5
- package/dist/resources/index.d.ts +2 -0
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +2 -0
- package/dist/resources/schemas.d.ts +120 -0
- package/dist/resources/schemas.d.ts.map +1 -0
- package/dist/resources/schemas.js +95 -0
- package/package.json +4 -2
package/dist/api/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Re-exports the Promise-based API for cleaner imports.
|
|
5
5
|
*/
|
|
6
|
-
export { Network, Container, database, Image, setDocker, resetDocker, destroyAll, type NetworkConfig, type NetworkResource, type ContainerConfig, type ContainerResource, type PortMapping, type ShutdownMetadata, type DatabaseConfig, type DatabaseResource, type ImageConfig, type ImageResource, } from './promise';
|
|
6
|
+
export { Network, Container, database, Image, setDocker, resetDocker, destroyAll, type NetworkConfig, type NetworkResource, type ContainerConfig, type ContainerResource, type PortMapping, type ShutdownMetadata, type DatabaseConfig, type DatabaseResource, type ImageConfig, type ImageResource, type ContainerStats, type ExecOptions, type ExecResult, type LogOptions, type LogLine, } from './promise';
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/api/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAEL,OAAO,EACP,SAAS,EACT,QAAQ,EACR,KAAK,EAGL,SAAS,EACT,WAAW,EACX,UAAU,EAGV,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,aAAa,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAEL,OAAO,EACP,SAAS,EACT,QAAQ,EACR,KAAK,EAGL,SAAS,EACT,WAAW,EACX,UAAU,EAGV,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,aAAa,EAGlB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,OAAO,GACb,MAAM,WAAW,CAAC"}
|
package/dist/api/promise.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* Inspired by Alchemy.run and Pulumi's IAC patterns.
|
|
8
8
|
*/
|
|
9
9
|
import Docker from 'dockerode';
|
|
10
|
+
import { type ContainerStats, type ExecOptions, type ExecResult, type LogOptions, type LogLine } from '../resources';
|
|
11
|
+
export type { ContainerStats, ExecOptions, ExecResult, LogOptions, LogLine, } from '../resources';
|
|
10
12
|
import type { Binding } from '../bindings/types';
|
|
11
13
|
export interface NetworkConfig {
|
|
12
14
|
/** Network driver (default: 'bridge') */
|
|
@@ -49,6 +51,12 @@ export interface ContainerResource {
|
|
|
49
51
|
stop(signal?: string, timeoutMs?: number): Promise<ShutdownMetadata>;
|
|
50
52
|
/** Get container IP in a network */
|
|
51
53
|
getIp(networkName: string): Promise<string>;
|
|
54
|
+
/** Get container resource statistics (CPU, memory, network) */
|
|
55
|
+
stats(): Promise<ContainerStats>;
|
|
56
|
+
/** Execute a command inside the container */
|
|
57
|
+
exec(cmd: string[], options?: Omit<ExecOptions, 'cmd'>): Promise<ExecResult>;
|
|
58
|
+
/** Stream container logs as an async iterable */
|
|
59
|
+
streamLogs(options?: LogOptions): Promise<AsyncIterable<LogLine>>;
|
|
52
60
|
/** Remove this container */
|
|
53
61
|
destroy(): Promise<void>;
|
|
54
62
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promise.d.ts","sourceRoot":"","sources":["../../src/api/promise.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"promise.d.ts","sourceRoot":"","sources":["../../src/api/promise.ts"],"names":[],"mappings":"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"}
|
package/dist/api/promise.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* Inspired by Alchemy.run and Pulumi's IAC patterns.
|
|
8
8
|
*/
|
|
9
9
|
import { Effect, Scope, Exit } from 'effect';
|
|
10
|
-
import { homedir } from 'os';
|
|
10
|
+
import { homedir, platform } from 'os';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
|
+
import { spawn } from 'child_process';
|
|
12
13
|
import Docker from 'dockerode';
|
|
13
14
|
// Import Effect-based resources (internal only)
|
|
14
15
|
import { Network as EffectNetwork, Container as EffectContainer, Image as EffectImage, } from '../resources';
|
|
@@ -44,6 +45,92 @@ function getDocker() {
|
|
|
44
45
|
}
|
|
45
46
|
return dockerClient;
|
|
46
47
|
}
|
|
48
|
+
// ============ Docker Auto-Start (Alchemy.run Style) ============
|
|
49
|
+
const DOCKER_STARTUP_TIMEOUT_MS = 60_000;
|
|
50
|
+
let dockerEnsured = false;
|
|
51
|
+
/**
|
|
52
|
+
* Start Docker Desktop on macOS/Windows if not running.
|
|
53
|
+
*/
|
|
54
|
+
function startDockerDesktop() {
|
|
55
|
+
const os = platform();
|
|
56
|
+
if (os === 'darwin') {
|
|
57
|
+
console.log('[Harpoon] Starting Docker Desktop...');
|
|
58
|
+
const child = spawn('open', ['-a', 'Docker', '--background'], {
|
|
59
|
+
detached: true,
|
|
60
|
+
stdio: 'ignore',
|
|
61
|
+
});
|
|
62
|
+
child.unref();
|
|
63
|
+
}
|
|
64
|
+
else if (os === 'win32') {
|
|
65
|
+
console.log('[Harpoon] Starting Docker Desktop...');
|
|
66
|
+
const child = spawn('cmd', ['/c', 'start', '', 'Docker Desktop'], {
|
|
67
|
+
detached: true,
|
|
68
|
+
stdio: 'ignore',
|
|
69
|
+
});
|
|
70
|
+
child.unref();
|
|
71
|
+
}
|
|
72
|
+
// Linux: Docker daemon managed by systemd, no auto-start
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Wait for Docker daemon to be ready with polling.
|
|
76
|
+
*/
|
|
77
|
+
async function waitForDockerReady(socketPath, timeoutMs) {
|
|
78
|
+
const start = Date.now();
|
|
79
|
+
let lastError = null;
|
|
80
|
+
while (Date.now() - start < timeoutMs) {
|
|
81
|
+
try {
|
|
82
|
+
if (existsSync(socketPath)) {
|
|
83
|
+
const docker = new Docker({ socketPath });
|
|
84
|
+
await docker.ping();
|
|
85
|
+
console.log('[Harpoon] Docker is ready');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
lastError = e;
|
|
91
|
+
}
|
|
92
|
+
await new Promise(r => setTimeout(r, 500));
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`[Harpoon] Docker not ready after ${timeoutMs}ms: ${lastError?.message}`);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Ensure Docker is running, auto-starting if needed.
|
|
98
|
+
* This is the Alchemy.run-style declarative approach.
|
|
99
|
+
*/
|
|
100
|
+
async function ensureDockerRunning() {
|
|
101
|
+
if (dockerEnsured)
|
|
102
|
+
return;
|
|
103
|
+
const socketPath = detectDockerSocket();
|
|
104
|
+
// Check if already running
|
|
105
|
+
if (existsSync(socketPath)) {
|
|
106
|
+
try {
|
|
107
|
+
const docker = new Docker({ socketPath });
|
|
108
|
+
await docker.ping();
|
|
109
|
+
dockerEnsured = true;
|
|
110
|
+
return; // Docker is ready
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Socket exists but daemon not responding, wait for it
|
|
114
|
+
console.log('[Harpoon] Docker socket exists but daemon not responding, waiting...');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Socket doesn't exist, start Docker
|
|
119
|
+
console.log('[Harpoon] Docker not running, attempting auto-start...');
|
|
120
|
+
startDockerDesktop();
|
|
121
|
+
}
|
|
122
|
+
// Wait for Docker to be ready
|
|
123
|
+
await waitForDockerReady(socketPath, DOCKER_STARTUP_TIMEOUT_MS);
|
|
124
|
+
dockerEnsured = true;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get Docker client with auto-start support.
|
|
128
|
+
* Ensures Docker is running before returning client.
|
|
129
|
+
*/
|
|
130
|
+
async function getDockerAsync() {
|
|
131
|
+
await ensureDockerRunning();
|
|
132
|
+
return getDocker();
|
|
133
|
+
}
|
|
47
134
|
/**
|
|
48
135
|
* Set a custom Docker client.
|
|
49
136
|
* Useful for testing or custom configurations.
|
|
@@ -100,7 +187,7 @@ function registerCleanup() {
|
|
|
100
187
|
*/
|
|
101
188
|
export async function Network(name, config = {}) {
|
|
102
189
|
registerCleanup();
|
|
103
|
-
const docker =
|
|
190
|
+
const docker = await getDockerAsync();
|
|
104
191
|
const scope = Effect.runSync(Scope.make());
|
|
105
192
|
const effectConfig = {
|
|
106
193
|
name,
|
|
@@ -139,7 +226,7 @@ export async function Network(name, config = {}) {
|
|
|
139
226
|
*/
|
|
140
227
|
export async function Container(name, config) {
|
|
141
228
|
registerCleanup();
|
|
142
|
-
const docker =
|
|
229
|
+
const docker = await getDockerAsync();
|
|
143
230
|
const scope = Effect.runSync(Scope.make());
|
|
144
231
|
// Convert NetworkResource to { name: string }
|
|
145
232
|
const networks = config.networks?.map((n) => 'destroy' in n ? { name: n.name } : n);
|
|
@@ -169,6 +256,15 @@ export async function Container(name, config) {
|
|
|
169
256
|
async getIp(networkName) {
|
|
170
257
|
return Effect.runPromise(effectResource.getIp(networkName));
|
|
171
258
|
},
|
|
259
|
+
async stats() {
|
|
260
|
+
return Effect.runPromise(effectResource.stats());
|
|
261
|
+
},
|
|
262
|
+
async exec(cmd, options) {
|
|
263
|
+
return Effect.runPromise(effectResource.exec({ cmd, ...options }));
|
|
264
|
+
},
|
|
265
|
+
async streamLogs(options) {
|
|
266
|
+
return Effect.runPromise(effectResource.streamLogs(options));
|
|
267
|
+
},
|
|
172
268
|
async destroy() {
|
|
173
269
|
await cleanup();
|
|
174
270
|
resources.delete(resourceId);
|
|
@@ -199,7 +295,7 @@ export async function Container(name, config) {
|
|
|
199
295
|
*/
|
|
200
296
|
export async function database(name, config) {
|
|
201
297
|
registerCleanup();
|
|
202
|
-
const docker =
|
|
298
|
+
const docker = await getDockerAsync();
|
|
203
299
|
const scope = Effect.runSync(Scope.make());
|
|
204
300
|
// Convert NetworkResource to { name: string }
|
|
205
301
|
const networks = config.networks?.map((n) => 'destroy' in n ? { name: n.name } : n);
|
|
@@ -232,6 +328,15 @@ export async function database(name, config) {
|
|
|
232
328
|
async getIp(networkName) {
|
|
233
329
|
return Effect.runPromise(effectResource.getIp(networkName));
|
|
234
330
|
},
|
|
331
|
+
async stats() {
|
|
332
|
+
return Effect.runPromise(effectResource.stats());
|
|
333
|
+
},
|
|
334
|
+
async exec(cmd, options) {
|
|
335
|
+
return Effect.runPromise(effectResource.exec({ cmd, ...options }));
|
|
336
|
+
},
|
|
337
|
+
async streamLogs(options) {
|
|
338
|
+
return Effect.runPromise(effectResource.streamLogs(options));
|
|
339
|
+
},
|
|
235
340
|
async destroy() {
|
|
236
341
|
await cleanup();
|
|
237
342
|
resources.delete(resourceId);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @harpoon/core
|
|
2
|
+
* @docker-harpoon/core
|
|
3
3
|
*
|
|
4
4
|
* Infrastructure as Code for Docker testing environments.
|
|
5
5
|
*
|
|
6
6
|
* Simple Promise-based API:
|
|
7
7
|
*
|
|
8
8
|
* ```typescript
|
|
9
|
-
* import { Network, Container, database } from '@harpoon/core';
|
|
9
|
+
* import { Network, Container, database } from '@docker-harpoon/core';
|
|
10
10
|
*
|
|
11
11
|
* const network = await Network("my-net");
|
|
12
12
|
* const db = await database("postgres", { image: "postgres:16" });
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* - Binding interface for env var injection and lifecycle hooks
|
|
28
28
|
* - BuildBinding interface for Dockerfile transformations
|
|
29
29
|
*/
|
|
30
|
-
export { Network, Container, database, Image, setDocker, resetDocker, destroyAll, type NetworkConfig, type NetworkResource, type ContainerConfig, type ContainerResource, type PortMapping, type ShutdownMetadata, type DatabaseConfig, type DatabaseResource, type ImageConfig, type ImageResource, } from './api';
|
|
30
|
+
export { Network, Container, database, Image, setDocker, resetDocker, destroyAll, type NetworkConfig, type NetworkResource, type ContainerConfig, type ContainerResource, type PortMapping, type ShutdownMetadata, type DatabaseConfig, type DatabaseResource, type ImageConfig, type ImageResource, type ContainerStats, type ExecOptions, type ExecResult, type LogOptions, type LogLine, } from './api';
|
|
31
31
|
export type { Binding, BuildBinding, BindingsEnv, FlatBindingsEnv, } from './bindings';
|
|
32
32
|
export { isBuildBinding, mergeBindingsEnv, createEnvBinding, } from './bindings';
|
|
33
33
|
export { HarpoonError, NetworkError, ImageError, ContainerError, ScopeError, BindingError, toHarpoonError, asCause, isHarpoonError, isNetworkError, isImageError, isContainerError, isScopeError, isBindingError, } from './errors';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,EAEL,OAAO,EACP,SAAS,EACT,QAAQ,EACR,KAAK,EAGL,SAAS,EACT,WAAW,EACX,UAAU,EAGV,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,aAAa,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,EAEL,OAAO,EACP,SAAS,EACT,QAAQ,EACR,KAAK,EAGL,SAAS,EACT,WAAW,EACX,UAAU,EAGV,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,aAAa,EAGlB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,OAAO,GACb,MAAM,OAAO,CAAC;AAIf,YAAY,EACV,OAAO,EACP,YAAY,EACZ,WAAW,EACX,eAAe,GAChB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAIpB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,cAAc,EACd,UAAU,EACV,YAAY,EACZ,cAAc,EACd,OAAO,EACP,cAAc,EACd,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,cAAc,GACf,MAAM,UAAU,CAAC;AAElB,YAAY,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAIhD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,qBAAqB,GACtB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,aAAa,EACb,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,EACrB,8BAA8B,EAC9B,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,WAAW,EACX,MAAM,EACN,gBAAgB,GACjB,MAAM,2BAA2B,CAAC;AAEnC,YAAY,EACV,sBAAsB,EACtB,qBAAqB,EACrB,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,GAChB,MAAM,2BAA2B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @harpoon/core
|
|
2
|
+
* @docker-harpoon/core
|
|
3
3
|
*
|
|
4
4
|
* Infrastructure as Code for Docker testing environments.
|
|
5
5
|
*
|
|
6
6
|
* Simple Promise-based API:
|
|
7
7
|
*
|
|
8
8
|
* ```typescript
|
|
9
|
-
* import { Network, Container, database } from '@harpoon/core';
|
|
9
|
+
* import { Network, Container, database } from '@docker-harpoon/core';
|
|
10
10
|
*
|
|
11
11
|
* const network = await Network("my-net");
|
|
12
12
|
* const db = await database("postgres", { image: "postgres:16" });
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { Effect, Scope } from 'effect';
|
|
10
10
|
import Docker from 'dockerode';
|
|
11
11
|
import type { Binding } from '../bindings/types';
|
|
12
|
+
import { type ContainerStats, type ExecOptions, type ExecResult, type LogOptions, type LogLine } from './schemas';
|
|
12
13
|
export interface PortMapping {
|
|
13
14
|
internal: number;
|
|
14
15
|
external: number;
|
|
@@ -34,9 +35,18 @@ export interface ShutdownMetadata {
|
|
|
34
35
|
export interface ContainerResource {
|
|
35
36
|
readonly id: string;
|
|
36
37
|
readonly name: string;
|
|
38
|
+
/** Wait for a log pattern to appear */
|
|
37
39
|
readonly waitForLog: (pattern: string | RegExp, timeoutMs?: number) => Effect.Effect<void, Error>;
|
|
40
|
+
/** Stop container gracefully with signal */
|
|
38
41
|
readonly stopGracefully: (signal?: string, timeoutMs?: number) => Effect.Effect<ShutdownMetadata, Error>;
|
|
42
|
+
/** Get container IP in a specific network */
|
|
39
43
|
readonly getIp: (networkName: string) => Effect.Effect<string, Error>;
|
|
44
|
+
/** Get container resource statistics (CPU, memory, network) */
|
|
45
|
+
readonly stats: () => Effect.Effect<ContainerStats, Error>;
|
|
46
|
+
/** Execute a command inside the container */
|
|
47
|
+
readonly exec: (options: ExecOptions) => Effect.Effect<ExecResult, Error>;
|
|
48
|
+
/** Stream container logs as an async iterable */
|
|
49
|
+
readonly streamLogs: (options?: LogOptions) => Effect.Effect<AsyncIterable<LogLine>, Error>;
|
|
40
50
|
}
|
|
41
51
|
export declare const Container: (resourceId: string, config: ContainerConfig, docker: Docker) => Effect.Effect<ContainerResource, Error, Scope.Scope>;
|
|
42
52
|
//# sourceMappingURL=container.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../../src/resources/container.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,MAAM,EAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../../src/resources/container.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,MAAM,EAAE,KAAK,EAA0B,MAAM,QAAQ,CAAC;AAC/D,OAAO,MAAM,MAAM,WAAW,CAAC;AAE/B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAKL,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,OAAO,EACb,MAAM,WAAW,CAAC;AAEnB,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,QAAQ,CAAC,UAAU,EAAE,CACnB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,SAAS,CAAC,EAAE,MAAM,KACf,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAChC,4CAA4C;IAC5C,QAAQ,CAAC,cAAc,EAAE,CACvB,MAAM,CAAC,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,KACf,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;IAC5C,6CAA6C;IAC7C,QAAQ,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACtE,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;IAC3D,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAC1E,iDAAiD;IACjD,QAAQ,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC;CAC7F;AA6ED,eAAO,MAAM,SAAS,GACpB,YAAY,MAAM,EAClB,QAAQ,eAAe,EACvB,QAAQ,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAuhBnD,CAAC"}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
import { Effect, Schedule, Exit } from 'effect';
|
|
10
10
|
import { toHarpoonError, ContainerError } from '../errors';
|
|
11
11
|
import { mergeBindingsEnv } from '../bindings/types';
|
|
12
|
-
|
|
12
|
+
import { containerStatsSchema, execOptionsSchema, execResultSchema, logOptionsSchema, } from './schemas';
|
|
13
|
+
const CLEANUP_TIMEOUT_MS = 30_000; // 30 seconds for database graceful shutdown
|
|
13
14
|
const safeRemoveContainer = async (docker, containerId, containerName) => {
|
|
14
15
|
const container = docker.getContainer(containerId);
|
|
15
16
|
try {
|
|
@@ -19,16 +20,27 @@ const safeRemoveContainer = async (docker, containerId, containerName) => {
|
|
|
19
20
|
try {
|
|
20
21
|
await container.kill({ signal: 'SIGTERM' });
|
|
21
22
|
await new Promise((resolve) => {
|
|
23
|
+
let resolved = false;
|
|
24
|
+
const safeResolve = () => {
|
|
25
|
+
if (!resolved) {
|
|
26
|
+
resolved = true;
|
|
27
|
+
clearTimeout(timeout);
|
|
28
|
+
resolve();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
22
31
|
const checkStopped = async () => {
|
|
32
|
+
if (resolved)
|
|
33
|
+
return;
|
|
23
34
|
try {
|
|
24
35
|
const state = await container.inspect();
|
|
25
36
|
if (!state.State.Running) {
|
|
26
|
-
|
|
37
|
+
safeResolve();
|
|
27
38
|
return;
|
|
28
39
|
}
|
|
29
40
|
}
|
|
30
41
|
catch {
|
|
31
|
-
|
|
42
|
+
// Container gone or error - consider it stopped
|
|
43
|
+
safeResolve();
|
|
32
44
|
return;
|
|
33
45
|
}
|
|
34
46
|
setTimeout(checkStopped, 100);
|
|
@@ -36,9 +48,9 @@ const safeRemoveContainer = async (docker, containerId, containerName) => {
|
|
|
36
48
|
const timeout = setTimeout(() => {
|
|
37
49
|
console.warn(`[Cleanup] Container ${containerName} did not stop gracefully, forcing`);
|
|
38
50
|
container.kill({ signal: 'SIGKILL' }).catch(() => { });
|
|
39
|
-
|
|
51
|
+
safeResolve();
|
|
40
52
|
}, 5000);
|
|
41
|
-
checkStopped()
|
|
53
|
+
checkStopped();
|
|
42
54
|
});
|
|
43
55
|
}
|
|
44
56
|
catch (e) {
|
|
@@ -222,6 +234,211 @@ export const Container = (resourceId, config, docker) => Effect.acquireRelease(E
|
|
|
222
234
|
},
|
|
223
235
|
catch: (e) => new Error(`getIp failed: ${e}`),
|
|
224
236
|
}),
|
|
237
|
+
/**
|
|
238
|
+
* Get container resource statistics (CPU, memory, network).
|
|
239
|
+
* Uses dockerode stats() with one-shot mode for a single snapshot.
|
|
240
|
+
*/
|
|
241
|
+
stats: () => Effect.tryPromise({
|
|
242
|
+
try: async () => {
|
|
243
|
+
const statsData = await container.stats({ stream: false });
|
|
244
|
+
// Calculate CPU percentage
|
|
245
|
+
const cpuDelta = statsData.cpu_stats.cpu_usage.total_usage -
|
|
246
|
+
statsData.precpu_stats.cpu_usage.total_usage;
|
|
247
|
+
const systemDelta = statsData.cpu_stats.system_cpu_usage -
|
|
248
|
+
statsData.precpu_stats.system_cpu_usage;
|
|
249
|
+
const cpuCores = statsData.cpu_stats.online_cpus || 1;
|
|
250
|
+
const cpuPercent = systemDelta > 0
|
|
251
|
+
? (cpuDelta / systemDelta) * cpuCores * 100
|
|
252
|
+
: 0;
|
|
253
|
+
// Calculate memory stats
|
|
254
|
+
const memUsage = statsData.memory_stats?.usage ?? 0;
|
|
255
|
+
const memLimit = statsData.memory_stats?.limit ?? 0;
|
|
256
|
+
const memPercent = memLimit > 0 ? (memUsage / memLimit) * 100 : 0;
|
|
257
|
+
// Calculate network stats (sum all interfaces)
|
|
258
|
+
const networks = statsData.networks ?? {};
|
|
259
|
+
let rxBytes = 0;
|
|
260
|
+
let txBytes = 0;
|
|
261
|
+
let rxPackets = 0;
|
|
262
|
+
let txPackets = 0;
|
|
263
|
+
for (const iface of Object.values(networks)) {
|
|
264
|
+
rxBytes += iface.rx_bytes ?? 0;
|
|
265
|
+
txBytes += iface.tx_bytes ?? 0;
|
|
266
|
+
rxPackets += iface.rx_packets ?? 0;
|
|
267
|
+
txPackets += iface.tx_packets ?? 0;
|
|
268
|
+
}
|
|
269
|
+
const result = {
|
|
270
|
+
cpu: {
|
|
271
|
+
usage: statsData.cpu_stats.cpu_usage.total_usage,
|
|
272
|
+
system: statsData.cpu_stats.system_cpu_usage,
|
|
273
|
+
percent: Math.round(cpuPercent * 100) / 100,
|
|
274
|
+
cores: cpuCores,
|
|
275
|
+
},
|
|
276
|
+
memory: {
|
|
277
|
+
usage: memUsage,
|
|
278
|
+
limit: memLimit,
|
|
279
|
+
percent: Math.round(memPercent * 100) / 100,
|
|
280
|
+
usageMB: Math.round((memUsage / 1024 / 1024) * 100) / 100,
|
|
281
|
+
limitMB: Math.round((memLimit / 1024 / 1024) * 100) / 100,
|
|
282
|
+
},
|
|
283
|
+
network: {
|
|
284
|
+
rxBytes,
|
|
285
|
+
txBytes,
|
|
286
|
+
rxPackets,
|
|
287
|
+
txPackets,
|
|
288
|
+
},
|
|
289
|
+
timestamp: new Date().toISOString(),
|
|
290
|
+
containerId: container.id,
|
|
291
|
+
};
|
|
292
|
+
// Validate with Zod schema
|
|
293
|
+
return containerStatsSchema.parse(result);
|
|
294
|
+
},
|
|
295
|
+
catch: (e) => new Error(`stats failed: ${e}`),
|
|
296
|
+
}),
|
|
297
|
+
/**
|
|
298
|
+
* Execute a command inside the container.
|
|
299
|
+
* Returns stdout, stderr, and exit code.
|
|
300
|
+
*/
|
|
301
|
+
exec: (options) => Effect.tryPromise({
|
|
302
|
+
try: async () => {
|
|
303
|
+
const startTime = Date.now();
|
|
304
|
+
// Validate input with Zod
|
|
305
|
+
const validated = execOptionsSchema.parse(options);
|
|
306
|
+
// Create exec instance
|
|
307
|
+
const exec = await container.exec({
|
|
308
|
+
Cmd: validated.cmd,
|
|
309
|
+
AttachStdout: true,
|
|
310
|
+
AttachStderr: true,
|
|
311
|
+
Env: validated.env
|
|
312
|
+
? Object.entries(validated.env).map(([k, v]) => `${k}=${v}`)
|
|
313
|
+
: undefined,
|
|
314
|
+
WorkingDir: validated.workingDir,
|
|
315
|
+
User: validated.user,
|
|
316
|
+
Privileged: validated.privileged,
|
|
317
|
+
Tty: validated.tty,
|
|
318
|
+
});
|
|
319
|
+
// Start exec and capture output
|
|
320
|
+
const stream = await exec.start({ Detach: false, Tty: validated.tty ?? false });
|
|
321
|
+
// Collect output chunks
|
|
322
|
+
const stdoutChunks = [];
|
|
323
|
+
const stderrChunks = [];
|
|
324
|
+
await new Promise((resolve, reject) => {
|
|
325
|
+
// Docker multiplexes stdout/stderr in the stream
|
|
326
|
+
// First 8 bytes of each frame indicate stream type and size
|
|
327
|
+
stream.on('data', (chunk) => {
|
|
328
|
+
// For TTY mode, all output goes to stdout
|
|
329
|
+
if (validated.tty) {
|
|
330
|
+
stdoutChunks.push(chunk);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Parse multiplexed stream (Docker stream format)
|
|
334
|
+
let offset = 0;
|
|
335
|
+
while (offset < chunk.length) {
|
|
336
|
+
if (offset + 8 > chunk.length)
|
|
337
|
+
break;
|
|
338
|
+
const streamType = chunk.readUInt8(offset);
|
|
339
|
+
const size = chunk.readUInt32BE(offset + 4);
|
|
340
|
+
const payload = chunk.slice(offset + 8, offset + 8 + size);
|
|
341
|
+
if (streamType === 1) {
|
|
342
|
+
stdoutChunks.push(payload);
|
|
343
|
+
}
|
|
344
|
+
else if (streamType === 2) {
|
|
345
|
+
stderrChunks.push(payload);
|
|
346
|
+
}
|
|
347
|
+
offset += 8 + size;
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
stream.on('end', resolve);
|
|
351
|
+
stream.on('error', reject);
|
|
352
|
+
});
|
|
353
|
+
// Get exit code
|
|
354
|
+
const inspection = await exec.inspect();
|
|
355
|
+
const exitCode = inspection.ExitCode ?? 0;
|
|
356
|
+
const durationMs = Date.now() - startTime;
|
|
357
|
+
const result = {
|
|
358
|
+
exitCode,
|
|
359
|
+
stdout: Buffer.concat(stdoutChunks).toString('utf-8'),
|
|
360
|
+
stderr: Buffer.concat(stderrChunks).toString('utf-8'),
|
|
361
|
+
durationMs,
|
|
362
|
+
};
|
|
363
|
+
// Validate output with Zod
|
|
364
|
+
return execResultSchema.parse(result);
|
|
365
|
+
},
|
|
366
|
+
catch: (e) => new Error(`exec failed: ${e}`),
|
|
367
|
+
}),
|
|
368
|
+
/**
|
|
369
|
+
* Stream container logs as an async iterable.
|
|
370
|
+
* Yields LogLine objects with stream type and message.
|
|
371
|
+
*/
|
|
372
|
+
streamLogs: (options) => Effect.tryPromise({
|
|
373
|
+
try: async () => {
|
|
374
|
+
// Validate and apply defaults
|
|
375
|
+
const opts = logOptionsSchema.parse(options ?? {});
|
|
376
|
+
const stdout = opts.stdout ?? true;
|
|
377
|
+
const stderr = opts.stderr ?? true;
|
|
378
|
+
const timestamps = opts.timestamps ?? false;
|
|
379
|
+
const stream = await container.logs({
|
|
380
|
+
follow: true,
|
|
381
|
+
stdout,
|
|
382
|
+
stderr,
|
|
383
|
+
since: opts.since,
|
|
384
|
+
until: opts.until,
|
|
385
|
+
tail: opts.tail,
|
|
386
|
+
timestamps,
|
|
387
|
+
});
|
|
388
|
+
// Create a wrapper to make the stream async iterable
|
|
389
|
+
const readable = stream;
|
|
390
|
+
// Convert NodeJS.ReadableStream to AsyncIterable<LogLine>
|
|
391
|
+
async function* logIterator() {
|
|
392
|
+
const chunks = [];
|
|
393
|
+
// Use Promise-based iteration for ReadableStream
|
|
394
|
+
for await (const chunk of readable) {
|
|
395
|
+
// Parse Docker log format (8-byte header + payload)
|
|
396
|
+
let offset = 0;
|
|
397
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
398
|
+
while (offset < buffer.length) {
|
|
399
|
+
if (offset + 8 > buffer.length) {
|
|
400
|
+
// Incomplete header, yield rest as stdout
|
|
401
|
+
yield {
|
|
402
|
+
stream: 'stdout',
|
|
403
|
+
message: buffer.slice(offset).toString('utf-8').trim(),
|
|
404
|
+
};
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
const streamType = buffer.readUInt8(offset);
|
|
408
|
+
const size = buffer.readUInt32BE(offset + 4);
|
|
409
|
+
if (offset + 8 + size > buffer.length) {
|
|
410
|
+
// Incomplete payload
|
|
411
|
+
yield {
|
|
412
|
+
stream: streamType === 2 ? 'stderr' : 'stdout',
|
|
413
|
+
message: buffer.slice(offset + 8).toString('utf-8').trim(),
|
|
414
|
+
};
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
const payload = buffer.slice(offset + 8, offset + 8 + size);
|
|
418
|
+
const message = payload.toString('utf-8');
|
|
419
|
+
// Parse timestamp if present (format: "2024-01-14T21:00:00.000000000Z message")
|
|
420
|
+
let timestamp;
|
|
421
|
+
let content = message;
|
|
422
|
+
if (timestamps) {
|
|
423
|
+
const match = message.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\s+(.*)$/s);
|
|
424
|
+
if (match) {
|
|
425
|
+
timestamp = match[1];
|
|
426
|
+
content = match[2] ?? '';
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
yield {
|
|
430
|
+
stream: streamType === 2 ? 'stderr' : 'stdout',
|
|
431
|
+
timestamp,
|
|
432
|
+
message: content.trim(),
|
|
433
|
+
};
|
|
434
|
+
offset += 8 + size;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return logIterator();
|
|
439
|
+
},
|
|
440
|
+
catch: (e) => new Error(`streamLogs failed: ${e}`),
|
|
441
|
+
}),
|
|
225
442
|
};
|
|
226
443
|
// Call onStart hooks for bindings
|
|
227
444
|
// This is done in Effect context for proper error handling
|
|
@@ -7,6 +7,8 @@ export { Network } from './network';
|
|
|
7
7
|
export type { NetworkConfig, NetworkResource } from './network';
|
|
8
8
|
export { Container } from './container';
|
|
9
9
|
export type { ContainerConfig, ContainerResource, PortMapping, ShutdownMetadata, } from './container';
|
|
10
|
+
export { containerStatsSchema, execOptionsSchema, execResultSchema, logOptionsSchema, logLineSchema, cpuStatsSchema, memoryStatsSchema, networkStatsSchema, statsOptionsSchema, } from './schemas';
|
|
11
|
+
export type { ContainerStats, CpuStats, MemoryStats, NetworkStats, ExecOptions, ExecResult, LogOptions, LogLine, StatsOptions, } from './schemas';
|
|
10
12
|
export { Image } from './image';
|
|
11
13
|
export type { ImageConfig, ImageResource } from './image';
|
|
12
14
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEhE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,gBAAgB,GACjB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEhE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,cAAc,EACd,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,WAAW,EACX,UAAU,EACV,UAAU,EACV,OAAO,EACP,YAAY,GACb,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/resources/index.js
CHANGED
|
@@ -5,4 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { Network } from './network';
|
|
7
7
|
export { Container } from './container';
|
|
8
|
+
// Container resource schemas and types
|
|
9
|
+
export { containerStatsSchema, execOptionsSchema, execResultSchema, logOptionsSchema, logLineSchema, cpuStatsSchema, memoryStatsSchema, networkStatsSchema, statsOptionsSchema, } from './schemas';
|
|
8
10
|
export { Image } from './image';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod Schemas for Container Resources
|
|
3
|
+
*
|
|
4
|
+
* Defines validation schemas for container stats, exec, and log streaming.
|
|
5
|
+
* Following Alchemy.run patterns for declarative resource definitions.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
/**
|
|
9
|
+
* CPU statistics from Docker container
|
|
10
|
+
*/
|
|
11
|
+
export declare const cpuStatsSchema: z.ZodObject<{
|
|
12
|
+
usage: z.ZodNumber;
|
|
13
|
+
system: z.ZodNumber;
|
|
14
|
+
percent: z.ZodNumber;
|
|
15
|
+
cores: z.ZodNumber;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
/**
|
|
18
|
+
* Memory statistics from Docker container
|
|
19
|
+
*/
|
|
20
|
+
export declare const memoryStatsSchema: z.ZodObject<{
|
|
21
|
+
usage: z.ZodNumber;
|
|
22
|
+
limit: z.ZodNumber;
|
|
23
|
+
percent: z.ZodNumber;
|
|
24
|
+
usageMB: z.ZodNumber;
|
|
25
|
+
limitMB: z.ZodNumber;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
/**
|
|
28
|
+
* Network I/O statistics from Docker container
|
|
29
|
+
*/
|
|
30
|
+
export declare const networkStatsSchema: z.ZodObject<{
|
|
31
|
+
rxBytes: z.ZodNumber;
|
|
32
|
+
txBytes: z.ZodNumber;
|
|
33
|
+
rxPackets: z.ZodNumber;
|
|
34
|
+
txPackets: z.ZodNumber;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
/**
|
|
37
|
+
* Complete container statistics
|
|
38
|
+
*/
|
|
39
|
+
export declare const containerStatsSchema: z.ZodObject<{
|
|
40
|
+
cpu: z.ZodObject<{
|
|
41
|
+
usage: z.ZodNumber;
|
|
42
|
+
system: z.ZodNumber;
|
|
43
|
+
percent: z.ZodNumber;
|
|
44
|
+
cores: z.ZodNumber;
|
|
45
|
+
}, z.core.$strip>;
|
|
46
|
+
memory: z.ZodObject<{
|
|
47
|
+
usage: z.ZodNumber;
|
|
48
|
+
limit: z.ZodNumber;
|
|
49
|
+
percent: z.ZodNumber;
|
|
50
|
+
usageMB: z.ZodNumber;
|
|
51
|
+
limitMB: z.ZodNumber;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
network: z.ZodObject<{
|
|
54
|
+
rxBytes: z.ZodNumber;
|
|
55
|
+
txBytes: z.ZodNumber;
|
|
56
|
+
rxPackets: z.ZodNumber;
|
|
57
|
+
txPackets: z.ZodNumber;
|
|
58
|
+
}, z.core.$strip>;
|
|
59
|
+
timestamp: z.ZodString;
|
|
60
|
+
containerId: z.ZodString;
|
|
61
|
+
}, z.core.$strip>;
|
|
62
|
+
export type ContainerStats = z.infer<typeof containerStatsSchema>;
|
|
63
|
+
export type CpuStats = z.infer<typeof cpuStatsSchema>;
|
|
64
|
+
export type MemoryStats = z.infer<typeof memoryStatsSchema>;
|
|
65
|
+
export type NetworkStats = z.infer<typeof networkStatsSchema>;
|
|
66
|
+
/**
|
|
67
|
+
* Options for executing a command in a container
|
|
68
|
+
*/
|
|
69
|
+
export declare const execOptionsSchema: z.ZodObject<{
|
|
70
|
+
cmd: z.ZodArray<z.ZodString>;
|
|
71
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
72
|
+
workingDir: z.ZodOptional<z.ZodString>;
|
|
73
|
+
user: z.ZodOptional<z.ZodString>;
|
|
74
|
+
privileged: z.ZodOptional<z.ZodBoolean>;
|
|
75
|
+
tty: z.ZodOptional<z.ZodBoolean>;
|
|
76
|
+
}, z.core.$strip>;
|
|
77
|
+
/**
|
|
78
|
+
* Result of executing a command in a container
|
|
79
|
+
*/
|
|
80
|
+
export declare const execResultSchema: z.ZodObject<{
|
|
81
|
+
exitCode: z.ZodNumber;
|
|
82
|
+
stdout: z.ZodString;
|
|
83
|
+
stderr: z.ZodString;
|
|
84
|
+
durationMs: z.ZodNumber;
|
|
85
|
+
}, z.core.$strip>;
|
|
86
|
+
export type ExecOptions = z.infer<typeof execOptionsSchema>;
|
|
87
|
+
export type ExecResult = z.infer<typeof execResultSchema>;
|
|
88
|
+
/**
|
|
89
|
+
* Options for streaming container logs
|
|
90
|
+
*/
|
|
91
|
+
export declare const logOptionsSchema: z.ZodObject<{
|
|
92
|
+
stdout: z.ZodOptional<z.ZodBoolean>;
|
|
93
|
+
stderr: z.ZodOptional<z.ZodBoolean>;
|
|
94
|
+
since: z.ZodOptional<z.ZodNumber>;
|
|
95
|
+
until: z.ZodOptional<z.ZodNumber>;
|
|
96
|
+
tail: z.ZodOptional<z.ZodNumber>;
|
|
97
|
+
timestamps: z.ZodOptional<z.ZodBoolean>;
|
|
98
|
+
}, z.core.$strip>;
|
|
99
|
+
/**
|
|
100
|
+
* A single log line from a container
|
|
101
|
+
*/
|
|
102
|
+
export declare const logLineSchema: z.ZodObject<{
|
|
103
|
+
stream: z.ZodEnum<{
|
|
104
|
+
stdout: "stdout";
|
|
105
|
+
stderr: "stderr";
|
|
106
|
+
}>;
|
|
107
|
+
timestamp: z.ZodOptional<z.ZodString>;
|
|
108
|
+
message: z.ZodString;
|
|
109
|
+
}, z.core.$strip>;
|
|
110
|
+
export type LogOptions = z.infer<typeof logOptionsSchema>;
|
|
111
|
+
export type LogLine = z.infer<typeof logLineSchema>;
|
|
112
|
+
/**
|
|
113
|
+
* Options for getting container stats
|
|
114
|
+
*/
|
|
115
|
+
export declare const statsOptionsSchema: z.ZodObject<{
|
|
116
|
+
stream: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
117
|
+
oneShot: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
118
|
+
}, z.core.$strip>;
|
|
119
|
+
export type StatsOptions = z.infer<typeof statsOptionsSchema>;
|
|
120
|
+
//# sourceMappingURL=schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/resources/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;GAEG;AACH,eAAO,MAAM,cAAc;;;;;iBAKzB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;iBAM5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;iBAK7B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;iBAM/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;iBAO5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;iBAK3B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;;;iBAO3B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;iBAIxB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC1D,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAIpD;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod Schemas for Container Resources
|
|
3
|
+
*
|
|
4
|
+
* Defines validation schemas for container stats, exec, and log streaming.
|
|
5
|
+
* Following Alchemy.run patterns for declarative resource definitions.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
// ============ Container Stats Schema ============
|
|
9
|
+
/**
|
|
10
|
+
* CPU statistics from Docker container
|
|
11
|
+
*/
|
|
12
|
+
export const cpuStatsSchema = z.object({
|
|
13
|
+
usage: z.number().min(0).describe('CPU usage in nanoseconds'),
|
|
14
|
+
system: z.number().min(0).describe('System CPU usage in nanoseconds'),
|
|
15
|
+
percent: z.number().min(0).max(100).describe('CPU usage as percentage'),
|
|
16
|
+
cores: z.number().int().min(0).describe('Number of CPU cores available'),
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Memory statistics from Docker container
|
|
20
|
+
*/
|
|
21
|
+
export const memoryStatsSchema = z.object({
|
|
22
|
+
usage: z.number().min(0).describe('Current memory usage in bytes'),
|
|
23
|
+
limit: z.number().min(0).describe('Memory limit in bytes'),
|
|
24
|
+
percent: z.number().min(0).max(100).describe('Memory usage as percentage'),
|
|
25
|
+
usageMB: z.number().min(0).describe('Current memory usage in megabytes'),
|
|
26
|
+
limitMB: z.number().min(0).describe('Memory limit in megabytes'),
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* Network I/O statistics from Docker container
|
|
30
|
+
*/
|
|
31
|
+
export const networkStatsSchema = z.object({
|
|
32
|
+
rxBytes: z.number().min(0).describe('Bytes received'),
|
|
33
|
+
txBytes: z.number().min(0).describe('Bytes transmitted'),
|
|
34
|
+
rxPackets: z.number().min(0).describe('Packets received'),
|
|
35
|
+
txPackets: z.number().min(0).describe('Packets transmitted'),
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Complete container statistics
|
|
39
|
+
*/
|
|
40
|
+
export const containerStatsSchema = z.object({
|
|
41
|
+
cpu: cpuStatsSchema,
|
|
42
|
+
memory: memoryStatsSchema,
|
|
43
|
+
network: networkStatsSchema,
|
|
44
|
+
timestamp: z.string().datetime().describe('ISO 8601 timestamp of stats collection'),
|
|
45
|
+
containerId: z.string().describe('Docker container ID'),
|
|
46
|
+
});
|
|
47
|
+
// ============ Exec Schemas ============
|
|
48
|
+
/**
|
|
49
|
+
* Options for executing a command in a container
|
|
50
|
+
*/
|
|
51
|
+
export const execOptionsSchema = z.object({
|
|
52
|
+
cmd: z.array(z.string()).min(1).describe('Command and arguments to execute'),
|
|
53
|
+
env: z.record(z.string(), z.string()).optional().describe('Environment variables'),
|
|
54
|
+
workingDir: z.string().optional().describe('Working directory for the command'),
|
|
55
|
+
user: z.string().optional().describe('User to run the command as'),
|
|
56
|
+
privileged: z.boolean().optional().describe('Run with elevated privileges'),
|
|
57
|
+
tty: z.boolean().optional().describe('Allocate a pseudo-TTY'),
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Result of executing a command in a container
|
|
61
|
+
*/
|
|
62
|
+
export const execResultSchema = z.object({
|
|
63
|
+
exitCode: z.number().int().describe('Exit code of the command'),
|
|
64
|
+
stdout: z.string().describe('Standard output'),
|
|
65
|
+
stderr: z.string().describe('Standard error'),
|
|
66
|
+
durationMs: z.number().min(0).describe('Execution duration in milliseconds'),
|
|
67
|
+
});
|
|
68
|
+
// ============ Log Streaming Schemas ============
|
|
69
|
+
/**
|
|
70
|
+
* Options for streaming container logs
|
|
71
|
+
*/
|
|
72
|
+
export const logOptionsSchema = z.object({
|
|
73
|
+
stdout: z.boolean().optional().describe('Include stdout (default: true)'),
|
|
74
|
+
stderr: z.boolean().optional().describe('Include stderr (default: true)'),
|
|
75
|
+
since: z.number().optional().describe('Unix timestamp to start from'),
|
|
76
|
+
until: z.number().optional().describe('Unix timestamp to end at'),
|
|
77
|
+
tail: z.number().int().min(0).optional().describe('Number of lines to tail'),
|
|
78
|
+
timestamps: z.boolean().optional().describe('Include timestamps (default: false)'),
|
|
79
|
+
});
|
|
80
|
+
/**
|
|
81
|
+
* A single log line from a container
|
|
82
|
+
*/
|
|
83
|
+
export const logLineSchema = z.object({
|
|
84
|
+
stream: z.enum(['stdout', 'stderr']).describe('Which stream this line came from'),
|
|
85
|
+
timestamp: z.string().datetime().optional().describe('Timestamp if enabled'),
|
|
86
|
+
message: z.string().describe('Log message content'),
|
|
87
|
+
});
|
|
88
|
+
// ============ Stats Options Schema ============
|
|
89
|
+
/**
|
|
90
|
+
* Options for getting container stats
|
|
91
|
+
*/
|
|
92
|
+
export const statsOptionsSchema = z.object({
|
|
93
|
+
stream: z.boolean().optional().default(false).describe('Stream continuous stats'),
|
|
94
|
+
oneShot: z.boolean().optional().default(true).describe('Get single stats snapshot'),
|
|
95
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docker-harpoon/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Core Docker resource primitives and binding interface",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"import": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
-
"files": [
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
16
18
|
"scripts": {
|
|
17
19
|
"build": "tsc",
|
|
18
20
|
"typecheck": "tsc --noEmit"
|