@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.
@@ -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
@@ -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,GACnB,MAAM,WAAW,CAAC"}
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"}
@@ -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":"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"}
1
+ {"version":3,"file":"promise.d.ts","sourceRoot":"","sources":["../../src/api/promise.ts"],"names":[],"mappings":"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"}
@@ -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 = getDocker();
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 = getDocker();
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 = getDocker();
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';
@@ -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,GACnB,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"}
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,EAAkB,MAAM,QAAQ,CAAC;AACvD,OAAO,MAAM,MAAM,WAAW,CAAC;AAE/B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAGjD,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,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,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,QAAQ,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;CACvE;AAgED,eAAO,MAAM,SAAS,GACpB,YAAY,MAAM,EAClB,QAAQ,eAAe,EACvB,QAAQ,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAsSnD,CAAC"}
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
- const CLEANUP_TIMEOUT_MS = 10_000;
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
- resolve();
37
+ safeResolve();
27
38
  return;
28
39
  }
29
40
  }
30
41
  catch {
31
- resolve();
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
- resolve();
51
+ safeResolve();
40
52
  }, 5000);
41
- checkStopped().finally(() => clearTimeout(timeout));
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;AAErB,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,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"}
@@ -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.0",
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": ["dist"],
15
+ "files": [
16
+ "dist"
17
+ ],
16
18
  "scripts": {
17
19
  "build": "tsc",
18
20
  "typecheck": "tsc --noEmit"