@agents-at-scale/ark 0.1.36 → 0.1.38

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 (129) hide show
  1. package/README.md +53 -70
  2. package/dist/arkServices.d.ts +3 -27
  3. package/dist/arkServices.js +31 -3
  4. package/dist/arkServices.spec.js +118 -10
  5. package/dist/commands/chat/index.js +1 -2
  6. package/dist/commands/completion/index.js +0 -2
  7. package/dist/commands/generate/generators/project.js +33 -26
  8. package/dist/commands/generate/index.js +2 -2
  9. package/dist/commands/generate/templateDiscovery.js +13 -4
  10. package/dist/commands/install/index.js +49 -58
  11. package/dist/commands/models/create.d.ts +9 -1
  12. package/dist/commands/models/create.js +97 -90
  13. package/dist/commands/models/create.spec.js +9 -37
  14. package/dist/commands/models/index.js +8 -2
  15. package/dist/commands/models/index.spec.js +1 -1
  16. package/dist/commands/status/index.d.ts +3 -1
  17. package/dist/commands/status/index.js +54 -2
  18. package/dist/components/AsyncOperation.d.ts +54 -0
  19. package/dist/components/AsyncOperation.js +110 -0
  20. package/dist/components/ChatUI.js +39 -72
  21. package/dist/components/SelectMenu.d.ts +17 -0
  22. package/dist/components/SelectMenu.js +21 -0
  23. package/dist/components/StatusMessage.d.ts +20 -0
  24. package/dist/components/StatusMessage.js +13 -0
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/lib/arkApiClient.d.ts +1 -2
  28. package/dist/lib/arkApiClient.js +5 -6
  29. package/dist/lib/config.d.ts +4 -0
  30. package/dist/lib/config.js +9 -0
  31. package/dist/lib/nextSteps.js +1 -1
  32. package/dist/lib/nextSteps.spec.js +1 -1
  33. package/dist/lib/security.js +4 -0
  34. package/dist/lib/startup.js +6 -2
  35. package/dist/lib/startup.spec.js +1 -1
  36. package/dist/lib/timeout.d.ts +1 -0
  37. package/dist/lib/timeout.js +20 -0
  38. package/dist/lib/timeout.spec.d.ts +1 -0
  39. package/dist/lib/timeout.spec.js +14 -0
  40. package/dist/lib/waitForReady.d.ts +8 -0
  41. package/dist/lib/waitForReady.js +32 -0
  42. package/dist/lib/waitForReady.spec.d.ts +1 -0
  43. package/dist/lib/waitForReady.spec.js +104 -0
  44. package/dist/types/arkService.d.ts +27 -0
  45. package/dist/types/arkService.js +1 -0
  46. package/dist/ui/asyncOperations/connectingToArk.d.ts +15 -0
  47. package/dist/ui/asyncOperations/connectingToArk.js +63 -0
  48. package/package.json +7 -5
  49. package/templates/agent/agent.template.yaml +27 -0
  50. package/templates/marketplace/.editorconfig +24 -0
  51. package/templates/marketplace/.github/.keep +11 -0
  52. package/templates/marketplace/.github/workflows/.keep +16 -0
  53. package/templates/marketplace/.helmignore +23 -0
  54. package/templates/marketplace/.prettierrc.json +20 -0
  55. package/templates/marketplace/.yamllint.yml +53 -0
  56. package/templates/marketplace/README.md +197 -0
  57. package/templates/marketplace/agents/.keep +29 -0
  58. package/templates/marketplace/docs/.keep +19 -0
  59. package/templates/marketplace/mcp-servers/.keep +32 -0
  60. package/templates/marketplace/models/.keep +23 -0
  61. package/templates/marketplace/projects/.keep +43 -0
  62. package/templates/marketplace/queries/.keep +25 -0
  63. package/templates/marketplace/teams/.keep +29 -0
  64. package/templates/marketplace/tools/.keep +32 -0
  65. package/templates/marketplace/tools/examples/.keep +17 -0
  66. package/templates/mcp-server/Dockerfile +133 -0
  67. package/templates/mcp-server/Makefile +186 -0
  68. package/templates/mcp-server/README.md +178 -0
  69. package/templates/mcp-server/build.sh +76 -0
  70. package/templates/mcp-server/chart/Chart.yaml +22 -0
  71. package/templates/mcp-server/chart/templates/_helpers.tpl +62 -0
  72. package/templates/mcp-server/chart/templates/deployment.yaml +80 -0
  73. package/templates/mcp-server/chart/templates/hpa.yaml +32 -0
  74. package/templates/mcp-server/chart/templates/mcpserver.yaml +21 -0
  75. package/templates/mcp-server/chart/templates/secret.yaml +11 -0
  76. package/templates/mcp-server/chart/templates/service.yaml +15 -0
  77. package/templates/mcp-server/chart/templates/serviceaccount.yaml +13 -0
  78. package/templates/mcp-server/chart/values.yaml +84 -0
  79. package/templates/mcp-server/example-values.yaml +74 -0
  80. package/templates/mcp-server/examples/{{ .Values.mcpServerName }}-agent.yaml +33 -0
  81. package/templates/mcp-server/examples/{{ .Values.mcpServerName }}-query.yaml +24 -0
  82. package/templates/models/azure.yaml +33 -0
  83. package/templates/models/claude.yaml +28 -0
  84. package/templates/models/gemini.yaml +28 -0
  85. package/templates/models/openai.yaml +39 -0
  86. package/templates/project/.editorconfig +24 -0
  87. package/templates/project/.helmignore +24 -0
  88. package/templates/project/.prettierrc.json +16 -0
  89. package/templates/project/.yamllint.yml +50 -0
  90. package/templates/project/Chart.yaml +19 -0
  91. package/templates/project/Makefile +360 -0
  92. package/templates/project/README.md +377 -0
  93. package/templates/project/agents/.keep +11 -0
  94. package/templates/project/docs/.keep +14 -0
  95. package/templates/project/mcp-servers/.keep +34 -0
  96. package/templates/project/models/.keep +17 -0
  97. package/templates/project/queries/.keep +11 -0
  98. package/templates/project/scripts/setup.sh +108 -0
  99. package/templates/project/teams/.keep +11 -0
  100. package/templates/project/templates/00-rbac.yaml +168 -0
  101. package/templates/project/templates/01-models.yaml +11 -0
  102. package/templates/project/templates/02-mcp-servers.yaml +22 -0
  103. package/templates/project/templates/03-tools.yaml +12 -0
  104. package/templates/project/templates/04-agents.yaml +12 -0
  105. package/templates/project/templates/05-teams.yaml +11 -0
  106. package/templates/project/templates/06-queries.yaml +11 -0
  107. package/templates/project/templates/_helpers.tpl +91 -0
  108. package/templates/project/tests/e2e/.keep +10 -0
  109. package/templates/project/tests/unit/.keep +10 -0
  110. package/templates/project/tools/.keep +25 -0
  111. package/templates/project/tools/example-tool.yaml.disabled +94 -0
  112. package/templates/project/tools/examples/data-tool/Dockerfile +32 -0
  113. package/templates/project/values.yaml +141 -0
  114. package/templates/query/query.template.yaml +13 -0
  115. package/templates/team/team.template.yaml +17 -0
  116. package/templates/tool/.python-version +1 -0
  117. package/templates/tool/Dockerfile +23 -0
  118. package/templates/tool/README.md +238 -0
  119. package/templates/tool/agent.yaml +19 -0
  120. package/templates/tool/deploy.sh +10 -0
  121. package/templates/tool/deployment/deployment.yaml +31 -0
  122. package/templates/tool/deployment/kustomization.yaml +7 -0
  123. package/templates/tool/deployment/mcpserver.yaml +12 -0
  124. package/templates/tool/deployment/service.yaml +12 -0
  125. package/templates/tool/deployment/serviceaccount.yaml +8 -0
  126. package/templates/tool/deployment/values.yaml +3 -0
  127. package/templates/tool/pyproject.toml +9 -0
  128. package/templates/tool/src/main.py +36 -0
  129. package/templates/tool/uv.lock +498 -0
