@agents-at-scale/ark 0.1.60 → 0.1.61

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.
@@ -74,6 +74,19 @@ const defaultArkServices = {
74
74
  k8sDeploymentName: 'ark-controller',
75
75
  k8sDevDeploymentName: 'ark-controller-devspace',
76
76
  },
77
+ 'ark-apiserver': {
78
+ name: 'ark-apiserver',
79
+ helmReleaseName: 'ark-apiserver',
80
+ description: 'Aggregated API server serving ark.mckinsey.com APIs from PostgreSQL',
81
+ enabled: true,
82
+ mandatory: true,
83
+ category: 'core',
84
+ namespace: 'ark-system',
85
+ chartPath: `${REGISTRY_BASE}/ark-apiserver:${CHART_VERSION}`,
86
+ installArgs: ['--create-namespace'],
87
+ k8sDeploymentName: 'ark-apiserver',
88
+ requiresBackend: 'postgresql',
89
+ },
77
90
  'ark-completions': {
78
91
  name: 'ark-completions',
79
92
  helmReleaseName: 'ark-completions',
@@ -5,6 +5,7 @@ import { StatusChecker } from '../../components/statusChecker.js';
5
5
  import { StatusFormatter, } from '../../ui/statusFormatter.js';
6
6
  import { fetchVersionInfo } from '../../lib/versions.js';
7
7
  import { waitForServicesReady, } from '../../lib/waitForReady.js';
8
+ import { runReadinessChecks, detectStorageBackend, } from '../../lib/readinessChecks.js';
8
9
  import { arkServices } from '../../arkServices.js';
9
10
  import output from '../../lib/output.js';
10
11
  import { parseTimeoutToSeconds } from '../../lib/timeout.js';
@@ -270,13 +271,15 @@ export async function checkStatus(serviceNames, options) {
270
271
  StatusFormatter.printSections(sections);
271
272
  if (options?.waitForReady) {
272
273
  const timeoutSeconds = parseTimeoutToSeconds(options.waitForReady);
274
+ const backend = await detectStorageBackend();
273
275
  let servicesToWait = [];
274
276
  if (serviceNames && serviceNames.length > 0) {
275
277
  servicesToWait = serviceNames
276
278
  .map((name) => Object.values(arkServices).find((s) => s.name === name))
277
279
  .filter((s) => s !== undefined &&
278
280
  s.k8sDeploymentName !== undefined &&
279
- s.namespace !== undefined);
281
+ s.namespace !== undefined &&
282
+ (!s.requiresBackend || s.requiresBackend === backend));
280
283
  if (servicesToWait.length === 0) {
281
284
  output.error(`No valid services found matching: ${serviceNames.join(', ')}`);
282
285
  process.exit(1);
@@ -286,7 +289,8 @@ export async function checkStatus(serviceNames, options) {
286
289
  servicesToWait = Object.values(arkServices).filter((s) => s.enabled &&
287
290
  s.category === 'core' &&
288
291
  s.k8sDeploymentName &&
289
- s.namespace);
292
+ s.namespace &&
293
+ (!s.requiresBackend || s.requiresBackend === backend));
290
294
  }
291
295
  console.log();
292
296
  const waitSpinner = ora(`Waiting for services to be ready (timeout: ${timeoutSeconds}s)...`).start();
@@ -305,14 +309,23 @@ export async function checkStatus(serviceNames, options) {
305
309
  });
306
310
  waitSpinner.text = `Waiting for services to be ready (${elapsed}/${timeoutSeconds}s)...\n${lines.join('\n')}`;
307
311
  });
