@compforge/harness-toolbox 0.1.2 → 0.3.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.
Files changed (42) hide show
  1. package/README.md +62 -5
  2. package/dist/{chunk-94anqtxh.js → chunk-0cq1gx86.js} +5 -5
  3. package/dist/chunk-1kpbvat9.js +89 -0
  4. package/dist/chunk-4pdwsfcx.js +111 -0
  5. package/dist/chunk-a9htb6cr.js +4 -0
  6. package/dist/{chunk-s06hs7a8.js → chunk-m91t44jh.js} +36 -5
  7. package/dist/{chunk-vjsr2jb3.js → chunk-mnad4zkp.js} +3 -3
  8. package/dist/chunk-ntfm5e85.js +27 -0
  9. package/dist/{chunk-9x1apyp4.js → chunk-ssdxknsk.js} +30 -8
  10. package/dist/chunk-y3dx8jh6.js +94 -0
  11. package/dist/chunk-z5pm2emz.js +4 -0
  12. package/dist/client-manager.d.ts +1 -1
  13. package/dist/client.d.ts +1 -1
  14. package/dist/client.js +6 -0
  15. package/dist/datasource.d.ts +2 -1
  16. package/dist/datasource.js +11 -3
  17. package/dist/errors.d.ts +14 -0
  18. package/dist/errors.js +10 -0
  19. package/dist/kubernetes/client.d.ts +6 -1
  20. package/dist/kubernetes/client.js +9 -85
  21. package/dist/kubernetes/environment.d.ts +19 -0
  22. package/dist/kubernetes/environment.js +43 -0
  23. package/dist/kubernetes/pod-log-client.js +2 -2
  24. package/dist/kubernetes/pod-log-datasource.d.ts +3 -3
  25. package/dist/kubernetes/pod-log-datasource.js +8 -6
  26. package/dist/kubernetes/resources.d.ts +23 -0
  27. package/dist/kubernetes/resources.js +8 -0
  28. package/dist/kubernetes/service-port-forward.js +2 -2
  29. package/dist/kubernetes/workload.d.ts +7 -0
  30. package/dist/kubernetes/workload.js +7 -0
  31. package/dist/mysql/client.d.ts +7 -1
  32. package/dist/mysql/client.js +2 -2
  33. package/dist/mysql/index.js +2 -2
  34. package/dist/mysql/pod.d.ts +10 -1
  35. package/dist/mysql/pod.js +4 -2
  36. package/dist/mysql/types.d.ts +4 -0
  37. package/dist/redis/index.js +3 -3
  38. package/dist/s3/datasource.d.ts +3 -3
  39. package/dist/s3/datasource.js +3 -2
  40. package/dist/s3/index.js +3 -2
  41. package/package.json +6 -2
  42. package/dist/chunk-fnc291vk.js +0 -4
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Harness Toolbox
2
2
 
3
- Client, DataSource and ClientManager are owned by the sibling TypeScript package
3
+ Client, ClientProvider, DataSource, EnvironmentContext and ClientManager are owned by the sibling TypeScript package
4
4
  `@compforge/harness-common`. Toolbox provides protocol implementations and transports;
5
5
  its lifecycle entrypoints re-export the same common symbols.
6
6
 
@@ -12,20 +12,77 @@ The package supports Node.js 22+ and Bun. Install with `npm install @compforge/h
12
12
  Protocol adapters have separate entry points; importing lifecycle or transport contracts does
13
13
  not load database or Kubernetes drivers.
14
14
 
15
- A `DataSource` identifies a reusable client and constructs it. The `Client` owns initialization
15
+ ## Workload discovery
16
+
17
+ Borrow a KubernetesClient through KubernetesEnvironment and the shared ClientManager lifecycle, then call
18
+ `client.resolveWorkload(workload, environmentId)`. The ID is a stable target identity from your
19
+ environment registry, not a display name or kubeconfig path. The Workload's namespace overrides the client's default.
20
+ Resource, Service selector and labels locations return all matching Pod incarnations without
21
+ readiness filtering; successful empty discovery returns an empty array. Missing resources,
22
+ selectorless Services and failed access raise KubernetesError.
23
+
24
+ The resource path uses client-node kubeconfig authentication/TLS and bounded native JSON reads,
25
+ not kubectl error text. Configure request timeout, concurrency and response bytes with the
26
+ client's ResourceLimits constructor argument. Existing exec/port-forward operations remain unchanged.
27
+ See [the shared Workload model](../../../docs/workload.md).
28
+
29
+ ```ts
30
+ import { ClientManager, EnvironmentContext } from "@compforge/harness-toolbox";
31
+ import { KubernetesEnvironment } from "@compforge/harness-toolbox/kubernetes/environment";
32
+
33
+ const clients = new ClientManager();
34
+ try {
35
+ const ctx = new EnvironmentContext(
36
+ new KubernetesEnvironment("dev",
37
+ { kubeconfig: "/config/cluster", context: "dev", namespace: "app" },
38
+ { timeoutMs: 10_000, concurrency: 4, maxBytes: 8 * 1024 * 1024 }),
39
+ clients, performance.now() + 30_000,
40
+ );
41
+ const kube = await ctx.clients.get(ctx.environment);
42
+ const pods = await kube.resolveWorkload({
43
+ name: "api", platform: "kubernetes",
44
+ location: { kind: "resource", resource_kind: "Deployment", name: "api" },
45
+ }, "cluster-a/runtime");
46
+ } finally {
47
+ await clients.dispose();
48
+ }
49
+ ```
50
+
51
+ The context does not cancel work automatically. Its budget is available as remainingMs;
52
+ the root owns cancellation and must join child work before disposing clients.
53
+
54
+ Catch ToolboxError from the root entry or `@compforge/harness-toolbox/errors`; use `kind` and
55
+ `code` for decisions. Native `cause` may contain sensitive details and is debug-only.
56
+
57
+ For coordinated common/toolbox development, install and build common before installing toolbox:
58
+
59
+ ```sh
60
+ # From sdks/typescript/toolbox
61
+ (cd ../common && bun install --frozen-lockfile && make build)
62
+ bun install --frozen-lockfile
63
+ make lint
64
+ make test
65
+ ```
66
+
67
+ The toolbox root override resolves common from `../common` only for local development;
68
+ the published dependency remains a registry version. Publish common before toolbox.
69
+ The Makefile builds common before typechecking or building toolbox.
70
+
71
+ A `ClientProvider` identifies a reusable client and constructs it; `DataSource` adds data-access semantics.
72
+ Accessible environments implement ClientProvider directly without becoming data sources. The `Client` owns initialization
16
73
  and idempotent disposal; a `ClientManager` joins concurrent initialization for the same key and
17
74
  closes owned clients when the caller finishes. A `ConnectionSource` resolves connection settings
18
75
  and available `Transport` paths. Clients execute protocol operations through those paths.
19
76
 
20
77
  ```ts
21
- import { ClientManager, dataSourceKey, type DataSource } from "@compforge/harness-toolbox";
78
+ import { ClientManager, clientKey, type DataSource } from "@compforge/harness-toolbox";
22
79
  import { MysqlClient } from "@compforge/harness-toolbox/mysql";
