@agents-at-scale/ark 0.1.35 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/arkServices.d.ts +42 -0
- package/dist/arkServices.js +138 -0
- package/dist/arkServices.spec.d.ts +1 -0
- package/dist/arkServices.spec.js +24 -0
- package/dist/commands/agents/index.d.ts +3 -0
- package/dist/commands/agents/index.js +65 -0
- package/dist/commands/agents/index.spec.d.ts +1 -0
- package/dist/commands/agents/index.spec.js +67 -0
- package/dist/commands/chat/index.d.ts +3 -0
- package/dist/commands/chat/index.js +29 -0
- package/dist/commands/cluster/get.d.ts +2 -0
- package/dist/commands/cluster/get.js +39 -0
- package/dist/commands/cluster/get.spec.d.ts +1 -0
- package/dist/commands/cluster/get.spec.js +92 -0
- package/dist/commands/cluster/index.d.ts +2 -1
- package/dist/commands/cluster/index.js +3 -5
- package/dist/commands/cluster/index.spec.d.ts +1 -0
- package/dist/commands/cluster/index.spec.js +24 -0
- package/dist/commands/completion/index.d.ts +3 -0
- package/dist/commands/completion/index.js +230 -0
- package/dist/commands/completion/index.spec.d.ts +1 -0
- package/dist/commands/completion/index.spec.js +34 -0
- package/dist/commands/config/index.d.ts +3 -0
- package/dist/commands/config/index.js +42 -0
- package/dist/commands/config/index.spec.d.ts +1 -0
- package/dist/commands/config/index.spec.js +78 -0
- package/dist/commands/dashboard/index.d.ts +4 -0
- package/dist/commands/dashboard/index.js +39 -0
- package/dist/commands/docs/index.d.ts +4 -0
- package/dist/commands/docs/index.js +18 -0
- package/dist/commands/generate/config.js +5 -24
- package/dist/commands/generate/generators/mcpserver.d.ts +2 -1
- package/dist/commands/generate/generators/mcpserver.js +26 -5
- package/dist/commands/generate/generators/project.js +22 -41
- package/dist/commands/generate/index.d.ts +2 -1
- package/dist/commands/generate/index.js +1 -1
- package/dist/commands/install/index.d.ts +8 -0
- package/dist/commands/install/index.js +295 -0
- package/dist/commands/install/index.spec.d.ts +1 -0
- package/dist/commands/install/index.spec.js +143 -0
- package/dist/commands/models/create.d.ts +1 -0
- package/dist/commands/models/create.js +213 -0
- package/dist/commands/models/create.spec.d.ts +1 -0
- package/dist/commands/models/create.spec.js +125 -0
- package/dist/commands/models/index.d.ts +3 -0
- package/dist/commands/models/index.js +75 -0
- package/dist/commands/models/index.spec.d.ts +1 -0
- package/dist/commands/models/index.spec.js +96 -0
- package/dist/commands/query/index.d.ts +3 -0
- package/dist/commands/query/index.js +24 -0
- package/dist/commands/query/index.spec.d.ts +1 -0
- package/dist/commands/query/index.spec.js +53 -0
- package/dist/commands/routes/index.d.ts +3 -0
- package/dist/commands/routes/index.js +93 -0
- package/dist/commands/status/index.d.ts +3 -0
- package/dist/commands/status/index.js +281 -0
- package/dist/commands/targets/index.d.ts +3 -0
- package/dist/commands/targets/index.js +72 -0
- package/dist/commands/targets/index.spec.d.ts +1 -0
- package/dist/commands/targets/index.spec.js +154 -0
- package/dist/commands/teams/index.d.ts +3 -0
- package/dist/commands/teams/index.js +64 -0
- package/dist/commands/teams/index.spec.d.ts +1 -0
- package/dist/commands/teams/index.spec.js +70 -0
- package/dist/commands/tools/index.d.ts +3 -0
- package/dist/commands/tools/index.js +49 -0
- package/dist/commands/tools/index.spec.d.ts +1 -0
- package/dist/commands/tools/index.spec.js +70 -0
- package/dist/commands/uninstall/index.d.ts +3 -0
- package/dist/commands/uninstall/index.js +101 -0
- package/dist/commands/uninstall/index.spec.d.ts +1 -0
- package/dist/commands/uninstall/index.spec.js +125 -0
- package/dist/components/ChatUI.d.ts +16 -0
- package/dist/components/ChatUI.js +801 -0
- package/dist/components/statusChecker.d.ts +14 -24
- package/dist/components/statusChecker.js +295 -129
- package/dist/index.d.ts +1 -1
- package/dist/index.js +42 -42
- package/dist/lib/arkApiClient.d.ts +53 -0
- package/dist/lib/arkApiClient.js +102 -0
- package/dist/lib/arkApiProxy.d.ts +9 -0
- package/dist/lib/arkApiProxy.js +22 -0
- package/dist/lib/arkServiceProxy.d.ts +14 -0
- package/dist/lib/arkServiceProxy.js +95 -0
- package/dist/lib/arkStatus.d.ts +10 -0
- package/dist/lib/arkStatus.js +79 -0
- package/dist/lib/arkStatus.spec.d.ts +1 -0
- package/dist/lib/arkStatus.spec.js +49 -0
- package/dist/lib/chatClient.d.ts +33 -0
- package/dist/lib/chatClient.js +93 -0
- package/dist/lib/cluster.d.ts +2 -1
- package/dist/lib/cluster.js +37 -16
- package/dist/lib/cluster.spec.d.ts +1 -0
- package/dist/lib/cluster.spec.js +338 -0
- package/dist/lib/commands.d.ts +16 -0
- package/dist/lib/commands.js +29 -0
- package/dist/lib/commands.spec.d.ts +1 -0
- package/dist/lib/commands.spec.js +146 -0
- package/dist/lib/config.d.ts +26 -80
- package/dist/lib/config.js +70 -205
- package/dist/lib/config.spec.d.ts +1 -0
- package/dist/lib/config.spec.js +99 -0
- package/dist/lib/errors.js +1 -1
- package/dist/lib/errors.spec.d.ts +1 -0
- package/dist/lib/errors.spec.js +221 -0
- package/dist/lib/executeQuery.d.ts +20 -0
- package/dist/lib/executeQuery.js +135 -0
- package/dist/lib/executeQuery.spec.d.ts +1 -0
- package/dist/lib/executeQuery.spec.js +170 -0
- package/dist/lib/nextSteps.d.ts +4 -0
- package/dist/lib/nextSteps.js +20 -0
- package/dist/lib/nextSteps.spec.d.ts +1 -0
- package/dist/lib/nextSteps.spec.js +59 -0
- package/dist/lib/output.d.ts +36 -0
- package/dist/lib/output.js +89 -0
- package/dist/lib/output.spec.d.ts +1 -0
- package/dist/lib/output.spec.js +123 -0
- package/dist/lib/startup.d.ts +9 -0
- package/dist/lib/startup.js +87 -0
- package/dist/lib/startup.spec.d.ts +1 -0
- package/dist/lib/startup.spec.js +152 -0
- package/dist/lib/types.d.ts +87 -3
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +51 -0
- package/dist/types/types.d.ts +40 -0
- package/dist/types/types.js +1 -0
- package/dist/ui/AgentSelector.d.ts +8 -0
- package/dist/ui/AgentSelector.js +53 -0
- package/dist/ui/MainMenu.d.ts +5 -1
- package/dist/ui/MainMenu.js +226 -91
- package/dist/ui/ModelSelector.d.ts +8 -0
- package/dist/ui/ModelSelector.js +53 -0
- package/dist/ui/TeamSelector.d.ts +8 -0
- package/dist/ui/TeamSelector.js +55 -0
- package/dist/ui/ToolSelector.d.ts +8 -0
- package/dist/ui/ToolSelector.js +53 -0
- package/dist/ui/statusFormatter.d.ts +22 -7
- package/dist/ui/statusFormatter.js +39 -39
- package/dist/ui/statusFormatter.spec.d.ts +1 -0
- package/dist/ui/statusFormatter.spec.js +58 -0
- package/package.json +16 -5
- package/dist/commands/cluster/get-ip.d.ts +0 -2
- package/dist/commands/cluster/get-ip.js +0 -32
- package/dist/commands/cluster/get-type.d.ts +0 -2
- package/dist/commands/cluster/get-type.js +0 -26
- package/dist/commands/completion.d.ts +0 -2
- package/dist/commands/completion.js +0 -108
- package/dist/commands/config.d.ts +0 -5
- package/dist/commands/config.js +0 -327
- package/dist/components/DashboardCLI.d.ts +0 -3
- package/dist/components/DashboardCLI.js +0 -149
- package/dist/config.d.ts +0 -42
- package/dist/config.js +0 -243
- package/dist/lib/arkClient.d.ts +0 -32
- package/dist/lib/arkClient.js +0 -43
- package/dist/lib/consts.d.ts +0 -10
- package/dist/lib/consts.js +0 -15
- package/dist/lib/exec.d.ts +0 -5
- package/dist/lib/exec.js +0 -20
- package/dist/lib/gatewayManager.d.ts +0 -24
- package/dist/lib/gatewayManager.js +0 -85
- package/dist/lib/kubernetes.d.ts +0 -28
- package/dist/lib/kubernetes.js +0 -122
- package/dist/lib/progress.d.ts +0 -128
- package/dist/lib/progress.js +0 -273
- package/dist/lib/wrappers/git.d.ts +0 -2
- package/dist/lib/wrappers/git.js +0 -43
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import output from '../../lib/output.js';
|
|
4
|
+
export async function createModel(modelName) {
|
|
5
|
+
// Step 1: Get model name if not provided
|
|
6
|
+
if (!modelName) {
|
|
7
|
+
const nameAnswer = await inquirer.prompt([
|
|
8
|
+
{
|
|
9
|
+
type: 'input',
|
|
10
|
+
name: 'modelName',
|
|
11
|
+
message: 'model name:',
|
|
12
|
+
default: 'default',
|
|
13
|
+
validate: (input) => {
|
|
14
|
+
if (!input)
|
|
15
|
+
return 'model name is required';
|
|
16
|
+
// Kubernetes name validation
|
|
17
|
+
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(input)) {
|
|
18
|
+
return 'model name must be a valid Kubernetes resource name';
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
]);
|
|
24
|
+
modelName = nameAnswer.modelName;
|
|
25
|
+
}
|
|
26
|
+
// Check if model already exists
|
|
27
|
+
try {
|
|
28
|
+
await execa('kubectl', ['get', 'model', modelName], { stdio: 'pipe' });
|
|
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;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Model doesn't exist, continue
|
|
45
|
+
}
|
|
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
|
+
}
|
|
81
|
+
},
|
|
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([
|
|
90
|
+
{
|
|
91
|
+
type: 'input',
|
|
92
|
+
name: 'apiVersion',
|
|
93
|
+
message: 'Azure API version:',
|
|
94
|
+
default: '2024-12-01-preview',
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
apiVersion = azureAnswers.apiVersion;
|
|
98
|
+
}
|
|
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;
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
// Step 6: Create the Kubernetes secret
|
|
114
|
+
const secretName = `${modelName}-model-api-key`;
|
|
115
|
+
output.info(`creating secret ${secretName}...`);
|
|
116
|
+
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
|
+
await execa('kubectl', [
|
|
125
|
+
'create',
|
|
126
|
+
'secret',
|
|
127
|
+
'generic',
|
|
128
|
+
secretName,
|
|
129
|
+
`--from-literal=api-key=${apiKey}`,
|
|
130
|
+
], { stdio: 'pipe' });
|
|
131
|
+
output.success(`secret ${secretName} created`);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
output.error('failed to create secret');
|
|
135
|
+
console.error(error);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
// Step 7: Create the Model resource
|
|
139
|
+
output.info(`creating model ${modelName}...`);
|
|
140
|
+
const modelManifest = {
|
|
141
|
+
apiVersion: 'ark.mckinsey.com/v1alpha1',
|
|
142
|
+
kind: 'Model',
|
|
143
|
+
metadata: {
|
|
144
|
+
name: modelName,
|
|
145
|
+
},
|
|
146
|
+
spec: {
|
|
147
|
+
type: modelType,
|
|
148
|
+
model: {
|
|
149
|
+
value: commonAnswers.modelVersion,
|
|
150
|
+
},
|
|
151
|
+
config: {},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
// Add provider-specific config
|
|
155
|
+
if (modelType === 'azure') {
|
|
156
|
+
modelManifest.spec.config.azure = {
|
|
157
|
+
apiKey: {
|
|
158
|
+
valueFrom: {
|
|
159
|
+
secretKeyRef: {
|
|
160
|
+
name: secretName,
|
|
161
|
+
key: 'api-key',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
baseUrl: {
|
|
166
|
+
value: baseUrl,
|
|
167
|
+
},
|
|
168
|
+
apiVersion: {
|
|
169
|
+
value: apiVersion,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
modelManifest.spec.config.openai = {
|
|
175
|
+
apiKey: {
|
|
176
|
+
valueFrom: {
|
|
177
|
+
secretKeyRef: {
|
|
178
|
+
name: secretName,
|
|
179
|
+
key: 'api-key',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
baseUrl: {
|
|
184
|
+
value: baseUrl,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
// Apply the model manifest using kubectl
|
|
190
|
+
const manifestJson = JSON.stringify(modelManifest);
|
|
191
|
+
await execa('kubectl', ['apply', '-f', '-'], {
|
|
192
|
+
input: manifestJson,
|
|
193
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
194
|
+
});
|
|
195
|
+
output.success(`model ${modelName} created successfully`);
|
|
196
|
+
console.log();
|
|
197
|
+
output.info('you can now use this model with ARK agents and queries');
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
output.error('failed to create model');
|
|
202
|
+
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
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
const mockExeca = jest.fn();
|
|
3
|
+
jest.unstable_mockModule('execa', () => ({
|
|
4
|
+
execa: mockExeca,
|
|
5
|
+
}));
|
|
6
|
+
const mockInquirer = {
|
|
7
|
+
prompt: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
jest.unstable_mockModule('inquirer', () => ({
|
|
10
|
+
default: mockInquirer,
|
|
11
|
+
}));
|
|
12
|
+
const mockOutput = {
|
|
13
|
+
info: jest.fn(),
|
|
14
|
+
warning: jest.fn(),
|
|
15
|
+
error: jest.fn(),
|
|
16
|
+
success: jest.fn(),
|
|
17
|
+
};
|
|
18
|
+
jest.unstable_mockModule('../../lib/output.js', () => ({
|
|
19
|
+
default: mockOutput,
|
|
20
|
+
}));
|
|
21
|
+
jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
22
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
23
|
+
const { createModel } = await import('./create.js');
|
|
24
|
+
describe('createModel', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
it('creates new model with provided name', async () => {
|
|
29
|
+
// Model doesn't exist
|
|
30
|
+
mockExeca.mockRejectedValueOnce(new Error('not found'));
|
|
31
|
+
// Prompts for model details
|
|
32
|
+
mockInquirer.prompt
|
|
33
|
+
.mockResolvedValueOnce({ modelType: 'openai' })
|
|
34
|
+
.mockResolvedValueOnce({
|
|
35
|
+
modelVersion: 'gpt-4',
|
|
36
|
+
baseUrl: 'https://api.openai.com/',
|
|
37
|
+
})
|
|
38
|
+
.mockResolvedValueOnce({ apiKey: 'secret-key' });
|
|
39
|
+
// Secret operations succeed
|
|
40
|
+
mockExeca.mockResolvedValueOnce({}); // delete secret (may not exist)
|
|
41
|
+
mockExeca.mockResolvedValueOnce({}); // create secret
|
|
42
|
+
mockExeca.mockResolvedValueOnce({}); // apply model
|
|
43
|
+
const result = await createModel('test-model');
|
|
44
|
+
expect(result).toBe(true);
|
|
45
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'model', 'test-model'], { stdio: 'pipe' });
|
|
46
|
+
expect(mockOutput.success).toHaveBeenCalledWith('model test-model created successfully');
|
|
47
|
+
});
|
|
48
|
+
it('prompts for name when not provided', async () => {
|
|
49
|
+
mockInquirer.prompt
|
|
50
|
+
.mockResolvedValueOnce({ modelName: 'prompted-model' })
|
|
51
|
+
.mockResolvedValueOnce({ modelType: 'azure' })
|
|
52
|
+
.mockResolvedValueOnce({
|
|
53
|
+
modelVersion: 'gpt-4',
|
|
54
|
+
baseUrl: 'https://azure.com',
|
|
55
|
+
})
|
|
56
|
+
.mockResolvedValueOnce({ apiVersion: '2024-12-01' })
|
|
57
|
+
.mockResolvedValueOnce({ apiKey: 'secret' });
|
|
58
|
+
mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
|
|
59
|
+
mockExeca.mockResolvedValue({}); // all kubectl ops succeed
|
|
60
|
+
const result = await createModel();
|
|
61
|
+
expect(result).toBe(true);
|
|
62
|
+
expect(mockInquirer.prompt).toHaveBeenCalledWith([
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
name: 'modelName',
|
|
65
|
+
message: 'model name:',
|
|
66
|
+
}),
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
it('handles overwrite confirmation when model exists', async () => {
|
|
70
|
+
// Model exists
|
|
71
|
+
mockExeca.mockResolvedValueOnce({});
|
|
72
|
+
mockInquirer.prompt
|
|
73
|
+
.mockResolvedValueOnce({ overwrite: true })
|
|
74
|
+
.mockResolvedValueOnce({ modelType: 'openai' })
|
|
75
|
+
.mockResolvedValueOnce({
|
|
76
|
+
modelVersion: 'gpt-4',
|
|
77
|
+
baseUrl: 'https://api.openai.com',
|
|
78
|
+
})
|
|
79
|
+
.mockResolvedValueOnce({ apiKey: 'secret' });
|
|
80
|
+
mockExeca.mockResolvedValue({}); // remaining kubectl ops
|
|
81
|
+
const result = await createModel('existing-model');
|
|
82
|
+
expect(result).toBe(true);
|
|
83
|
+
expect(mockOutput.warning).toHaveBeenCalledWith('model existing-model already exists');
|
|
84
|
+
});
|
|
85
|
+
it('cancels when user declines overwrite', async () => {
|
|
86
|
+
mockExeca.mockResolvedValueOnce({}); // model exists
|
|
87
|
+
mockInquirer.prompt.mockResolvedValueOnce({ overwrite: false });
|
|
88
|
+
const result = await createModel('existing-model');
|
|
89
|
+
expect(result).toBe(false);
|
|
90
|
+
expect(mockOutput.info).toHaveBeenCalledWith('model creation cancelled');
|
|
91
|
+
});
|
|
92
|
+
it('handles secret creation failure', async () => {
|
|
93
|
+
mockExeca.mockRejectedValueOnce(new Error('not found')); // model doesn't exist
|
|
94
|
+
mockInquirer.prompt
|
|
95
|
+
.mockResolvedValueOnce({ modelType: 'openai' })
|
|
96
|
+
.mockResolvedValueOnce({
|
|
97
|
+
modelVersion: 'gpt-4',
|
|
98
|
+
baseUrl: 'https://api.openai.com',
|
|
99
|
+
})
|
|
100
|
+
.mockResolvedValueOnce({ apiKey: 'secret' });
|
|
101
|
+
mockExeca.mockRejectedValueOnce(new Error('delete failed')); // delete secret may fail
|
|
102
|
+
mockExeca.mockRejectedValueOnce(new Error('secret creation failed')); // create secret fails
|
|
103
|
+
const result = await createModel('test-model');
|
|
104
|
+
expect(result).toBe(false);
|
|
105
|
+
expect(mockOutput.error).toHaveBeenCalledWith('failed to create secret');
|
|
106
|
+
});
|
|
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
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
import output from '../../lib/output.js';
|
|
4
|
+
import { createModel } from './create.js';
|
|
5
|
+
import { executeQuery } from '../../lib/executeQuery.js';
|
|
6
|
+
async function listModels(options) {
|
|
7
|
+
try {
|
|
8
|
+
// Use kubectl to get models
|
|
9
|
+
const result = await execa('kubectl', ['get', 'models', '-o', 'json'], {
|
|
10
|
+
stdio: 'pipe',
|
|
11
|
+
});
|
|
12
|
+
const data = JSON.parse(result.stdout);
|
|
13
|
+
const models = data.items || [];
|
|
14
|
+
if (options.output === 'json') {
|
|
15
|
+
// Output the raw items for JSON format
|
|
16
|
+
console.log(JSON.stringify(models, null, 2));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
if (models.length === 0) {
|
|
20
|
+
output.info('No models found');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Just output the model names
|
|
24
|
+
models.forEach((model) => {
|
|
25
|
+
console.log(model.metadata.name);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
output.error('fetching models:', error instanceof Error ? error.message : error);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function createModelsCommand(_) {
|
|
35
|
+
const modelsCommand = new Command('models');
|
|
36
|
+
modelsCommand
|
|
37
|
+
.description('List available models')
|
|
38
|
+
.option('-o, --output <format>', 'Output format (json)', 'text')
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
await listModels(options);
|
|
41
|
+
});
|
|
42
|
+
const listCommand = new Command('list');
|
|
43
|
+
listCommand
|
|
44
|
+
.alias('ls')
|
|
45
|
+
.description('List available models')
|
|
46
|
+
.option('-o, --output <format>', 'Output format (json)', 'text')
|
|
47
|
+
.action(async (options) => {
|
|
48
|
+
await listModels(options);
|
|
49
|
+
});
|
|
50
|
+
modelsCommand.addCommand(listCommand);
|
|
51
|
+
// Add create command
|
|
52
|
+
const createCommand = new Command('create');
|
|
53
|
+
createCommand
|
|
54
|
+
.description('Create a new model')
|
|
55
|
+
.argument('[name]', 'Model name (optional)')
|
|
56
|
+
.action(async (name) => {
|
|
57
|
+
await createModel(name);
|
|
58
|
+
});
|
|
59
|
+
modelsCommand.addCommand(createCommand);
|
|
60
|
+
// Add query command
|
|
61
|
+
const queryCommand = new Command('query');
|
|
62
|
+
queryCommand
|
|
63
|
+
.description('Query a model')
|
|
64
|
+
.argument('<name>', 'Model name (e.g., default)')
|
|
65
|
+
.argument('<message>', 'Message to send')
|
|
66
|
+
.action(async (name, message) => {
|
|
67
|
+
await executeQuery({
|
|
68
|
+
targetType: 'model',
|
|
69
|
+
targetName: name,
|
|
70
|
+
message,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
modelsCommand.addCommand(queryCommand);
|
|
74
|
+
return modelsCommand;
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
const mockExeca = jest.fn();
|
|
4
|
+
jest.unstable_mockModule('execa', () => ({
|
|
5
|
+
execa: mockExeca,
|
|
6
|
+
}));
|
|
7
|
+
const mockOutput = {
|
|
8
|
+
info: jest.fn(),
|
|
9
|
+
error: jest.fn(),
|
|
10
|
+
};
|
|
11
|
+
jest.unstable_mockModule('../../lib/output.js', () => ({
|
|
12
|
+
default: mockOutput,
|
|
13
|
+
}));
|
|
14
|
+
const mockCreateModel = jest.fn();
|
|
15
|
+
jest.unstable_mockModule('./create.js', () => ({
|
|
16
|
+
createModel: mockCreateModel,
|
|
17
|
+
}));
|
|
18
|
+
const mockExecuteQuery = jest.fn();
|
|
19
|
+
jest.unstable_mockModule('../../lib/executeQuery.js', () => ({
|
|
20
|
+
executeQuery: mockExecuteQuery,
|
|
21
|
+
parseTarget: jest.fn(),
|
|
22
|
+
}));
|
|
23
|
+
const mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {
|
|
24
|
+
throw new Error('process.exit called');
|
|
25
|
+
}));
|
|
26
|
+
const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
27
|
+
const { createModelsCommand } = await import('./index.js');
|
|
28
|
+
describe('models command', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
it('creates command with correct structure', () => {
|
|
33
|
+
const command = createModelsCommand({});
|
|
34
|
+
expect(command).toBeInstanceOf(Command);
|
|
35
|
+
expect(command.name()).toBe('models');
|
|
36
|
+
});
|
|
37
|
+
it('lists models in text format', async () => {
|
|
38
|
+
const mockModels = {
|
|
39
|
+
items: [{ metadata: { name: 'gpt-4' } }, { metadata: { name: 'claude-3' } }],
|
|
40
|
+
};
|
|
41
|
+
mockExeca.mockResolvedValue({ stdout: JSON.stringify(mockModels) });
|
|
42
|
+
const command = createModelsCommand({});
|
|
43
|
+
await command.parseAsync(['node', 'test']);
|
|
44
|
+
expect(mockExeca).toHaveBeenCalledWith('kubectl', ['get', 'models', '-o', 'json'], { stdio: 'pipe' });
|
|
45
|
+
expect(mockConsoleLog).toHaveBeenCalledWith('gpt-4');
|
|
46
|
+
expect(mockConsoleLog).toHaveBeenCalledWith('claude-3');
|
|
47
|
+
});
|
|
48
|
+
it('lists models in json format', async () => {
|
|
49
|
+
const mockModels = {
|
|
50
|
+
items: [{ metadata: { name: 'gpt-4' } }],
|
|
51
|
+
};
|
|
52
|
+
mockExeca.mockResolvedValue({ stdout: JSON.stringify(mockModels) });
|
|
53
|
+
const command = createModelsCommand({});
|
|
54
|
+
await command.parseAsync(['node', 'test', '-o', 'json']);
|
|
55
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(JSON.stringify(mockModels.items, null, 2));
|
|
56
|
+
});
|
|
57
|
+
it('shows info when no models', async () => {
|
|
58
|
+
mockExeca.mockResolvedValue({ stdout: JSON.stringify({ items: [] }) });
|
|
59
|
+
const command = createModelsCommand({});
|
|
60
|
+
await command.parseAsync(['node', 'test']);
|
|
61
|
+
expect(mockOutput.info).toHaveBeenCalledWith('No models found');
|
|
62
|
+
});
|
|
63
|
+
it('handles errors', async () => {
|
|
64
|
+
mockExeca.mockRejectedValue(new Error('kubectl failed'));
|
|
65
|
+
const command = createModelsCommand({});
|
|
66
|
+
await expect(command.parseAsync(['node', 'test'])).rejects.toThrow('process.exit called');
|
|
67
|
+
expect(mockOutput.error).toHaveBeenCalledWith('fetching models:', 'kubectl failed');
|
|
68
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
69
|
+
});
|
|
70
|
+
it('list subcommand works', async () => {
|
|
71
|
+
mockExeca.mockResolvedValue({ stdout: JSON.stringify({ items: [] }) });
|
|
72
|
+
const command = createModelsCommand({});
|
|
73
|
+
await command.parseAsync(['node', 'test', 'list']);
|
|
74
|
+
expect(mockExeca).toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
it('create subcommand works', async () => {
|
|
77
|
+
const command = createModelsCommand({});
|
|
78
|
+
await command.parseAsync(['node', 'test', 'create', 'my-model']);
|
|
79
|
+
expect(mockCreateModel).toHaveBeenCalledWith('my-model');
|
|
80
|
+
});
|
|
81
|
+
it('query subcommand works', async () => {
|
|
82
|
+
const command = createModelsCommand({});
|
|
83
|
+
await command.parseAsync([
|
|
84
|
+
'node',
|
|
85
|
+
'test',
|
|
86
|
+
'query',
|
|
87
|
+
'default',
|
|
88
|
+
'Hello world',
|
|
89
|
+
]);
|
|
90
|
+
expect(mockExecuteQuery).toHaveBeenCalledWith({
|
|
91
|
+
targetType: 'model',
|
|
92
|
+
targetName: 'default',
|
|
93
|
+
message: 'Hello world',
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import output from '../../lib/output.js';
|
|
3
|
+
import { executeQuery, parseTarget } from '../../lib/executeQuery.js';
|
|
4
|
+
export function createQueryCommand(_) {
|
|
5
|
+
const queryCommand = new Command('query');
|
|
6
|
+
queryCommand
|
|
7
|
+
.description('Execute a single query against a model or agent')
|
|
8
|
+
.argument('<target>', 'Query target (e.g., model/default, agent/my-agent)')
|
|
9
|
+
.argument('<message>', 'Message to send')
|
|
10
|
+
.action(async (target, message) => {
|
|
11
|
+
// Parse and validate target format
|
|
12
|
+
const parsed = parseTarget(target);
|
|
13
|
+
if (!parsed) {
|
|
14
|
+
output.error('Invalid target format. Use: model/name or agent/name etc');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
await executeQuery({
|
|
18
|
+
targetType: parsed.type,
|
|
19
|
+
targetName: parsed.name,
|
|
20
|
+
message,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
return queryCommand;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
const mockExecuteQuery = jest.fn();
|
|
4
|
+
const mockParseTarget = jest.fn();
|
|
5
|
+
jest.unstable_mockModule('../../lib/executeQuery.js', () => ({
|
|
6
|
+
executeQuery: mockExecuteQuery,
|
|
7
|
+
parseTarget: mockParseTarget,
|
|
8
|
+
}));
|
|
9
|
+
const mockOutput = {
|
|
10
|
+
error: jest.fn(),
|
|
11
|
+
};
|
|
12
|
+
jest.unstable_mockModule('../../lib/output.js', () => ({
|
|
13
|
+
default: mockOutput,
|
|
14
|
+
}));
|
|
15
|
+
const mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {
|
|
16
|
+
throw new Error('process.exit called');
|
|
17
|
+
}));
|
|
18
|
+
const { createQueryCommand } = await import('./index.js');
|
|
19
|
+
describe('createQueryCommand', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
it('should create a query command', () => {
|
|
24
|
+
const command = createQueryCommand({});
|
|
25
|
+
expect(command).toBeInstanceOf(Command);
|
|
26
|
+
expect(command.name()).toBe('query');
|
|
27
|
+
expect(command.description()).toBe('Execute a single query against a model or agent');
|
|
28
|
+
});
|
|
29
|
+
it('should parse and execute query with valid target', async () => {
|
|
30
|
+
mockParseTarget.mockReturnValue({
|
|
31
|
+
type: 'model',
|
|
32
|
+
name: 'default',
|
|
33
|
+
});
|
|
34
|
+
mockExecuteQuery.mockResolvedValue(undefined);
|
|
35
|
+
const command = createQueryCommand({});
|
|
36
|
+
await command.parseAsync(['node', 'test', 'model/default', 'Hello world']);
|
|
37
|
+
expect(mockParseTarget).toHaveBeenCalledWith('model/default');
|
|
38
|
+
expect(mockExecuteQuery).toHaveBeenCalledWith({
|
|
39
|
+
targetType: 'model',
|
|
40
|
+
targetName: 'default',
|
|
41
|
+
message: 'Hello world',
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
it('should error on invalid target format', async () => {
|
|
45
|
+
mockParseTarget.mockReturnValue(null);
|
|
46
|
+
const command = createQueryCommand({});
|
|
47
|
+
await expect(command.parseAsync(['node', 'test', 'invalid-target', 'Hello'])).rejects.toThrow('process.exit called');
|
|
48
|
+
expect(mockParseTarget).toHaveBeenCalledWith('invalid-target');
|
|
49
|
+
expect(mockExecuteQuery).not.toHaveBeenCalled();
|
|
50
|
+
expect(mockOutput.error).toHaveBeenCalledWith('Invalid target format. Use: model/name or agent/name etc');
|
|
51
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
52
|
+
});
|
|
53
|
+
});
|