@@ -4,10 +4,11 @@ import { execute } from '../../lib/commands.js';
4
4
  import inquirer from 'inquirer';
5
5
  import { showNoClusterError } from '../../lib/startup.js';
6
6
  import output from '../../lib/output.js';
7
- import { getInstallableServices, arkDependencies, } from '../../arkServices.js';
8
- import { isArkReady } from '../../lib/arkStatus.js';
7
+ import { getInstallableServices, arkDependencies, arkServices, } from '../../arkServices.js';
9
8
  import { printNextSteps } from '../../lib/nextSteps.js';
10
9
  import ora from 'ora';
10
+ import { waitForServicesReady, } from '../../lib/waitForReady.js';
11
+ import { parseTimeoutToSeconds } from '../../lib/timeout.js';
11
12
  async function installService(service, verbose = false) {
12
13
  const helmArgs = [
13
14
  'upgrade',
@@ -67,6 +68,12 @@ export async function installArk(config, serviceName, options = {}) {
67
68
  console.log(chalk.cyan.bold('\nSelect components to install:'));
68
69
  console.log(chalk.gray('Use arrow keys to navigate, space to toggle, enter to confirm\n'));
69
70
  // Build choices for the checkbox prompt
71
+ const coreServices = Object.values(arkServices)
72
+ .filter((s) => s.category === 'core')
73
+ .sort((a, b) => a.name.localeCompare(b.name));
74
+ const otherServices = Object.values(arkServices)
75
+ .filter((s) => s.category === 'service')
76
+ .sort((a, b) => a.name.localeCompare(b.name));
70
77
  const allChoices = [
71
78
  new inquirer.Separator(chalk.bold('──── Dependencies ────')),
72
79
  {
@@ -80,32 +87,17 @@ export async function installArk(config, serviceName, options = {}) {
80
87
  checked: true,
81
88
  },
82
89
  new inquirer.Separator(chalk.bold('──── Ark Core ────')),
83
- {
84
- name: `ark-controller ${chalk.gray('- Core Ark controller')}`,
85
- value: 'ark-controller',
86
- checked: true,
87
- },
90
+ ...coreServices.map((service) => ({
91
+ name: `${service.name} ${chalk.gray(`- ${service.description}`)}`,
92
+ value: service.helmReleaseName,
93
+ checked: Boolean(service.enabled),
94
+ })),
88
95
  new inquirer.Separator(chalk.bold('──── Ark Services ────')),
89
- {
90
- name: `ark-api ${chalk.gray('- API service')}`,
91
- value: 'ark-api',
92
- checked: true,
93
- },
94
- {
95
- name: `ark-dashboard ${chalk.gray('- Web dashboard')}`,
96
- value: 'ark-dashboard',
97
- checked: true,
98
- },
99
- {
100
- name: `ark-mcp ${chalk.gray('- MCP services')}`,
101
- value: 'ark-mcp',
102
- checked: true,
103
- },
104
- {
105
- name: `localhost-gateway ${chalk.gray('- Gateway for local access')}`,
106
- value: 'localhost-gateway',
107
- checked: true,
108
- },
96
+ ...otherServices.map((service) => ({
97
+ name: `${service.name} ${chalk.gray(`- ${service.description}`)}`,
98
+ value: service.helmReleaseName,
99
+ checked: Boolean(service.enabled),
100
+ })),
109
101
  ];
110
102
  let selectedComponents = [];
111
103
  try {
@@ -190,11 +182,9 @@ export async function installArk(config, serviceName, options = {}) {
190
182
  }
191
183
  }
192
184
  // Install selected services
193
- const services = getInstallableServices();
194
- for (const service of Object.values(services)) {
195
- // Check if this service was selected
196
- const serviceKey = service.helmReleaseName;
197
- if (!selectedComponents.includes(serviceKey)) {
185
+ for (const serviceName of selectedComponents) {
186
+ const service = Object.values(arkServices).find((s) => s.helmReleaseName === serviceName);
187
+ if (!service || !service.chartPath) {
198
188
  continue;
199
189
  }
200
190
  output.info(`installing ${service.name}...`);
@@ -203,8 +193,8 @@ export async function installArk(config, serviceName, options = {}) {
203
193
  console.log(); // Add blank line after command output
204
194
  }
205
195
  catch {
206
- // Continue with remaining services on error
207
196
  console.log(); // Add blank line after error output
197
+ process.exit(1);
208
198
  }
209
199
  }
210
200
  }
@@ -234,8 +224,8 @@ export async function installArk(config, serviceName, options = {}) {
234
224
  console.log(); // Add blank line after command output
235
225
  }
236
226
  catch {
237
- // Continue with remaining services on error
238
227
  console.log(); // Add blank line after error output
228
+ process.exit(1);
239
229
  }
240
230
  }
241
231
  }
@@ -245,34 +235,35 @@ export async function installArk(config, serviceName, options = {}) {
245
235
  }
246
236
  // Wait for Ark to be ready if requested
247
237
  if (options.waitForReady) {
248
- // Parse timeout value (e.g., '30s', '2m', '60')
249
- const parseTimeout = (value) => {
250
- const match = value.match(/^(\d+)([sm])?$/);
251
- if (!match) {
252
- throw new Error('Invalid timeout format. Use format like 30s or 2m');
253
- }
254
- const num = parseInt(match[1], 10);
255
- const unit = match[2] || 's';
256
- return unit === 'm' ? num * 60 : num;
257
- };
258
238
  try {
259
- const timeoutSeconds = parseTimeout(options.waitForReady);
260
- const startTime = Date.now();
261
- const endTime = startTime + timeoutSeconds * 1000;
239
+ const timeoutSeconds = parseTimeoutToSeconds(options.waitForReady);
240
+ const servicesToWait = Object.values(arkServices).filter((s) => s.enabled &&
241
+ s.category === 'core' &&
242
+ s.k8sDeploymentName &&
243
+ s.namespace);
262
244
  const spinner = ora(`Waiting for Ark to be ready (timeout: ${timeoutSeconds}s)...`).start();
263
- while (Date.now() < endTime) {
264
- if (await isArkReady()) {
265
- spinner.succeed('Ark is ready!');
266
- return;
267
- }
245
+ const statusMap = new Map();
246
+ servicesToWait.forEach((s) => statusMap.set(s.name, false));
247
+ const startTime = Date.now();
248
+ const result = await waitForServicesReady(servicesToWait, timeoutSeconds, (progress) => {
249
+ statusMap.set(progress.serviceName, progress.ready);
268
250
  const elapsed = Math.floor((Date.now() - startTime) / 1000);
269
- spinner.text = `Waiting for Ark to be ready (${elapsed}/${timeoutSeconds}s)...`;
270
- // Wait 2 seconds before checking again
271
- await new Promise((resolve) => setTimeout(resolve, 2000));
251
+ const lines = servicesToWait.map((s) => {
252
+ const ready = statusMap.get(s.name);
253
+ const icon = ready ? '✓' : '⋯';
254
+ const status = ready ? 'ready' : 'waiting...';
255
+ const color = ready ? chalk.green : chalk.yellow;
256
+ return ` ${color(icon)} ${chalk.bold(s.name)} ${chalk.blue(`(${s.namespace})`)} - ${status}`;
257
+ });
258
+ spinner.text = `Waiting for Ark to be ready (${elapsed}/${timeoutSeconds}s)...\n${lines.join('\n')}`;
259
+ });
260
+ if (result) {
261
+ spinner.succeed('Ark is ready');
262
+ }
263
+ else {
264
+ spinner.fail(`Ark did not become ready within ${timeoutSeconds} seconds`);
265
+ process.exit(1);
272
266
  }
273
- // Timeout reached
274
- spinner.fail(`Ark did not become ready within ${timeoutSeconds} seconds`);
275
- process.exit(1);
276
267
  }
277
268
  catch (error) {
278
269
  output.error(`Failed to wait for ready: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -1 +1,9 @@
1
- export declare function createModel(modelName?: string): Promise<boolean>;
1
+ export interface CreateModelOptions {
2
+ type?: string;
3
+ model?: string;
4
+ baseUrl?: string;
5
+ apiKey?: string;
6
+ apiVersion?: string;
7
+ yes?: boolean;
8
+ }
9
+ export declare function createModel(modelName?: string, options?: CreateModelOptions): Promise<boolean>;
@@ -1,7 +1,7 @@
1
1
  import { execa } from 'execa';
2
2
  import inquirer from 'inquirer';
3
3
  import output from '../../lib/output.js';
4
- export async function createModel(modelName) {
4
+ export async function createModel(modelName, options = {}) {
5
5
  // Step 1: Get model name if not provided
6
6
  if (!modelName) {
7
7
  const nameAnswer = await inquirer.prompt([
@@ -27,66 +27,87 @@ export async function createModel(modelName) {
27
27
  try {
28
28
  await execa('kubectl', ['get', 'model', modelName], { stdio: 'pipe' });
29
29
  output.warning(`model ${modelName} already exists`);
30
- const { overwrite } = await inquirer.prompt([
31
- {
32
- type: 'confirm',
33
- name: 'overwrite',
34
- message: `overwrite existing model ${modelName}?`,
35
- default: false,
36
- },
37
- ]);
38
- if (!overwrite) {
39
- output.info('model creation cancelled');
40
- return false;
30
+ if (!options.yes) {
31
+ const { overwrite } = await inquirer.prompt([
32
+ {
33
+ type: 'confirm',
34
+ name: 'overwrite',
35
+ message: `overwrite existing model ${modelName}?`,
36
+ default: false,
37
+ },
38
+ ]);
39
+ if (!overwrite) {
40
+ output.info('model creation cancelled');
41
+ return false;
42
+ }
41
43
  }
42
44
  }
43
45
  catch {
44
46
  // Model doesn't exist, continue
45
47
  }
46
- // Step 2: Choose model type
47
- const { modelType } = await inquirer.prompt([
48
- {
49
- type: 'list',
50
- name: 'modelType',
51
- message: 'select model provider:',
52
- choices: [
53
- { name: 'Azure OpenAI', value: 'azure' },
54
- { name: 'OpenAI', value: 'openai' },
55
- ],
56
- default: 'azure',
57
- },
58
- ]);
59
- // Step 3: Get common parameters
60
- const commonAnswers = await inquirer.prompt([
61
- {
62
- type: 'input',
63
- name: 'modelVersion',
64
- message: 'model version:',
65
- default: 'gpt-4o-mini',
66
- },
67
- {
68
- type: 'input',
69
- name: 'baseUrl',
70
- message: 'base URL:',
71
- validate: (input) => {
72
- if (!input)
73
- return 'base URL is required';
74
- try {
75
- new URL(input);
76
- return true;
77
- }
78
- catch {
79
- return 'please enter a valid URL';
80
- }
48
+ // Step 2: Get model type
49
+ let modelType = options.type;
50
+ if (!modelType) {
51
+ const answer = await inquirer.prompt([
52
+ {
53
+ type: 'list',
54
+ name: 'modelType',
55
+ message: 'select model provider:',
56
+ choices: [
57
+ { name: 'Azure OpenAI', value: 'azure' },
58
+ { name: 'OpenAI', value: 'openai' },
59
+ ],
60
+ default: 'azure',
81
61
  },
82
- },
83
- ]);
84
- // Remove trailing slash from base URL
85
- const baseUrl = commonAnswers.baseUrl.replace(/\/$/, '');
86
- // Step 4: Get provider-specific parameters
87
- let apiVersion = '';
88
- if (modelType === 'azure') {
89
- const azureAnswers = await inquirer.prompt([
62
+ ]);
63
+ modelType = answer.modelType;
64
+ }
65
+ // Step 3: Get model name
66
+ let model = options.model;
67
+ if (!model) {
68
+ const answer = await inquirer.prompt([
69
+ {
70
+ type: 'input',
71
+ name: 'model',
72
+ message: 'model:',
73
+ default: 'gpt-4o-mini',
74
+ },
75
+ ]);
76
+ model = answer.model;
77
+ }
78
+ // Step 4: Get base URL
79
+ let baseUrl = options.baseUrl;
80
+ if (!baseUrl) {
81
+ const answer = await inquirer.prompt([
82
+ {
83
+ type: 'input',
84
+ name: 'baseUrl',
85
+ message: 'base URL:',
86
+ validate: (input) => {
87
+ if (!input)
88
+ return 'base URL is required';
89
+ try {
90
+ new URL(input);
91
+ return true;
92
+ }
93
+ catch {
94
+ return 'please enter a valid URL';
95
+ }
96
+ },
97
+ },
98
+ ]);
99
+ baseUrl = answer.baseUrl;
100
+ }
101
+ // Validate and clean base URL
102
+ if (!baseUrl) {
103
+ output.error('base URL is required');
104
+ return false;
105
+ }
106
+ baseUrl = baseUrl.replace(/\/$/, '');
107
+ // Step 5: Get API version (Azure only)
108
+ let apiVersion = options.apiVersion || '';
109
+ if (modelType === 'azure' && !options.apiVersion) {
110
+ const answer = await inquirer.prompt([
90
111
  {
91
112
  type: 'input',
92
113
  name: 'apiVersion',
@@ -94,33 +115,29 @@ export async function createModel(modelName) {
94
115
  default: '2024-12-01-preview',
95
116
  },
96
117
  ]);
97
- apiVersion = azureAnswers.apiVersion;
118
+ apiVersion = answer.apiVersion;
98
119
  }
99
- // Step 5: Get API key (password input)
100
- const { apiKey } = await inquirer.prompt([
101
- {
102
- type: 'password',
103
- name: 'apiKey',
104
- message: 'API key:',
105
- mask: '*',
106
- validate: (input) => {
107
- if (!input)
108
- return 'API key is required';
109
- return true;
120
+ // Step 6: Get API key
121
+ let apiKey = options.apiKey;
122
+ if (!apiKey) {
123
+ const answer = await inquirer.prompt([
124
+ {
125
+ type: 'password',
126
+ name: 'apiKey',
127
+ message: 'API key:',
128
+ mask: '*',
129
+ validate: (input) => {
130
+ if (!input)
131
+ return 'API key is required';
132
+ return true;
133
+ },
110
134
  },
111
- },
112
- ]);
135
+ ]);
136
+ apiKey = answer.apiKey;
137
+ }
113
138
  // Step 6: Create the Kubernetes secret
114
139
  const secretName = `${modelName}-model-api-key`;
115
- output.info(`creating secret ${secretName}...`);
116
140
  try {
117
- // Delete existing secret if it exists (update scenario)
118
- await execa('kubectl', ['delete', 'secret', secretName], {
119
- stdio: 'pipe',
120
- }).catch(() => {
121
- // Ignore error if secret doesn't exist
122
- });
123
- // Create the secret
124
141
  await execa('kubectl', [
125
142
  'create',
126
143
  'secret',
@@ -128,7 +145,7 @@ export async function createModel(modelName) {
128
145
  secretName,
129
146
  `--from-literal=api-key=${apiKey}`,
130
147
  ], { stdio: 'pipe' });
131
- output.success(`secret ${secretName} created`);
148
+ output.success(`created secret ${secretName}`);
132
149
  }
133
150
  catch (error) {
134
151
  output.error('failed to create secret');
@@ -146,7 +163,7 @@ export async function createModel(modelName) {
146
163
  spec: {
147
164
  type: modelType,
148
165
  model: {
149
- value: commonAnswers.modelVersion,
166
+ value: model,
150
167
  },
151
168
  config: {},
152
169
  },
@@ -192,22 +209,12 @@ export async function createModel(modelName) {
192
209
  input: manifestJson,
193
210
  stdio: ['pipe', 'pipe', 'pipe'],
194
211
  });
195
- output.success(`model ${modelName} created successfully`);
196
- console.log();
197
- output.info('you can now use this model with ARK agents and queries');
212
+ output.success(`model ${modelName} created`);
198
213
  return true;
199
214
  }
200
215
  catch (error) {
201
216
  output.error('failed to create model');
202
217
  console.error(error);
203
- // Try to clean up the secret if model creation failed
204
- try {
205
- await execa('kubectl', ['delete', 'secret', secretName], { stdio: 'pipe' });
206
- output.info(`cleaned up secret ${secretName}`);
207
- }
208
- catch {
209
- // Ignore cleanup errors
210
- }
211
218
  return false;
212
219
  }
213
220
  }
@@ -31,28 +31,23 @@ describe('createModel', () => {
31
31
  // Prompts for model details
32
32
  mockInquirer.prompt
33
33
  .mockResolvedValueOnce({ modelType: 'openai' })
34
- .mockResolvedValueOnce({
35
- modelVersion: 'gpt-4',
36
- baseUrl: 'https://api.openai.com/',
37
- })
34
+ .mockResolvedValueOnce({ model: 'gpt-4' })
35
+ .mockResolvedValueOnce({ baseUrl: 'https://api.openai.com/' })
38
36
  .mockResolvedValueOnce({ apiKey: 'secret-key' });
39
37
  // Secret operations succeed
40
- mockExeca.mockResolvedValueOnce({}); // delete secret (may not exist)
41
38
  mockExeca.mockResolvedValueOnce({}); // create secret
42
39
  mockExeca.mockResolvedValueOnce({}); // apply model
43
40
  const result = await createModel('test-model');
44
41
  expect(result).toBe(true);
45
42
  expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'model', 'test-model'], { stdio: 'pipe' });
46
- expect(mockOutput.success).toHaveBeenCalledWith('model test-model created successfully');
43
+ expect(mockOutput.success).toHaveBeenCalledWith('model test-model created');
47
44
  });
48
45
  it('prompts for name when not provided', async () => {
49
46
  mockInquirer.prompt
50
47
  .mockResolvedValueOnce({ modelName: 'prompted-model' })
51
48
  .mockResolvedValueOnce({ modelType: 'azure' })
52
- .mockResolvedValueOnce({
53
- modelVersion: 'gpt-4',
54
- baseUrl: 'https://azure.com',
55
- })
49
+ .mockResolvedValueOnce({ model: 'gpt-4' })
50
+ .mockResolvedValueOnce({ baseUrl: 'https://azure.com' })
56
51
  .mockResolvedValueOnce({ apiVersion: '2024-12-01' })
57
52
  .mockResolvedValueOnce({ apiKey: 'secret' });
58
53
  mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
@@ -72,10 +67,8 @@ describe('createModel', () => {
72
67
  mockInquirer.prompt
73
68
  .mockResolvedValueOnce({ overwrite: true })
74
69
  .mockResolvedValueOnce({ modelType: 'openai' })
75
- .mockResolvedValueOnce({
76
- modelVersion: 'gpt-4',
77
- baseUrl: 'https://api.openai.com',
78
- })
70
+ .mockResolvedValueOnce({ model: 'gpt-4' })
71
+ .mockResolvedValueOnce({ baseUrl: 'https://api.openai.com' })
79
72
  .mockResolvedValueOnce({ apiKey: 'secret' });
80
73
  mockExeca.mockResolvedValue({}); // remaining kubectl ops
81
74
  const result = await createModel('existing-model');
@@ -93,33 +86,12 @@ describe('createModel', () => {
93
86
  mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
94
87
  mockInquirer.prompt
95
88
  .mockResolvedValueOnce({ modelType: 'openai' })
96
- .mockResolvedValueOnce({
97
- modelVersion: 'gpt-4',
98
- baseUrl: 'https://api.openai.com',
99
- })
89
+ .mockResolvedValueOnce({ model: 'gpt-4' })
90
+ .mockResolvedValueOnce({ baseUrl: 'https://api.openai.com' })
100
91
  .mockResolvedValueOnce({ apiKey: 'secret' });
101
- mockExeca.mockRejectedValueOnce(new Error('delete failed')); // delete secret may fail
102
92
  mockExeca.mockRejectedValueOnce(new Error('secret creation failed')); // create secret fails
103
93
  const result = await createModel('test-model');
104
94
  expect(result).toBe(false);
105
95
  expect(mockOutput.error).toHaveBeenCalledWith('failed to create secret');
106
96
  });
107
- it('cleans up secret if model creation fails', async () => {
108
- mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
109
- mockInquirer.prompt
110
- .mockResolvedValueOnce({ modelType: 'openai' })
111
- .mockResolvedValueOnce({
112
- modelVersion: 'gpt-4',
113
- baseUrl: 'https://api.openai.com',
114
- })
115
- .mockResolvedValueOnce({ apiKey: 'secret' });
116
- mockExeca.mockResolvedValueOnce({}); // delete secret
117
- mockExeca.mockResolvedValueOnce({}); // create secret
118
- mockExeca.mockRejectedValueOnce(new Error('apply failed')); // apply model fails
119
- mockExeca.mockResolvedValueOnce({}); // cleanup secret
120
- const result = await createModel('test-model');
121
- expect(result).toBe(false);
122
- expect(mockOutput.error).toHaveBeenCalledWith('failed to create model');
123
- expect(mockExeca).toHaveBeenCalledWith('kubectl', ['delete', 'secret', 'test-model-model-api-key'], { stdio: 'pipe' });
124
- });
125
97
  });
@@ -53,8 +53,14 @@ export function createModelsCommand(_) {
53
53
  createCommand
54
54
  .description('Create a new model')
55
55
  .argument('[name]', 'Model name (optional)')
56
- .action(async (name) => {
57
- await createModel(name);
56
+ .option('--type <type>', 'Model provider type (azure, openai, bedrock)')
57
+ .option('--model <model>', 'Model name (e.g., gpt-4o-mini)')
58
+ .option('--base-url <url>', 'Base URL for the model API')
59
+ .option('--api-key <key>', 'API key for authentication')
60
+ .option('--api-version <version>', 'API version (Azure only)')
61
+ .option('--yes', 'Skip confirmation prompts')
62
+ .action(async (name, options) => {
63
+ await createModel(name, options);
58
64
  });
59
65
  modelsCommand.addCommand(createCommand);
60
66
  // Add query command
@@ -76,7 +76,7 @@ describe('models command', () => {
76
76
  it('create subcommand works', async () => {
77
77
  const command = createModelsCommand({});
78
78
  await command.parseAsync(['node', 'test', 'create', 'my-model']);
79
- expect(mockCreateModel).toHaveBeenCalledWith('my-model');
79
+ expect(mockCreateModel).toHaveBeenCalledWith('my-model', expect.objectContaining({}));
80
80
  });
81
81
  it('query subcommand works', async () => {
82
82
  const command = createModelsCommand({});
@@ -1,3 +1,5 @@
1
1
  import { Command } from 'commander';
2
- export declare function checkStatus(): Promise<void>;
2
+ export declare function checkStatus(serviceNames?: string[], options?: {
3
+ waitForReady?: string;
4
+ }): Promise<void>;
3
5
  export declare function createStatusCommand(): Command;
@@ -4,6 +4,10 @@ import ora from 'ora';
4
4
  import { StatusChecker } from '../../components/statusChecker.js';
5
5
  import { StatusFormatter, } from '../../ui/statusFormatter.js';
6
6
  import { fetchVersionInfo } from '../../lib/versions.js';
7
+ import { waitForServicesReady, } from '../../lib/waitForReady.js';
8
+ import { arkServices } from '../../arkServices.js';
9
+ import output from '../../lib/output.js';
10
+ import { parseTimeoutToSeconds } from '../../lib/timeout.js';
7
11
  /**
8
12
  * Enrich service with formatted details including version/revision
9
13
  */
@@ -249,7 +253,7 @@ function buildStatusSections(data, versionInfo) {
249
253
  sections.push({ title: 'ark status:', lines: arkStatusLines });
250
254
  return sections;
251
255
  }
252
- export async function checkStatus() {
256
+ export async function checkStatus(serviceNames, options) {
253
257
  const spinner = ora('Checking system status').start();
254
258
  try {
255
259
  spinner.text = 'Checking system dependencies';
@@ -264,6 +268,52 @@ export async function checkStatus() {
264
268
  spinner.stop();
265
269
  const sections = buildStatusSections(statusData, versionInfo);
266
270
  StatusFormatter.printSections(sections);
271
+ if (options?.waitForReady) {
272
+ const timeoutSeconds = parseTimeoutToSeconds(options.waitForReady);
273
+ let servicesToWait = [];
274
+ if (serviceNames && serviceNames.length > 0) {
275
+ servicesToWait = serviceNames
276
+ .map((name) => Object.values(arkServices).find((s) => s.name === name))
277
+ .filter((s) => s !== undefined &&
278
+ s.k8sDeploymentName !== undefined &&
279
+ s.namespace !== undefined);
280
+ if (servicesToWait.length === 0) {
281
+ output.error(`No valid services found matching: ${serviceNames.join(', ')}`);
282
+ process.exit(1);
283
+ }
284
+ }
285
+ else {
286
+ servicesToWait = Object.values(arkServices).filter((s) => s.enabled &&
287
+ s.category === 'core' &&
288
+ s.k8sDeploymentName &&
289
+ s.namespace);
290
+ }
291
+ console.log();
292
+ const waitSpinner = ora(`Waiting for services to be ready (timeout: ${timeoutSeconds}s)...`).start();
293
+ const statusMap = new Map();
294
+ servicesToWait.forEach((s) => statusMap.set(s.name, false));
295
+ const startTime = Date.now();
296
+ const result = await waitForServicesReady(servicesToWait, timeoutSeconds, (progress) => {
297
+ statusMap.set(progress.serviceName, progress.ready);
298
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
299
+ const lines = servicesToWait.map((s) => {
300
+ const ready = statusMap.get(s.name);
301
+ const icon = ready ? '✓' : '⋯';
302
+ const status = ready ? 'ready' : 'waiting...';
303
+ const color = ready ? chalk.green : chalk.yellow;
304
+ return ` ${color(icon)} ${chalk.bold(s.name)} ${chalk.blue(`(${s.namespace})`)} - ${status}`;
305
+ });
306
+ waitSpinner.text = `Waiting for services to be ready (${elapsed}/${timeoutSeconds}s)...\n${lines.join('\n')}`;
307
+ });
308
+ if (result) {
309
+ waitSpinner.succeed('All services are ready');
310
+ process.exit(0);
311
+ }
312
+ else {
313
+ waitSpinner.fail(`Services did not become ready within ${timeoutSeconds} seconds`);
314
+ process.exit(1);
315
+ }
316
+ }
267
317
  process.exit(0);
268
318
  }
269
319
  catch (error) {
@@ -276,6 +326,8 @@ export function createStatusCommand() {
276
326
  const statusCommand = new Command('status');
277
327
  statusCommand
278
328
  .description('Check ARK system status')
279
- .action(() => checkStatus());
329
+ .argument('[services...]', 'specific services to check (optional)')
330
+ .option('--wait-for-ready <timeout>', 'wait for services to be ready (e.g., 30s, 2m, 1h)')
331
+ .action((services, options) => checkStatus(services, options));
280
332
  return statusCommand;
281
333
  }