308
- if (result) {
309
- waitSpinner.succeed('All services are ready');
310
- process.exit(0);
311
- }
312
- else {
312
+ if (!result) {
313
313
  waitSpinner.fail(`Services did not become ready within ${timeoutSeconds} seconds`);
314
314
  process.exit(1);
315
315
  }
316
+ waitSpinner.succeed('All services are ready');
317
+ const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
318
+ const remainingSeconds = Math.max(1, timeoutSeconds - elapsedSeconds);
319
+ const deepResults = await runReadinessChecks(remainingSeconds, (r) => {
320
+ const icon = r.passed ? chalk.green('✓') : chalk.red('✗');
321
+ const dur = `${(r.durationMs / 1000).toFixed(1)}s`;
322
+ const suffix = r.message ? ` — ${r.message}` : '';
323
+ console.log(` ${icon} ${r.name} (${dur})${suffix}`);
324
+ });
325
+ if (deepResults.some((r) => !r.passed)) {
326
+ process.exit(1);
327
+ }
328
+ process.exit(0);
316
329
  }
317
330
  process.exit(0);
318
331
  }
@@ -0,0 +1,10 @@
1
+ export type StorageBackend = 'etcd' | 'postgresql';
2
+ export interface ReadinessCheckResult {
3
+ name: string;
4
+ passed: boolean;
5
+ durationMs: number;
6
+ message?: string;
7
+ }
8
+ export type ReadinessProgress = (result: ReadinessCheckResult) => void;
9
+ export declare function detectStorageBackend(): Promise<StorageBackend>;
10
+ export declare function runReadinessChecks(timeoutSeconds: number, onProgress?: ReadinessProgress): Promise<ReadinessCheckResult[]>;
@@ -0,0 +1,88 @@
1
+ import { execa } from 'execa';
2
+ const API_GROUP_POLL_INTERVAL_MS = 10000;
3
+ function sleep(ms) {
4
+ return new Promise((resolve) => setTimeout(resolve, ms));
5
+ }
6
+ async function runKubectl(args, timeoutMs) {
7
+ const result = await execa('kubectl', args, {
8
+ timeout: timeoutMs,
9
+ reject: false,
10
+ });
11
+ return {
12
+ exitCode: result.exitCode ?? 1,
13
+ stdout: result.stdout ?? '',
14
+ stderr: result.stderr ?? '',
15
+ };
16
+ }
17
+ export async function detectStorageBackend() {
18
+ const { exitCode } = await runKubectl(['get', 'crd', 'agents.ark.mckinsey.com'], 10000);
19
+ return exitCode === 0 ? 'etcd' : 'postgresql';
20
+ }
21
+ async function waitForApiServices(timeoutSeconds) {
22
+ const start = Date.now();
23
+ const primary = await runKubectl([
24
+ 'wait',
25
+ '--for=condition=Available',
26
+ 'apiservice',
27
+ 'v1alpha1.ark.mckinsey.com',
28
+ `--timeout=${timeoutSeconds}s`,
29
+ ], timeoutSeconds * 1000 + 5000);
30
+ await runKubectl([
31
+ 'wait',
32
+ '--for=condition=Available',
33
+ 'apiservice',
34
+ 'v1prealpha1.ark.mckinsey.com',
35
+ '--timeout=30s',
36
+ ], 35000);
37
+ return {
38
+ name: 'APIServices available',
39
+ passed: primary.exitCode === 0,
40
+ durationMs: Date.now() - start,
41
+ message: primary.exitCode === 0
42
+ ? undefined
43
+ : (primary.stderr || primary.stdout).trim(),
44
+ };
45
+ }
46
+ async function waitForApiGroup(timeoutSeconds) {
47
+ const start = Date.now();
48
+ const deadline = start + timeoutSeconds * 1000;
49
+ while (Date.now() < deadline) {
50
+ const { stdout, exitCode } = await runKubectl(['api-resources', '--api-group=ark.mckinsey.com', '-o', 'name'], 10000);
51
+ if (exitCode === 0 && /agents\./.test(stdout)) {
52
+ return {
53
+ name: 'API group registered',
54
+ passed: true,
55
+ durationMs: Date.now() - start,
56
+ };
57
+ }
58
+ await sleep(API_GROUP_POLL_INTERVAL_MS);
59
+ }
60
+ return {
61
+ name: 'API group registered',
62
+ passed: false,
63
+ durationMs: Date.now() - start,
64
+ message: 'timed out waiting for ark.mckinsey.com API group',
65
+ };
66
+ }
67
+ export async function runReadinessChecks(timeoutSeconds, onProgress) {
68
+ const backend = await detectStorageBackend();
69
+ if (backend === 'etcd') {
70
+ return [];
71
+ }
72
+ const overallStart = Date.now();
73
+ const remaining = () => Math.max(1, timeoutSeconds - Math.floor((Date.now() - overallStart) / 1000));
74
+ const checks = [
75
+ () => waitForApiServices(Math.min(remaining(), 120)),
76
+ () => waitForApiGroup(Math.min(remaining(), 300)),
77
+ ];
78
+ const results = [];
79
+ for (const check of checks) {
80
+ const result = await check();
81
+ results.push(result);
82
+ onProgress?.(result);
83
+ if (!result.passed) {
84
+ break;
85
+ }
86
+ }
87
+ return results;
88
+ }
@@ -18,6 +18,7 @@ export interface ArkService {
18
18
  k8sPortForwardLocalPort?: number;
19
19
  k8sDeploymentName?: string;
20
20
  k8sDevDeploymentName?: string;
21
+ requiresBackend?: 'etcd' | 'postgresql';
21
22
  }