23
80
  import { DirectTransport } from "@compforge/harness-toolbox/transport";
24
81
 
25
82
  const target = { host: "localhost", port: 3306, database: "app", user: "reader", password: "..." };
26
83
  const source: DataSource<MysqlClient> = {
27
- key: dataSourceKey("mysql", target),
28
- createClient: signal => new MysqlClient({
84
+ clientKey: clientKey("mysql", target),
85
+ createClient: (_clients, signal) => new MysqlClient({
29
86
  resolve: async () => target,
30
87
  transports: [new DirectTransport()],
31
88
  }, { signal, connectTimeoutMs: 5_000, queryTimeoutMs: 10_000 }),
@@ -1,6 +1,6 @@
1
1
  import {
2
- dataSourceKey
3
- } from "./chunk-fnc291vk.js";
2
+ clientKey
3
+ } from "./chunk-z5pm2emz.js";
4
4
  import {
5
5
  S3Client
6
6
  } from "./chunk-zp1s833x.js";
@@ -10,7 +10,7 @@ import {
10
10
 
11
11
  // src/s3/datasource.ts
12
12
  class S3DataSource {
13
- key;
13
+ clientKey;
14
14
  #target;
15
15
  #limits;
16
16
  #transport;
@@ -18,13 +18,13 @@ class S3DataSource {
18
18
  this.#target = structuredClone(target);
19
19
  this.#limits = { ...limits };
20
20
  this.#transport = route?.transport ?? new DirectTransport;
21
- this.key = dataSourceKey("s3", {
21
+ this.clientKey = clientKey("s3", {
22
22
  target: this.#target,
23
23
  limits: this.#limits,
24
24
  route: route ? { key: route.key, transport: route.transport.name } : "direct"
25
25
  });
26
26
  }
27
- createClient(signal) {
27
+ createClient(_clients, signal) {
28
28
  return new S3Client({
29
29
  resolve: async () => this.#target,
30
30
  transports: [this.#transport]
@@ -0,0 +1,89 @@
1
+ import {
2
+ ConcurrencyPool
3
+ } from "./chunk-v3hxvnrc.js";
4
+ import {
5
+ KubernetesError,
6
+ ToolboxError,
7
+ kubernetesHttpError
8
+ } from "./chunk-ntfm5e85.js";
9
+
10
+ // src/kubernetes/resources.ts
11
+ import { KubeConfig } from "@kubernetes/client-node";
12
+ import fetch from "node-fetch/lib/index.js";
13
+ function resourceAccess(options, signal, limits) {
14
+ if (![limits.timeoutMs, limits.concurrency, limits.maxBytes].every((value) => Number.isSafeInteger(value) && value > 0)) {
15
+ throw new KubernetesError("Resource limits must be positive integers", { kind: "invalid_argument" });
16
+ }
17
+ const config = new KubeConfig;
18
+ try {
19
+ if (options.kubeconfig)
20
+ config.loadFromFile(options.kubeconfig);
21
+ else
22
+ config.loadFromDefault();
23
+ if (options.context)
24
+ config.setCurrentContext(options.context);
25
+ } catch (cause) {
26
+ throw new KubernetesError("Cannot load Kubernetes access configuration", { kind: "connection_failed", cause });
27
+ }
28
+ const cluster = config.getCurrentCluster();
29
+ if (!cluster)
30
+ throw new KubernetesError("Kubernetes context has no cluster", { kind: "invalid_argument" });
31
+ if (new URL(cluster.server).protocol === "http:") {
32
+ config.clusters = config.clusters.map((item) => item === cluster ? { ...item, skipTLSVerify: true } : item);
33
+ }
34
+ const pool = new ConcurrencyPool(limits.concurrency);
35
+ return {
36
+ get: (namespace, resource, name, selector) => pool.run(async () => {
37
+ const group = ["pods", "services"].includes(resource) ? "/api/v1" : "/apis/apps/v1";
38
+ const path = group + "/namespaces/" + encodeURIComponent(namespace) + "/" + resource + (name ? "/" + encodeURIComponent(name) : "");
39
+ const url = new URL(cluster.server.replace(/\/$/, "") + path);
40
+ if (selector)
41
+ url.searchParams.set("labelSelector", selector);
42
+ const controller = new AbortController;
43
+ const abort = () => controller.abort(signal.reason);
44
+ signal.addEventListener("abort", abort, { once: true });
45
+ let timedOut = false;
46
+ const timer = setTimeout(() => {
47
+ timedOut = true;
48
+ controller.abort();
49
+ }, limits.timeoutMs);
50
+ try {
51
+ signal.throwIfAborted();
52
+ const init = await config.applyToFetchOptions({});
53
+ controller.signal.throwIfAborted();
54
+ const response = await fetch(url.href, { ...init, signal: controller.signal, size: limits.maxBytes });
55
+ if (!response.ok) {
56
+ controller.abort();
57
+ const error = kubernetesHttpError(response.status);
58
+ throw new KubernetesError("Read " + resource + " in namespace " + namespace + " failed", { kind: error.kind, code: error.code });
59
+ }
60
+ return await response.json();
61
+ } catch (cause) {
62
+ if (signal.aborted)
63
+ throw signal.reason;
64
+ if (cause instanceof ToolboxError)
65
+ throw cause;
66
+ const type = cause && typeof cause === "object" && "type" in cause ? cause.type : undefined;
67
+ const code = cause && typeof cause === "object" && "code" in cause && typeof cause.code === "string" ? cause.code : undefined;
68
+ const tls = code !== undefined && [
69
+ "CERT_HAS_EXPIRED",
70
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
71
+ "SELF_SIGNED_CERT_IN_CHAIN",
72
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
73
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
74
+ "ERR_TLS_CERT_ALTNAME_INVALID"
75
+ ].includes(code);
76
+ throw new KubernetesError("Read " + resource + " in namespace " + namespace + " failed", {
77
+ kind: timedOut ? "timeout" : type === "max-size" ? "limit_exceeded" : type === "invalid-json" ? "invalid_response" : tls ? "tls_verification_failed" : code === "ECONNRESET" ? "connection_lost" : "connection_failed",
78
+ cause,
79
+ code
80
+ });
81
+ } finally {
82
+ clearTimeout(timer);
83
+ signal.removeEventListener("abort", abort);
84
+ }
85
+ }, signal)
86
+ };
87
+ }
88
+
89
+ export { resourceAccess };
@@ -0,0 +1,111 @@
1
+ import {
2
+ ServicePortForwarder
3
+ } from "./chunk-205a4vma.js";
4
+ import {
5
+ KubectlExecutor
6
+ } from "./chunk-0rs1aacd.js";
7
+ import {
8
+ resourceAccess
9
+ } from "./chunk-1kpbvat9.js";
10
+ import {
11
+ resolveWorkload
12
+ } from "./chunk-y3dx8jh6.js";
13
+ import {
14
+ KubernetesError
15
+ } from "./chunk-ntfm5e85.js";
16
+
17
+ // src/kubernetes/client.ts
18
+ class KubernetesClient {
19
+ kube;
20
+ resourceLimits;
21
+ #controller = new AbortController;
22
+ signal;
23
+ #executors = new Map;
24
+ #forwarders = new Map;
25
+ #operations = new Set;
26
+ #disposal;
27
+ #resources;
28
+ constructor(kube, signal, executor, resourceLimits = { timeoutMs: 30000, concurrency: 4, maxBytes: 8 * 1024 * 1024 }, resources) {
29
+ this.kube = kube;
30
+ this.resourceLimits = resourceLimits;
31
+ this.signal = signal ? AbortSignal.any([signal, this.#controller.signal]) : this.#controller.signal;
32
+ if (executor)
33
+ this.#executors.set(kube.namespace, executor);
34
+ this.#resources = resources;
35
+ }
36
+ async initialize() {
37
+ this.signal.throwIfAborted();
38
+ }
39
+ resolveWorkload(workload, environment) {
40
+ return this.#track(() => {
41
+ const namespace = workload.namespace ?? this.kube.namespace;
42
+ if (!namespace.trim())
43
+ throw new KubernetesError("Workload namespace is required", { kind: "invalid_argument" });
44
+ if (!environment.trim())
45
+ throw new KubernetesError("Environment identity is required", { kind: "invalid_argument" });
46
+ this.#resources ??= resourceAccess(this.kube, this.signal, this.resourceLimits);
47
+ return resolveWorkload(this.#resources, workload, namespace, environment);
48
+ });
49
+ }
50
+ #executor(namespace) {
51
+ let executor = this.#executors.get(namespace);
52
+ if (!executor) {
53
+ executor = new KubectlExecutor({ ...this.kube, namespace });
54
+ this.#executors.set(namespace, executor);
55
+ }
56
+ return executor;
57
+ }
58
+ #track(work) {
59
+ this.signal.throwIfAborted();
60
+ const pending = Promise.resolve().then(() => {
61
+ this.signal.throwIfAborted();
62
+ return work();
63
+ });
64
+ this.#operations.add(pending);
65
+ pending.then(() => this.#operations.delete(pending), () => this.#operations.delete(pending));
66
+ return pending;
67
+ }
68
+ #options(options) {
69
+ return { ...options, signal: options?.signal ? AbortSignal.any([this.signal, options.signal]) : this.signal };
70
+ }
71
+ run(namespace, command, options) {
72
+ return this.#track(() => this.#executor(namespace).run(command, this.#options(options)));
73
+ }
74
+ exec(namespace, target, command, options) {
75
+ return this.#track(() => this.#executor(namespace).exec(target, command, this.#options(options)));
76
+ }
77
+ forward(namespace, target) {
78
+ return this.#track(async () => {
79
+ let pending = this.#forwarders.get(namespace);
80
+ if (!pending) {
81
+ pending = ServicePortForwarder.create(this.#executor(namespace), { ...this.kube, namespace });
82
+ this.#forwarders.set(namespace, pending);
83
+ pending.catch(() => {
84
+ if (this.#forwarders.get(namespace) === pending)
85
+ this.#forwarders.delete(namespace);
86
+ });
87
+ }
88
+ const forwarder = await pending;
89
+ this.signal.throwIfAborted();
90
+ return forwarder.forward(target);
91
+ });
92
+ }
93
+ dispose() {
94
+ return this.#disposal ??= (async () => {
95
+ this.#controller.abort(new Error("Kubernetes client disposed"));
96
+ await Promise.allSettled(this.#operations);
97
+ const results = await Promise.allSettled([...this.#forwarders.values()].map(async (pending) => {
98
+ const forwarder = await pending.catch(() => {
99
+ return;
100
+ });
101
+ forwarder?.stop();
102
+ }));
103
+ this.#forwarders.clear();
104
+ const errors = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
105
+ if (errors.length)
106
+ throw new AggregateError(errors, "Kubernetes client cleanup failed");
107
+ })();
108
+ }
109
+ }
110
+
111
+ export { KubernetesClient };
@@ -0,0 +1,4 @@
1
+ // src/client.ts
2
+ import { clientKey } from "@compforge/harness-common";
3
+
4
+ export { clientKey };
@@ -50,9 +50,20 @@ try:
50
50
  )
51
51
  try:
52
52
  with connection.cursor(pymysql.cursors.DictCursor) as cursor:
53
- cursor.execute(request["sql"], request["values"])
54
- rows = cursor.fetchall()
55
- print(json.dumps({"rows": rows}, default=encode, ensure_ascii=False))
53
+ statements = request.get("statements")
54
+ if statements is None:
55
+ cursor.execute(request["sql"], request["values"])
56
+ rows = cursor.fetchall()
57
+ print(json.dumps({"rows": rows}, default=encode, ensure_ascii=False))
58
+ else:
59
+ # Batch: one process and one connection for independent statements;
60
+ # the per-statement deadline is re-armed before each execute.
61
+ results = []
62
+ for statement in statements:
63
+ signal.alarm(math.ceil(request["queryTimeoutMs"] / 1000))
64
+ cursor.execute(statement["sql"], statement["values"])
65
+ results.append({"rows": cursor.fetchall()})
66
+ print(json.dumps({"results": results}, default=encode, ensure_ascii=False))
56
67
  finally:
57
68
  connection.close()
58
69
  except Exception as error:
@@ -62,6 +73,9 @@ except Exception as error:
62
73
  finally:
63
74
  signal.alarm(0)
64
75
  `;
76
+ function parsePodResponse(raw) {
77
+ return JSON.parse(raw, (_key, value) => value?.type === "Buffer" && Array.isArray(value.data) ? Buffer.from(value.data) : value);
78
+ }
65
79
  async function queryMysqlViaPod(transport, target, sql, values, options) {
66
80
  const raw = await transport.run(POD_QUERY, {
67
81
  target,
@@ -69,11 +83,28 @@ async function queryMysqlViaPod(transport, target, sql, values, options) {
69
83
  values,
70
84
  ...options
71
85
  }, options.queryTimeoutMs + 3000);
72
- const response = JSON.parse(raw, (_key, value) => value?.type === "Buffer" && Array.isArray(value.data) ? Buffer.from(value.data) : value);
86
+ const response = parsePodResponse(raw);
73
87
  if (response.error || !Array.isArray(response.rows)) {
74
88
  throw new Error(`MySQL via Pod failed: ${response.error ?? "invalid response"}${response.code ? ` (${response.code})` : ""}`);
75
89
  }
76
90
  return response.rows;
77
91
  }
92
+ async function queryMysqlViaPodBatch(transport, target, statements, options) {
93
+ const raw = await transport.run(POD_QUERY, {
94
+ target,
95
+ statements: statements.map(({ sql, values }) => ({ sql: preparePodSql(sql, values), values })),
96
+ ...options
97
+ }, options.connectTimeoutMs + options.queryTimeoutMs * statements.length + 3000);
98
+ const response = parsePodResponse(raw);
99
+ if (response.error || !Array.isArray(response.results)) {
100
+ throw new Error(`MySQL via Pod batch failed: ${response.error ?? "invalid response"}${response.code ? ` (${response.code})` : ""}`);
101
+ }
102
+ return response.results.map((result, index) => {
103
+ if (!Array.isArray(result.rows)) {
104
+ throw new Error(`MySQL via Pod batch statement ${index} returned no rows`);
105
+ }
106
+ return result.rows;
107
+ });
108
+ }
78
109
 
79
- export { preparePodSql, queryMysqlViaPod };
110
+ export { preparePodSql, queryMysqlViaPod, queryMysqlViaPodBatch };
@@ -9,8 +9,8 @@ import {
9
9
  LogCaptureFile
10
10
  } from "./chunk-1vfwvgkj.js";
11
11
  import {
12
- dataSourceKey
13
- } from "./chunk-fnc291vk.js";
12
+ clientKey
13
+ } from "./chunk-a9htb6cr.js";
14
14
 
15
15
  // src/kubernetes/pod-log-client.ts
16
16
  import { copyFileSync, mkdtempSync, rmSync } from "node:fs";
@@ -40,7 +40,7 @@ class PodLogClient {
40
40
  this.signal.throwIfAborted();
41
41
  if (!this.#directory || this.#disposal)
42
42
  throw new Error("Log capture session is not active");
43
- const key = scope.instance && request.sinceTime ? dataSourceKey("pod-log", {
43
+ const key = scope.instance && request.sinceTime ? clientKey("pod-log", {
44
44
  scope,
45
45
  pod: request.pod,
46
46
  container: request.container,
@@ -0,0 +1,27 @@
1
+ // src/errors.ts
2
+ class ToolboxError extends Error {
3
+ kind;
4
+ code;
5
+ constructor(message, options) {
6
+ super(message, { cause: options.cause });
7
+ this.name = new.target.name;
8
+ this.kind = options.kind;
9
+ this.code = options.code;
10
+ }
11
+ }
12
+
13
+ class KubernetesError extends ToolboxError {
14
+ }
15
+ function kubernetesHttpError(status) {
16
+ const kind = {
17
+ 401: "authentication_failed",
18
+ 403: "permission_denied",
19
+ 404: "resource_not_found",
20
+ 408: "timeout",
21
+ 429: "limit_exceeded",
22
+ 504: "timeout"
23
+ }[status] ?? "operation_failed";
24
+ return new KubernetesError("Kubernetes resource request failed", { kind, code: status });
25
+ }
26
+
27
+ export { ToolboxError, KubernetesError, kubernetesHttpError };
@@ -1,10 +1,11 @@
1
- import {
2
- queryMysqlViaPod
3
- } from "./chunk-s06hs7a8.js";
4
1
  import {
5
2
  queryReadonlySession,
6
3
  validateQueryLimits
7
4
  } from "./chunk-kbewz29a.js";
5
+ import {
6
+ queryMysqlViaPod,
7
+ queryMysqlViaPodBatch
8
+ } from "./chunk-m91t44jh.js";
8
9
  import {
9
10
  ConcurrencyPool
10
11
  } from "./chunk-v3hxvnrc.js";
@@ -28,6 +29,22 @@ class MysqlDatabase {
28
29
  query(target, sql, values) {
29
30
  return this.#queries.run(() => this.#query(target, sql, values), this.options.signal);
30
31
  }
32
+ queryBatch(target, statements) {
33
+ return this.#queries.run(async () => {
34
+ if (!statements.length)
35
+ return [];
36
+ return this.#withSession(target, async (session) => {
37
+ if (session.kind === "python")
38
+ return queryMysqlViaPodBatch(session.transport, target, statements, this.options);
39
+ const results = [];
40
+ for (const { sql, values } of statements) {
41
+ const [rows] = await session.connection.execute({ sql, values: [...values], timeout: this.options.queryTimeoutMs });
42
+ results.push(rows);
43
+ }
44
+ return results;
45
+ });
46
+ }, this.options.signal);
47
+ }
31
48
  queryReadonly(target, sql, values, limits) {
32
49
  validateQueryLimits(limits);
33
50
  return this.#queries.run(async () => {
@@ -60,7 +77,7 @@ class MysqlDatabase {
60
77
  }
61
78
  return pending;
62
79
  }
63
- async#query(target, sql, values) {
80
+ async#withSession(target, run) {
64
81
  this.options.signal?.throwIfAborted();
65
82
  const key = [target.host, target.port, target.database, target.user, target.password].join("\x00");
66
83
  const pending = this.#session(target);
@@ -68,10 +85,7 @@ class MysqlDatabase {
68
85
  try {
69
86
  session = await pending;
70
87
  this.options.signal?.throwIfAborted();
71
- if (session.kind === "python")
72
- return await queryMysqlViaPod(session.transport, target, sql, values, this.options);
73
- const [rows] = await session.connection.execute({ sql, values: [...values], timeout: this.options.queryTimeoutMs });
74
- return rows;
88
+ return await run(session);
75
89
  } catch (error) {
76
90
  if (this.#connections.get(key) === pending)
77
91
  this.#connections.delete(key);
@@ -80,6 +94,14 @@ class MysqlDatabase {
80
94
  throw error;
81
95
  }
82
96
  }
97
+ async#query(target, sql, values) {
98
+ return this.#withSession(target, async (session) => {
99
+ if (session.kind === "python")
100
+ return queryMysqlViaPod(session.transport, target, sql, values, this.options);
101
+ const [rows] = await session.connection.execute({ sql, values: [...values], timeout: this.options.queryTimeoutMs });
102
+ return rows;
103
+ });
104
+ }
83
105
  async#open(target) {
84
106
  let reason;
85
107
  for (let i = 0;i < this.transports.length; i++) {
@@ -0,0 +1,94 @@
1
+ import {
2
+ KubernetesError
3
+ } from "./chunk-ntfm5e85.js";
4
+
5
+ // src/kubernetes/workload.ts
6
+ function failure(message, kind) {
7
+ throw new KubernetesError(message, { kind });
8
+ }
9
+ function selector(value, service = false) {
10
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
11
+ return failure("Resource has no Pod selector", "unsupported_operation");
12
+ }
13
+ const input = value;
14
+ const labels = service ? value : input.matchLabels ?? {};
15
+ const terms = Object.entries(labels).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => {
16
+ if (!key || /[,=()!\s]/.test(key) || typeof item !== "string" || /[,=()!\s]/.test(item)) {
17
+ return failure("Invalid label selector", "invalid_argument");
18
+ }
19
+ return key + "=" + item;
20
+ });
21
+ for (const entry of service ? [] : input.matchExpressions ?? []) {
22
+ const values = entry.values ?? [];
23
+ if (!entry.key || /[,=()!\s]/.test(entry.key) || values.some((v) => /[,=()!\s]/.test(v))) {
24
+ return failure("Invalid selector expression", "invalid_response");
25
+ }
26
+ if ((entry.operator === "In" || entry.operator === "NotIn") && values.length) {
27
+ terms.push(entry.key + (entry.operator === "In" ? " in (" : " notin (") + [...values].sort().join(",") + ")");
28
+ } else if (entry.operator === "Exists" && !values.length)
29
+ terms.push(entry.key);
30
+ else if (entry.operator === "DoesNotExist" && !values.length)
31
+ terms.push("!" + entry.key);
32
+ else
33
+ return failure("Unsupported selector expression", "invalid_response");
34
+ }
35
+ if (!terms.length)
36
+ return failure("Resource has no Pod selector", "unsupported_operation");
37
+ return terms.join(",");
38
+ }
39
+ async function resolveWorkload(access, workload, namespace, environment) {
40
+ if (!environment.trim())
41
+ return failure("Environment identity is required", "invalid_argument");
42
+ if (!namespace.trim())
43
+ return failure("Workload namespace is required", "invalid_argument");
44
+ const location = workload.location;
45
+ let selected;
46
+ let pods;
47
+ if (location.kind === "labels") {
48
+ if (!Object.keys(location.labels).length)
49
+ return failure("Pod labels must be nonempty", "invalid_argument");
50
+ selected = selector(location.labels, true);
51
+ const result = await access.get(namespace, "pods", undefined, selected);
52
+ if (!Array.isArray(result.items))
53
+ return failure("Pod list lacks items", "invalid_response");
54
+ pods = result.items;
55
+ } else {
56
+ const resource = location.kind === "service" ? "services" : {
57
+ Pod: "pods",
58
+ Deployment: "deployments",
59
+ StatefulSet: "statefulsets",
60
+ DaemonSet: "daemonsets"
61
+ }[location.resource_kind];
62
+ if (!resource)
63
+ return failure("Unsupported workload resource", "unsupported_operation");
64
+ if (!location.name.trim())
65
+ return failure("Resource name is required", "invalid_argument");
66
+ const item = await access.get(namespace, resource, location.name);
67
+ if (resource === "pods")
68
+ pods = [item];
69
+ else {
70
+ selected = selector(item.spec?.selector, resource === "services");
71
+ const result = await access.get(namespace, "pods", undefined, selected);
72
+ if (!Array.isArray(result.items))
73
+ return failure("Pod list lacks items", "invalid_response");
74
+ pods = result.items;
75
+ }
76
+ }
77
+ return pods.map((pod) => {
78
+ const metadata = pod.metadata;
79
+ if (!metadata?.name || !metadata.uid || metadata.namespace !== namespace) {
80
+ return failure("Pod response lacks identity or has a different namespace", "invalid_response");
81
+ }
82
+ return {
83
+ platform: "kubernetes",
84
+ environment,
85
+ workload: workload.name,
86
+ namespace,
87
+ pod: metadata.name,
88
+ uid: metadata.uid,
89
+ container: workload.container
90
+ };
91
+ }).sort((a, b) => a.pod.localeCompare(b.pod) || a.uid.localeCompare(b.uid));
92
+ }
93
+
94
+ export { resolveWorkload };
@@ -0,0 +1,4 @@
1
+ // src/datasource.ts
2
+ import { clientKey, ClientManager, EnvironmentContext } from "@compforge/harness-common";
3
+
4
+ export { clientKey, ClientManager, EnvironmentContext };
@@ -1 +1 @@
1
- export { ClientManager, type ClientProvider } from "@compforge/harness-common";
1
+ export { ClientManager } from "@compforge/harness-common";
package/dist/client.d.ts CHANGED
@@ -1 +1 @@
1
- export type { Client } from "@compforge/harness-common";
1
+ export { clientKey, type Client, type ClientManager, type ClientProvider } from "@compforge/harness-common";
package/dist/client.js CHANGED
@@ -0,0 +1,6 @@
1
+ import {
2
+ clientKey
3
+ } from "./chunk-a9htb6cr.js";
4
+ export {
5
+ clientKey
6
+ };
@@ -1,5 +1,6 @@
1
- export { dataSourceKey, ClientManager, type Client, type ClientProvider, type DataSource, type ServiceDataSource } from "@compforge/harness-common";
1
+ export { clientKey, ClientManager, EnvironmentContext, type Client, type ClientProvider, type DataSource, type ServiceDataSource } from "@compforge/harness-common";
2
2
  import type { Transport } from "./transport";
3
+ export { ToolboxError, KubernetesError, type ErrorKind } from "./errors";
3
4
  /** Resolution owns configuration semantics; transports preserve the resolved target and identity. */
4
5
  export interface ConnectionSource<Target> {
5
6
  resolve(): Promise<Target>;
@@ -1,8 +1,16 @@
1
1
  import {
2
2
  ClientManager,
3
- dataSourceKey
4
- } from "./chunk-fnc291vk.js";
3
+ EnvironmentContext,
4
+ clientKey
5
+ } from "./chunk-z5pm2emz.js";
6
+ import {
7
+ KubernetesError,
8
+ ToolboxError
9
+ } from "./chunk-ntfm5e85.js";
5
10
  export {
6
- dataSourceKey,
11
+ clientKey,
12
+ ToolboxError,
13
+ KubernetesError,
14
+ EnvironmentContext,
7
15
  ClientManager
8
16
  };
@@ -0,0 +1,14 @@
1
+ /** Stable toolbox failure semantics; native causes are for debugging, not user reports. */
2
+ export type ErrorKind = "connection_failed" | "connection_lost" | "tls_verification_failed" | "authentication_failed" | "permission_denied" | "resource_not_found" | "timeout" | "limit_exceeded" | "invalid_response" | "operation_failed" | "invalid_argument" | "unsupported_operation";
3
+ export declare class ToolboxError extends Error {
4
+ readonly kind: ErrorKind;
5
+ readonly code?: number | string;
6
+ constructor(message: string, options: {
7
+ kind: ErrorKind;
8
+ code?: number | string;
9
+ cause?: unknown;
10
+ });
11
+ }
12
+ export declare class KubernetesError extends ToolboxError {
13
+ }
14
+ export declare function kubernetesHttpError(status: number): KubernetesError;
package/dist/errors.js ADDED
@@ -0,0 +1,10 @@
1
+ import {
2
+ KubernetesError,
3
+ ToolboxError,
4
+ kubernetesHttpError
5
+ } from "./chunk-ntfm5e85.js";
6
+ export {
7
+ kubernetesHttpError,
8
+ ToolboxError,
9
+ KubernetesError
10
+ };
@@ -1,15 +1,20 @@
1
1
  import type { Client } from "../client";
2
2
  import { type Executor, type KubectlOptions, type ExecTarget, type RunOptions } from "./executor";
3
3
  import type { KubernetesEndpoint } from "./endpoint";
4
+ import type { Workload, WorkloadInstance } from "@compforge/harness-common";
5
+ import { type ResourceAccess, type ResourceLimits } from "./resources";
4
6
  /** Owns cluster transports; protocol callers borrow this client without closing it. */
5
7
  export declare class KubernetesClient implements Client {
6
8
  #private;
7
9
  private readonly kube;
10
+ private readonly resourceLimits;
8
11
  readonly signal: AbortSignal;
9
12
  constructor(kube: KubectlOptions & {
10
13
  namespace: string;
11
- }, signal?: AbortSignal, executor?: Executor);
14
+ }, signal?: AbortSignal, executor?: Executor, resourceLimits?: ResourceLimits, resources?: ResourceAccess);
12
15
  initialize(): Promise<void>;
16
+ /** environment is a stable target ID from the caller's registry, not access configuration. */
17
+ resolveWorkload(workload: Workload, environment: string): Promise<WorkloadInstance[]>;
13
18
  run(namespace: string, command: string[], options?: RunOptions): Promise<import("./executor").ExecResult>;
14
19
  exec(namespace: string, target: ExecTarget, command: string[], options?: RunOptions): Promise<import("./executor").ExecResult>;
15
20
  forward(namespace: string, target: KubernetesEndpoint): Promise<import("./service-port-forward").ForwardedEndpoint>;
@@ -1,92 +1,16 @@
1
1
  import {
2
- ServicePortForwarder
3
- } from "../chunk-205a4vma.js";
2
+ KubernetesClient
3
+ } from "../chunk-4pdwsfcx.js";
4
+ import"../chunk-205a4vma.js";
4
5
  import"../chunk-hv2kx47y.js";
5
- import {
6
- KubectlExecutor
7
- } from "../chunk-0rs1aacd.js";
8
- import"../chunk-ztenxgjv.js";
9
6
  import"../chunk-j46x7j7c.js";
10
7
  import"../chunk-6fnhzhdb.js";
11
-
12
- // src/kubernetes/client.ts
13
- class KubernetesClient {
14
- kube;
15
- #controller = new AbortController;
16
- signal;
17
- #executors = new Map;
18
- #forwarders = new Map;
19
- #operations = new Set;
20
- #disposal;
21
- constructor(kube, signal, executor) {
22
- this.kube = kube;
23
- this.signal = signal ? AbortSignal.any([signal, this.#controller.signal]) : this.#controller.signal;
24
- if (executor)
25
- this.#executors.set(kube.namespace, executor);
26
- }
27
- async initialize() {
28
- this.signal.throwIfAborted();
29
- }
30
- #executor(namespace) {
31
- let executor = this.#executors.get(namespace);
32
- if (!executor) {
33
- executor = new KubectlExecutor({ ...this.kube, namespace });
34
- this.#executors.set(namespace, executor);
35
- }
36
- return executor;
37
- }
38
- #track(work) {
39
- this.signal.throwIfAborted();
40
- const pending = Promise.resolve().then(() => {
41
- this.signal.throwIfAborted();
42
- return work();
43
- });
44
- this.#operations.add(pending);
45
- pending.then(() => this.#operations.delete(pending), () => this.#operations.delete(pending));
46
- return pending;
47
- }
48
- #options(options) {
49
- return { ...options, signal: options?.signal ? AbortSignal.any([this.signal, options.signal]) : this.signal };
50
- }
51
- run(namespace, command, options) {
52
- return this.#track(() => this.#executor(namespace).run(command, this.#options(options)));
53
- }
54
- exec(namespace, target, command, options) {
55
- return this.#track(() => this.#executor(namespace).exec(target, command, this.#options(options)));
56
- }
57
- forward(namespace, target) {
58
- return this.#track(async () => {
59
- let pending = this.#forwarders.get(namespace);
60
- if (!pending) {
61
- pending = ServicePortForwarder.create(this.#executor(namespace), { ...this.kube, namespace });
62
- this.#forwarders.set(namespace, pending);
63
- pending.catch(() => {
64
- if (this.#forwarders.get(namespace) === pending)
65
- this.#forwarders.delete(namespace);
66
- });
67
- }
68
- const forwarder = await pending;
69
- this.signal.throwIfAborted();
70
- return forwarder.forward(target);
71
- });
72
- }
73
- dispose() {
74
- return this.#disposal ??= (async () => {
75
- this.#controller.abort(new Error("Kubernetes client disposed"));
76
- await Promise.allSettled(this.#operations);
77
- const results = await Promise.allSettled([...this.#forwarders.values()].map(async (pending) => {
78
- const forwarder = await pending.catch(() => {
79
- return;
80
- });
81
- forwarder?.stop();
82
- }));
83
- this.#forwarders.clear();
84
- const errors = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
85
- if (errors.length)
86
- throw new AggregateError(errors, "Kubernetes client cleanup failed");
87
- })();
88
- }
89
- }
8
+ import"../chunk-0rs1aacd.js";
9
+ import"../chunk-ztenxgjv.js";
10
+ import"../chunk-1kpbvat9.js";
11
+ import"../chunk-y3dx8jh6.js";
12
+ import"../chunk-v3hxvnrc.js";
13
+ import"../chunk-ntfm5e85.js";
90
14
  export {
91
15
  KubernetesClient
92
16
  };
@@ -0,0 +1,19 @@
1
+ import { type ClientProvider, type ClientManager } from "../client";
2
+ import type { Environment } from "@compforge/harness-common";
3
+ import { KubernetesClient } from "./client";
4
+ import type { KubectlOptions } from "./executor";
5
+ import type { ResourceLimits } from "./resources";
6
+ /** Cluster access is a client provider; the execution owns client lifetime. */
7
+ export declare class KubernetesEnvironment implements Environment, ClientProvider<KubernetesClient> {
8
+ #private;
9
+ readonly name: string;
10
+ readonly kind = "kubernetes";
11
+ readonly clientKey: string;
12
+ /** Deployment metadata only; not part of client reuse or access configuration. */
13
+ readonly imageRegistry?: string;
14
+ constructor(name: string, kube: KubectlOptions & {
15
+ namespace: string;
16
+ imageRegistry?: string;
17
+ }, limits: ResourceLimits);
18
+ createClient(_clients: Pick<ClientManager, "get">, signal: AbortSignal): KubernetesClient;
19
+ }
@@ -0,0 +1,43 @@
1
+ import {
2
+ KubernetesClient
3
+ } from "../chunk-4pdwsfcx.js";
4
+ import"../chunk-205a4vma.js";
5
+ import"../chunk-hv2kx47y.js";
6
+ import"../chunk-j46x7j7c.js";
7
+ import"../chunk-6fnhzhdb.js";
8
+ import"../chunk-0rs1aacd.js";
9
+ import"../chunk-ztenxgjv.js";
10
+ import"../chunk-1kpbvat9.js";
11
+ import"../chunk-y3dx8jh6.js";
12
+ import {
13
+ clientKey
14
+ } from "../chunk-a9htb6cr.js";
15
+ import"../chunk-v3hxvnrc.js";
16
+ import"../chunk-ntfm5e85.js";
17
+
18
+ // src/kubernetes/environment.ts
19
+ class KubernetesEnvironment {
20
+ name;
21
+ kind = "kubernetes";
22
+ clientKey;
23
+ imageRegistry;
24
+ #kube;
25
+ #limits;
26
+ constructor(name, kube, limits) {
27
+ this.name = name;
28
+ this.imageRegistry = kube.imageRegistry;
29
+ this.#kube = Object.freeze({
30
+ namespace: kube.namespace,
31
+ kubeconfig: kube.kubeconfig,
32
+ context: kube.context
33
+ });
34
+ this.#limits = Object.freeze({ ...limits });
35
+ this.clientKey = clientKey("kubernetes-client", [this.#kube, this.#limits]);
36
+ }
37
+ createClient(_clients, signal) {
38
+ return new KubernetesClient(this.#kube, signal, undefined, this.#limits);
39
+ }
40
+ }
41
+ export {
42
+ KubernetesEnvironment
43
+ };
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  PodLogClient
3
- } from "../chunk-vjsr2jb3.js";
3
+ } from "../chunk-mnad4zkp.js";
4
4
  import"../chunk-8nm9f0hh.js";
5
5
  import"../chunk-mp88s7r7.js";
6
6
  import"../chunk-1vfwvgkj.js";
7
- import"../chunk-fnc291vk.js";
7
+ import"../chunk-a9htb6cr.js";
8
8
  import"../chunk-v3hxvnrc.js";
9
9
  export {
10
10
  PodLogClient
@@ -1,4 +1,4 @@
1
- import { type DataSource } from "../datasource";
1
+ import { type DataSource, type ClientManager } from "../datasource";
2
2
  import type { ConcurrencyPool } from "../concurrency";
3
3
  import { PodLogClient } from "./pod-log-client";
4
4
  import type { PodLogByteBudget, PodLogCapturePolicy } from "./log-capture-plan";
@@ -7,11 +7,11 @@ export declare class PodLogDataSource implements DataSource<PodLogClient> {
7
7
  private readonly pool;
8
8
  private readonly budget;
9
9
  private readonly policy;
10
- readonly key: string;
10
+ readonly clientKey: string;
11
11
  constructor(target: {
12
12
  kubeconfig?: string;
13
13
  context?: string;
14
14
  namespace: string;
15
15
  }, pool: ConcurrencyPool, budget: PodLogByteBudget, policy: PodLogCapturePolicy);
16
- createClient(signal: AbortSignal): PodLogClient;
16
+ createClient(_clients: Pick<ClientManager, "get">, signal: AbortSignal): PodLogClient;
17
17
  }
@@ -1,27 +1,29 @@
1
1
  import {
2
2
  PodLogClient
3
- } from "../chunk-vjsr2jb3.js";
3
+ } from "../chunk-mnad4zkp.js";
4
4
  import"../chunk-8nm9f0hh.js";
5
5
  import"../chunk-mp88s7r7.js";
6
6
  import"../chunk-1vfwvgkj.js";
7
+ import"../chunk-a9htb6cr.js";
7
8
  import {
8
- dataSourceKey
9
- } from "../chunk-fnc291vk.js";
9
+ clientKey
10
+ } from "../chunk-z5pm2emz.js";
10
11
  import"../chunk-v3hxvnrc.js";
12
+ import"../chunk-ntfm5e85.js";
11
13
 
12
14
  // src/kubernetes/pod-log-datasource.ts
13
15
  class PodLogDataSource {
14
16
  pool;
15
17
  budget;
16
18
  policy;
17
- key;
19
+ clientKey;
18
20
  constructor(target, pool, budget, policy) {
19
21
  this.pool = pool;
20
22
  this.budget = budget;
21
23
  this.policy = policy;
22
- this.key = dataSourceKey("pod-log", target);
24
+ this.clientKey = clientKey("pod-log", target);
23
25
  }
24
- createClient(signal) {
26
+ createClient(_clients, signal) {
25
27
  return new PodLogClient(this.pool, signal, this.policy, this.budget);
26
28
  }
27
29
  }
@@ -0,0 +1,23 @@
1
+ import type { KubectlOptions } from "./executor";
2
+ export interface Resource {
3
+ metadata?: {
4
+ name?: string;
5
+ namespace?: string;
6
+ uid?: string;
7
+ };
8
+ spec?: {
9
+ selector?: unknown;
10
+ };
11
+ items?: Resource[];
12
+ kind?: string;
13
+ }
14
+ export interface ResourceAccess {
15
+ get(namespace: string, resource: string, name?: string, selector?: string): Promise<Resource>;
16
+ }
17
+ export interface ResourceLimits {
18
+ timeoutMs: number;
19
+ concurrency: number;
20
+ maxBytes: number;
21
+ }
22
+ /** Native JSON reads use the same client-node auth/TLS path as native Pod logs. */
23
+ export declare function resourceAccess(options: KubectlOptions, signal: AbortSignal, limits: ResourceLimits): ResourceAccess;
@@ -0,0 +1,8 @@
1
+ import {
2
+ resourceAccess
3
+ } from "../chunk-1kpbvat9.js";
4
+ import"../chunk-v3hxvnrc.js";
5
+ import"../chunk-ntfm5e85.js";
6
+ export {
7
+ resourceAccess
8
+ };
@@ -2,10 +2,10 @@ import {
2
2
  ServicePortForwarder
3
3
  } from "../chunk-205a4vma.js";
4
4
  import"../chunk-hv2kx47y.js";
5
- import"../chunk-0rs1aacd.js";
6
- import"../chunk-ztenxgjv.js";
7
5
  import"../chunk-j46x7j7c.js";
8
6
  import"../chunk-6fnhzhdb.js";
7
+ import"../chunk-0rs1aacd.js";
8
+ import"../chunk-ztenxgjv.js";
9
9
  export {
10
10
  ServicePortForwarder
11
11
  };
@@ -0,0 +1,7 @@
1
+ import type { Workload, WorkloadInstance } from "@compforge/harness-common";
2
+ import type { ResourceAccess } from "./resources";
3
+ /**
4
+ * No readiness filter or replica selection; consumers own collection policy.
5
+ * @rule environment is the caller's stable, non-secret target ID, independent of access routes.
6
+ */
7
+ export declare function resolveWorkload(access: ResourceAccess, workload: Workload, namespace: string, environment: string): Promise<WorkloadInstance[]>;
@@ -0,0 +1,7 @@
1
+ import {
2
+ resolveWorkload
3
+ } from "../chunk-y3dx8jh6.js";
4
+ import"../chunk-ntfm5e85.js";
5
+ export {
6
+ resolveWorkload
7
+ };
@@ -1,6 +1,6 @@
1
1
  import type { Client } from "../client";
2
2
  import { type Connection, type ConnectionOptions } from "mysql2/promise";
3
- import type { Database, DatabaseTarget, DatabaseRow, DatabaseQueryLimits, DatabaseQueryResult } from "./types";
3
+ import type { Database, DatabaseTarget, DatabaseRow, DatabaseQueryLimits, DatabaseQueryResult, SqlStatement } from "./types";
4
4
  import type { ConnectionSource, ClientLifecycle } from "../datasource";
5
5
  import { type Transport } from "../transport";
6
6
  export interface MysqlDatabaseOptions extends ClientLifecycle {
@@ -16,6 +16,12 @@ export declare class MysqlDatabase implements Database {
16
16
  private readonly connect;
17
17
  constructor(transports: readonly Transport[], options: MysqlDatabaseOptions, connect?: ConnectionFactory);
18
18
  query(target: DatabaseTarget, sql: string, values: readonly unknown[]): Promise<DatabaseRow[]>;
19
+ /**
20
+ * Independent statements in submission order, one session: a Pod transport pays the exec
21
+ * and connection cost once instead of per statement. Fail-fast like query(); use query()
22
+ * when a statement depends on an earlier result.
23
+ */
24
+ queryBatch(target: DatabaseTarget, statements: readonly SqlStatement[]): Promise<DatabaseRow[][]>;
19
25
  /** Bounded, single-statement prepared execution on an isolated READ ONLY session (native only). */
20
26
  queryReadonly(target: DatabaseTarget, sql: string, values: readonly unknown[], limits: DatabaseQueryLimits): Promise<DatabaseQueryResult>;
21
27
  initialize(target: DatabaseTarget): Promise<void>;
@@ -2,9 +2,9 @@ import {
2
2
  MysqlClient,
3
3
  MysqlDatabase,
4
4
  openMysql
5
- } from "../chunk-9x1apyp4.js";
6
- import"../chunk-s06hs7a8.js";
5
+ } from "../chunk-ssdxknsk.js";
7
6
  import"../chunk-kbewz29a.js";
7
+ import"../chunk-m91t44jh.js";
8
8
  import"../chunk-v3hxvnrc.js";
9
9
  import"../chunk-xy4g56g4.js";
10
10
  export {
@@ -6,9 +6,9 @@ import {
6
6
  MysqlClient,
7
7
  MysqlDatabase,
8
8
  openMysql
9
- } from "../chunk-9x1apyp4.js";
10
- import"../chunk-s06hs7a8.js";
9
+ } from "../chunk-ssdxknsk.js";
11
10
  import"../chunk-kbewz29a.js";
11
+ import"../chunk-m91t44jh.js";
12
12
  import"../chunk-v3hxvnrc.js";
13
13
  import"../chunk-xy4g56g4.js";
14
14
  export {
@@ -1,7 +1,16 @@
1
- import type { DatabaseRow, DatabaseTarget } from "./types";
1
+ import type { DatabaseRow, DatabaseTarget, SqlStatement } from "./types";
2
2
  import type { PodPythonTransport } from "../transport";
3
3
  export declare function preparePodSql(sql: string, values: readonly unknown[]): string;
4
4
  export declare function queryMysqlViaPod(transport: PodPythonTransport, target: DatabaseTarget, sql: string, values: readonly unknown[], options: {
5
5
  connectTimeoutMs: number;
6
6
  queryTimeoutMs: number;
7
7
  }): Promise<DatabaseRow[]>;
8
+ /**
9
+ * Independent statements over one Pod process and one MySQL connection: the per-exec cost
10
+ * (kubectl handshake, interpreter start, auth) is paid once. Fail-fast: a statement error
11
+ * aborts the batch, later statements never execute, and the whole batch throws.
12
+ */
13
+ export declare function queryMysqlViaPodBatch(transport: PodPythonTransport, target: DatabaseTarget, statements: readonly SqlStatement[], options: {
14
+ connectTimeoutMs: number;
15
+ queryTimeoutMs: number;
16
+ }): Promise<DatabaseRow[][]>;
package/dist/mysql/pod.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  preparePodSql,
3
- queryMysqlViaPod
4
- } from "../chunk-s06hs7a8.js";
3
+ queryMysqlViaPod,
4
+ queryMysqlViaPodBatch
5
+ } from "../chunk-m91t44jh.js";
5
6
  export {
7
+ queryMysqlViaPodBatch,
6
8
  queryMysqlViaPod,
7
9
  preparePodSql
8
10
  };
@@ -8,6 +8,10 @@ export interface DatabaseTarget extends DatabaseIdentity {
8
8
  database: string;
9
9
  }
10
10
  export type DatabaseRow = Record<string, unknown>;
11
+ export interface SqlStatement {
12
+ sql: string;
13
+ values: readonly unknown[];
14
+ }
11
15
  export interface DatabaseQueryLimits {
12
16
  timeoutMs: number;
13
17
  maxRows: number;
@@ -1,3 +1,6 @@
1
+ import {
2
+ discoverRedisTopology
3
+ } from "../chunk-c6z919qb.js";
1
4
  import {
2
5
  RedisAccess,
3
6
  RedisClient,
@@ -9,9 +12,6 @@ import {
9
12
  collectRedisKeyStats,
10
13
  redisGroupedSampleSizes
11
14
  } from "../chunk-jr9y0ghr.js";
12
- import {
13
- discoverRedisTopology
14
- } from "../chunk-c6z919qb.js";
15
15
  export {
16
16
  redisGroupedSampleSizes,
17
17
  parseRedisInfo,
@@ -1,14 +1,14 @@
1
- import { type DataSource } from "../datasource";
1
+ import { type DataSource, type ClientManager } from "../datasource";
2
2
  import { type TcpTransport } from "../transport";
3
3
  import { S3Client } from "./client";
4
4
  import type { S3Limits, S3Target } from "./types";
5
5
  /** Connection details are resolved by the caller; route identity includes the cluster/context, not just a transport name. */
6
6
  export declare class S3DataSource implements DataSource<S3Client> {
7
7
  #private;
8
- readonly key: string;
8
+ readonly clientKey: string;
9
9
  constructor(target: S3Target, limits: S3Limits, route?: {
10
10
  key: string;
11
11
  transport: TcpTransport;
12
12
  });
13
- createClient(signal: AbortSignal): S3Client;
13
+ createClient(_clients: Pick<ClientManager, "get">, signal: AbortSignal): S3Client;
14
14
  }
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  S3DataSource
3
- } from "../chunk-94anqtxh.js";
4
- import"../chunk-fnc291vk.js";
3
+ } from "../chunk-0cq1gx86.js";
4
+ import"../chunk-z5pm2emz.js";
5
5
  import"../chunk-zp1s833x.js";
6
6
  import"../chunk-v3hxvnrc.js";
7
7
  import"../chunk-x4ntb898.js";
8
8
  import"../chunk-xy4g56g4.js";
9
+ import"../chunk-ntfm5e85.js";
9
10
  export {
10
11
  S3DataSource
11
12
  };
package/dist/s3/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  S3DataSource
3
- } from "../chunk-94anqtxh.js";
4
- import"../chunk-fnc291vk.js";
3
+ } from "../chunk-0cq1gx86.js";
4
+ import"../chunk-z5pm2emz.js";
5
5
  import {
6
6
  S3Client
7
7
  } from "../chunk-zp1s833x.js";
8
8
  import"../chunk-v3hxvnrc.js";
9
9
  import"../chunk-x4ntb898.js";
10
10
  import"../chunk-xy4g56g4.js";
11
+ import"../chunk-ntfm5e85.js";
11
12
  export {
12
13
  S3DataSource,
13
14
  S3Client
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compforge/harness-toolbox",
3
- "version": "0.1.2",
3
+ "version": "0.3.2",
4
4
  "description": "Shared infrastructure clients, transports and bounded data capture",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -9,6 +9,10 @@
9
9
  "build": "bun scripts/build.ts && tsc -p tsconfig.build.json"
10
10
  },
11
11
  "exports": {
12
+ "./errors": {
13
+ "types": "./dist/errors.d.ts",
14
+ "import": "./dist/errors.js"
15
+ },
12
16
  ".": {
13
17
  "types": "./dist/datasource.d.ts",
14
18
  "import": "./dist/datasource.js"
@@ -83,7 +87,7 @@
83
87
  "README.md"
84
88
  ],
85
89
  "dependencies": {
86
- "@compforge/harness-common": "0.0.1",
90
+ "@compforge/harness-common": "0.3.0",
87
91
  "@aws-sdk/client-s3": "3.1133.0",
88
92
  "@kubernetes/client-node": "1.4.0",
89
93
  "@opensearch-project/opensearch": "3.6.0",
@@ -1,4 +0,0 @@
1
- // src/datasource.ts
2
- import { dataSourceKey, ClientManager } from "@compforge/harness-common";
3
-
4
- export { dataSourceKey, ClientManager };