22
23
  export interface ServiceCollection {
23
24
  [key: string]: ArkService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-at-scale/ark",
3
- "version": "0.1.60",
3
+ "version": "0.1.61",
4
4
  "description": "Ark CLI - Interactive terminal interface for ARK agents",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "@kubernetes/client-node": "^1.3.0",
46
46
  "@modelcontextprotocol/sdk": "^1.27.1",
47
- "axios": "^1.15.0",
47
+ "axios": "^1.16.0",
48
48
  "chalk": "^4.1.2",
49
49
  "commander": "^12.1.0",
50
50
  "debug": "^4.4.1",
@@ -91,6 +91,7 @@
91
91
  "@hono/node-server": "^1.19.13",
92
92
  "flatted": "^3.4.2",
93
93
  "tar": "^7.5.11",
94
- "express-rate-limit": "^8.3.0"
94
+ "express-rate-limit": "^8.3.0",
95
+ "fast-uri": "^3.1.1"
95
96
  }
96
97
  }
@@ -763,11 +763,11 @@ wheels = [
763
763
 
764
764
  [[package]]
765
765
  name = "python-dotenv"
766
- version = "1.1.1"
766
+ version = "1.2.2"
767
767
  source = { registry = "https://pypi.org/simple" }
768
- sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
768
+ sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
769
769
  wheels = [
770
- { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
770
+ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
771
771
  ]
772
772
 
773
773
  [[package]]
@@ -1007,15 +1007,15 @@ wheels = [
1007
1007
 
1008
1008
  [[package]]
1009
1009
  name = "starlette"
1010
- version = "0.47.1"
1010
+ version = "0.49.1"
1011
1011
  source = { registry = "https://pypi.org/simple" }
1012
1012
  dependencies = [
1013
1013
  { name = "anyio" },
1014
1014
  { name = "typing-extensions", marker = "python_full_version < '3.13'" },
1015
1015
  ]
1016
- sdist = { url = "https://files.pythonhosted.org/packages/0a/69/662169fdb92fb96ec3eaee218cf540a629d629c86d7993d9651226a6789b/starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b", size = 2583072, upload-time = "2025-06-21T04:03:17.337Z" }
1016
+ sdist = { url = "https://files.pythonhosted.org/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb", size = 2654703, upload-time = "2025-10-28T17:34:10.928Z" }
1017
1017
  wheels = [
1018
- { url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" },
1018
+ { url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" },
1019
1019
  ]
1020
1020
 
1021
1021
  [[package]]