@inkeep/agents-cli 0.1.4 → 0.1.6
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/SUPPLEMENTAL_TERMS.md +40 -0
- package/dist/commands/create.d.ts +12 -0
- package/dist/commands/create.js +865 -0
- package/dist/config.d.ts +4 -4
- package/dist/index.js +5308 -20409
- package/package.json +15 -6
- package/dist/__tests__/api.test.d.ts +0 -1
- package/dist/__tests__/api.test.js +0 -257
- package/dist/__tests__/cli.test.d.ts +0 -1
- package/dist/__tests__/cli.test.js +0 -153
- package/dist/__tests__/commands/config.test.d.ts +0 -1
- package/dist/__tests__/commands/config.test.js +0 -154
- package/dist/__tests__/commands/init.test.d.ts +0 -1
- package/dist/__tests__/commands/init.test.js +0 -186
- package/dist/__tests__/commands/pull.test.d.ts +0 -1
- package/dist/__tests__/commands/pull.test.js +0 -54
- package/dist/__tests__/commands/push-spinner.test.d.ts +0 -1
- package/dist/__tests__/commands/push-spinner.test.js +0 -127
- package/dist/__tests__/commands/push.test.d.ts +0 -1
- package/dist/__tests__/commands/push.test.js +0 -265
- package/dist/__tests__/config-validation.test.d.ts +0 -1
- package/dist/__tests__/config-validation.test.js +0 -98
- package/dist/__tests__/package.test.d.ts +0 -1
- package/dist/__tests__/package.test.js +0 -82
- package/dist/__tests__/utils/json-comparator.test.d.ts +0 -1
- package/dist/__tests__/utils/json-comparator.test.js +0 -174
- package/dist/__tests__/utils/ts-loader.test.d.ts +0 -1
- package/dist/__tests__/utils/ts-loader.test.js +0 -232
- package/dist/api.d.ts +0 -23
- package/dist/api.js +0 -140
- package/dist/commands/chat-enhanced.d.ts +0 -7
- package/dist/commands/chat-enhanced.js +0 -396
- package/dist/commands/chat.d.ts +0 -5
- package/dist/commands/chat.js +0 -125
- package/dist/commands/config.d.ts +0 -6
- package/dist/commands/config.js +0 -128
- package/dist/commands/init.d.ts +0 -5
- package/dist/commands/init.js +0 -171
- package/dist/commands/list-graphs.d.ts +0 -6
- package/dist/commands/list-graphs.js +0 -131
- package/dist/commands/pull.d.ts +0 -15
- package/dist/commands/pull.js +0 -305
- package/dist/commands/pull.llm-generate.d.ts +0 -10
- package/dist/commands/pull.llm-generate.js +0 -184
- package/dist/commands/push.d.ts +0 -6
- package/dist/commands/push.js +0 -268
- package/dist/exports.d.ts +0 -2
- package/dist/exports.js +0 -2
- package/dist/index.js.map +0 -7
- package/dist/types/config.d.ts +0 -9
- package/dist/types/config.js +0 -3
- package/dist/types/graph.d.ts +0 -10
- package/dist/types/graph.js +0 -1
- package/dist/utils/json-comparator.d.ts +0 -60
- package/dist/utils/json-comparator.js +0 -222
- package/dist/utils/mcp-runner.d.ts +0 -6
- package/dist/utils/mcp-runner.js +0 -147
- package/dist/utils/ts-loader.d.ts +0 -5
- package/dist/utils/ts-loader.js +0 -145
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inkeep/agents-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Inkeep CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./dist/index.js",
|
|
12
12
|
"./config": "./dist/config.js",
|
|
13
|
+
"./commands/create": "./dist/commands/create.js",
|
|
13
14
|
"./package.json": "./package.json"
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"@ai-sdk/openai": "2.0.11",
|
|
25
26
|
"@babel/parser": "^7.23.0",
|
|
26
27
|
"@babel/types": "^7.23.0",
|
|
28
|
+
"@clack/prompts": "^0.11.0",
|
|
27
29
|
"@libsql/client": "^0.15.7",
|
|
28
30
|
"ai": "5.0.11",
|
|
29
31
|
"ast-types": "^0.14.2",
|
|
@@ -32,16 +34,19 @@
|
|
|
32
34
|
"commander": "^14.0.0",
|
|
33
35
|
"dotenv": "^17.2.1",
|
|
34
36
|
"drizzle-orm": "^0.44.5",
|
|
37
|
+
"fs-extra": "^11.2.0",
|
|
35
38
|
"inquirer": "^9.2.12",
|
|
36
39
|
"inquirer-autocomplete-prompt": "^3.0.1",
|
|
37
40
|
"ora": "^8.0.1",
|
|
41
|
+
"picocolors": "^1.1.1",
|
|
38
42
|
"recast": "^0.23.0",
|
|
39
43
|
"ts-morph": "^26.0.0",
|
|
40
44
|
"tsx": "^4.20.5",
|
|
41
|
-
"@inkeep/agents-core": "^0.1.
|
|
42
|
-
"@inkeep/agents-manage-ui": "^0.1.
|
|
45
|
+
"@inkeep/agents-core": "^0.1.6",
|
|
46
|
+
"@inkeep/agents-manage-ui": "^0.1.6"
|
|
43
47
|
},
|
|
44
48
|
"devDependencies": {
|
|
49
|
+
"@types/fs-extra": "^11.0.4",
|
|
45
50
|
"@types/inquirer": "^9.0.7",
|
|
46
51
|
"@types/node": "^20.10.0",
|
|
47
52
|
"@vitest/coverage-v8": "^3.2.4",
|
|
@@ -49,17 +54,21 @@
|
|
|
49
54
|
"typescript": "^5.9.2",
|
|
50
55
|
"vitest": "^3.2.4"
|
|
51
56
|
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"zod": "^4.1.5"
|
|
59
|
+
},
|
|
52
60
|
"engines": {
|
|
53
61
|
"node": ">=22.0.0"
|
|
54
62
|
},
|
|
55
63
|
"publishConfig": {
|
|
56
|
-
"access": "
|
|
64
|
+
"access": "public",
|
|
57
65
|
"registry": "https://registry.npmjs.org/"
|
|
58
66
|
},
|
|
59
67
|
"files": [
|
|
60
68
|
"dist",
|
|
61
69
|
"README.md",
|
|
62
|
-
"LICENSE.md"
|
|
70
|
+
"LICENSE.md",
|
|
71
|
+
"SUPPLEMENTAL_TERMS.md"
|
|
63
72
|
],
|
|
64
73
|
"repository": {
|
|
65
74
|
"type": "git",
|
|
@@ -68,7 +77,7 @@
|
|
|
68
77
|
},
|
|
69
78
|
"scripts": {
|
|
70
79
|
"build": "tsup",
|
|
71
|
-
"dev": "
|
|
80
|
+
"dev": "MODE=watch tsup",
|
|
72
81
|
"test": "node -e \"process.exit(process.env.CI ? 0 : 1)\" && vitest --run --config vitest.config.ci.ts || vitest --run",
|
|
73
82
|
"test:debug": "vitest --run --reporter=verbose --no-coverage",
|
|
74
83
|
"test:watch": "vitest",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { ManagementApiClient, ExecutionApiClient } from '../api.js';
|
|
3
|
-
// Mock fetch globally
|
|
4
|
-
const mockFetch = vi.fn();
|
|
5
|
-
global.fetch = mockFetch;
|
|
6
|
-
// Mock config module
|
|
7
|
-
vi.mock('../config.js', () => ({
|
|
8
|
-
getApiUrl: vi.fn(async (override) => override || 'http://localhost:3002'),
|
|
9
|
-
getManagementApiUrl: vi.fn(async (override) => override || 'http://localhost:3002'),
|
|
10
|
-
getExecutionApiUrl: vi.fn(async (override) => override || 'http://localhost:3003'),
|
|
11
|
-
getTenantId: vi.fn(async () => 'test-tenant-id'),
|
|
12
|
-
getProjectId: vi.fn(async () => 'test-project-id'),
|
|
13
|
-
loadConfig: vi.fn(),
|
|
14
|
-
}));
|
|
15
|
-
describe('ApiClient', () => {
|
|
16
|
-
let apiClient;
|
|
17
|
-
let executionApiClient;
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
apiClient = await ManagementApiClient.create();
|
|
20
|
-
executionApiClient = await ExecutionApiClient.create();
|
|
21
|
-
mockFetch.mockClear();
|
|
22
|
-
vi.clearAllMocks();
|
|
23
|
-
});
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
vi.clearAllMocks();
|
|
26
|
-
});
|
|
27
|
-
describe('create', () => {
|
|
28
|
-
it('should use default API URL when none provided', async () => {
|
|
29
|
-
const client = await ManagementApiClient.create();
|
|
30
|
-
expect(client).toBeDefined();
|
|
31
|
-
});
|
|
32
|
-
it('should use provided API URL', async () => {
|
|
33
|
-
const customUrl = 'http://custom.example.com';
|
|
34
|
-
const client = await ManagementApiClient.create(customUrl);
|
|
35
|
-
expect(client).toBeDefined();
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
describe('listGraphs', () => {
|
|
39
|
-
it('should fetch and return graphs list successfully', async () => {
|
|
40
|
-
const mockGraphs = [
|
|
41
|
-
{ id: 'graph1', name: 'Test Graph 1' },
|
|
42
|
-
{ id: 'graph2', name: 'Test Graph 2' },
|
|
43
|
-
];
|
|
44
|
-
mockFetch.mockResolvedValueOnce({
|
|
45
|
-
ok: true,
|
|
46
|
-
json: async () => ({ data: mockGraphs }),
|
|
47
|
-
});
|
|
48
|
-
const result = await apiClient.listGraphs();
|
|
49
|
-
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3002/tenants/test-tenant-id/crud/projects/test-project-id/agent-graphs', {
|
|
50
|
-
method: 'GET',
|
|
51
|
-
headers: {
|
|
52
|
-
'Content-Type': 'application/json',
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
expect(result).toEqual(mockGraphs);
|
|
56
|
-
});
|
|
57
|
-
it('should return empty array when no data field in response', async () => {
|
|
58
|
-
mockFetch.mockResolvedValueOnce({
|
|
59
|
-
ok: true,
|
|
60
|
-
json: async () => ({}),
|
|
61
|
-
});
|
|
62
|
-
const result = await apiClient.listGraphs();
|
|
63
|
-
expect(result).toEqual([]);
|
|
64
|
-
});
|
|
65
|
-
it('should throw error when request fails', async () => {
|
|
66
|
-
mockFetch.mockResolvedValueOnce({
|
|
67
|
-
ok: false,
|
|
68
|
-
statusText: 'Not Found',
|
|
69
|
-
});
|
|
70
|
-
await expect(apiClient.listGraphs()).rejects.toThrow('Failed to list graphs: Not Found');
|
|
71
|
-
});
|
|
72
|
-
it('should throw error when tenant ID is not configured', async () => {
|
|
73
|
-
const { getTenantId } = await import('../config.js');
|
|
74
|
-
vi.mocked(getTenantId).mockResolvedValueOnce(undefined);
|
|
75
|
-
const client = await ManagementApiClient.create();
|
|
76
|
-
await expect(client.listGraphs()).rejects.toThrow('No tenant ID configured. Please run: inkeep init');
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
describe('getGraph', () => {
|
|
80
|
-
it('should return graph when found in list', async () => {
|
|
81
|
-
const mockGraphs = [
|
|
82
|
-
{ id: 'graph1', name: 'Test Graph 1' },
|
|
83
|
-
{ id: 'graph2', name: 'Test Graph 2' },
|
|
84
|
-
];
|
|
85
|
-
mockFetch.mockResolvedValueOnce({
|
|
86
|
-
ok: true,
|
|
87
|
-
json: async () => ({ data: mockGraphs }),
|
|
88
|
-
});
|
|
89
|
-
const result = await apiClient.getGraph('graph1');
|
|
90
|
-
expect(result).toEqual({ id: 'graph1', name: 'Test Graph 1' });
|
|
91
|
-
});
|
|
92
|
-
it('should return null when graph not found', async () => {
|
|
93
|
-
const mockGraphs = [{ id: 'graph1', name: 'Test Graph 1' }];
|
|
94
|
-
mockFetch.mockResolvedValueOnce({
|
|
95
|
-
ok: true,
|
|
96
|
-
json: async () => ({ data: mockGraphs }),
|
|
97
|
-
});
|
|
98
|
-
const result = await apiClient.getGraph('nonexistent');
|
|
99
|
-
expect(result).toBeNull();
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
describe('pushGraph', () => {
|
|
103
|
-
it('should push graph successfully', async () => {
|
|
104
|
-
const graphDefinition = {
|
|
105
|
-
id: 'test-graph',
|
|
106
|
-
name: 'Test Graph',
|
|
107
|
-
description: 'A test graph',
|
|
108
|
-
};
|
|
109
|
-
const expectedResponse = {
|
|
110
|
-
id: 'test-graph',
|
|
111
|
-
name: 'Test Graph',
|
|
112
|
-
tenantId: 'test-tenant-id',
|
|
113
|
-
};
|
|
114
|
-
mockFetch.mockResolvedValueOnce({
|
|
115
|
-
ok: true,
|
|
116
|
-
json: async () => ({ data: expectedResponse }),
|
|
117
|
-
});
|
|
118
|
-
const result = await apiClient.pushGraph(graphDefinition);
|
|
119
|
-
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3002/tenants/test-tenant-id/crud/projects/test-project-id/graph/test-graph', {
|
|
120
|
-
method: 'PUT',
|
|
121
|
-
headers: {
|
|
122
|
-
'Content-Type': 'application/json',
|
|
123
|
-
},
|
|
124
|
-
body: JSON.stringify({
|
|
125
|
-
...graphDefinition,
|
|
126
|
-
tenantId: 'test-tenant-id',
|
|
127
|
-
}),
|
|
128
|
-
});
|
|
129
|
-
expect(result).toEqual(expectedResponse);
|
|
130
|
-
});
|
|
131
|
-
it('should throw error when graph has no id', async () => {
|
|
132
|
-
const graphDefinition = {
|
|
133
|
-
name: 'Test Graph',
|
|
134
|
-
description: 'A test graph without id',
|
|
135
|
-
};
|
|
136
|
-
await expect(apiClient.pushGraph(graphDefinition)).rejects.toThrow('Graph must have an id property');
|
|
137
|
-
});
|
|
138
|
-
it('should throw error when push request fails', async () => {
|
|
139
|
-
const graphDefinition = {
|
|
140
|
-
id: 'test-graph',
|
|
141
|
-
name: 'Test Graph',
|
|
142
|
-
};
|
|
143
|
-
mockFetch.mockResolvedValueOnce({
|
|
144
|
-
ok: false,
|
|
145
|
-
statusText: 'Bad Request',
|
|
146
|
-
text: async () => 'Invalid graph definition',
|
|
147
|
-
});
|
|
148
|
-
await expect(apiClient.pushGraph(graphDefinition)).rejects.toThrow('Failed to push graph: Bad Request\nInvalid graph definition');
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
describe('chatCompletion', () => {
|
|
152
|
-
it('should return streaming response when content type is event-stream', async () => {
|
|
153
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
154
|
-
const mockStream = new ReadableStream();
|
|
155
|
-
mockFetch.mockResolvedValueOnce({
|
|
156
|
-
ok: true,
|
|
157
|
-
headers: {
|
|
158
|
-
get: (key) => (key === 'content-type' ? 'text/event-stream' : null),
|
|
159
|
-
},
|
|
160
|
-
body: mockStream,
|
|
161
|
-
});
|
|
162
|
-
const result = await executionApiClient.chatCompletion('test-graph', messages);
|
|
163
|
-
expect(mockFetch).toHaveBeenCalled();
|
|
164
|
-
expect(result).toBe(mockStream);
|
|
165
|
-
});
|
|
166
|
-
it('should return text response when content type is not event-stream', async () => {
|
|
167
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
168
|
-
const mockResponse = {
|
|
169
|
-
choices: [
|
|
170
|
-
{
|
|
171
|
-
message: {
|
|
172
|
-
content: 'Hello! How can I help you?',
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
};
|
|
177
|
-
mockFetch.mockResolvedValueOnce({
|
|
178
|
-
ok: true,
|
|
179
|
-
headers: {
|
|
180
|
-
get: (key) => (key === 'content-type' ? 'application/json' : null),
|
|
181
|
-
},
|
|
182
|
-
json: async () => mockResponse,
|
|
183
|
-
});
|
|
184
|
-
const result = await executionApiClient.chatCompletion('test-graph', messages);
|
|
185
|
-
expect(result).toBe('Hello! How can I help you?');
|
|
186
|
-
});
|
|
187
|
-
it('should handle response with result field instead of choices', async () => {
|
|
188
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
189
|
-
const mockResponse = {
|
|
190
|
-
result: 'This is the result',
|
|
191
|
-
};
|
|
192
|
-
mockFetch.mockResolvedValueOnce({
|
|
193
|
-
ok: true,
|
|
194
|
-
headers: {
|
|
195
|
-
get: (key) => (key === 'content-type' ? 'application/json' : null),
|
|
196
|
-
},
|
|
197
|
-
json: async () => mockResponse,
|
|
198
|
-
});
|
|
199
|
-
const result = await executionApiClient.chatCompletion('test-graph', messages);
|
|
200
|
-
expect(result).toBe('This is the result');
|
|
201
|
-
});
|
|
202
|
-
it('should include conversation ID when provided', async () => {
|
|
203
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
204
|
-
const conversationId = 'conv-123';
|
|
205
|
-
const mockStream = new ReadableStream();
|
|
206
|
-
mockFetch.mockResolvedValueOnce({
|
|
207
|
-
ok: true,
|
|
208
|
-
headers: {
|
|
209
|
-
get: (key) => (key === 'content-type' ? 'text/event-stream' : null),
|
|
210
|
-
},
|
|
211
|
-
body: mockStream,
|
|
212
|
-
});
|
|
213
|
-
await executionApiClient.chatCompletion('test-graph', messages, conversationId);
|
|
214
|
-
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
215
|
-
body: JSON.stringify({
|
|
216
|
-
model: 'gpt-4o-mini',
|
|
217
|
-
messages,
|
|
218
|
-
conversationId,
|
|
219
|
-
stream: true,
|
|
220
|
-
}),
|
|
221
|
-
}));
|
|
222
|
-
});
|
|
223
|
-
it('should throw error when chat request fails', async () => {
|
|
224
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
225
|
-
mockFetch.mockResolvedValueOnce({
|
|
226
|
-
ok: false,
|
|
227
|
-
statusText: 'Unauthorized',
|
|
228
|
-
text: async () => 'Invalid credentials',
|
|
229
|
-
});
|
|
230
|
-
await expect(executionApiClient.chatCompletion('test-graph', messages)).rejects.toThrow('Chat request failed: Unauthorized\nInvalid credentials');
|
|
231
|
-
});
|
|
232
|
-
it('should return empty string when no content in response', async () => {
|
|
233
|
-
const messages = [{ role: 'user', content: 'Hello' }];
|
|
234
|
-
mockFetch.mockResolvedValueOnce({
|
|
235
|
-
ok: true,
|
|
236
|
-
headers: {
|
|
237
|
-
get: (key) => (key === 'content-type' ? 'application/json' : null),
|
|
238
|
-
},
|
|
239
|
-
json: async () => ({}),
|
|
240
|
-
});
|
|
241
|
-
const result = await executionApiClient.chatCompletion('test-graph', messages);
|
|
242
|
-
expect(result).toBe('');
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
describe('checkTenantId', () => {
|
|
246
|
-
it('should throw error for all methods when tenant ID is not configured', async () => {
|
|
247
|
-
const { getTenantId } = await import('../config.js');
|
|
248
|
-
vi.mocked(getTenantId).mockResolvedValue(undefined);
|
|
249
|
-
const client = await ManagementApiClient.create();
|
|
250
|
-
const execClient = await ExecutionApiClient.create();
|
|
251
|
-
await expect(client.listGraphs()).rejects.toThrow('No tenant ID configured. Please run: inkeep init');
|
|
252
|
-
await expect(client.getGraph('test')).rejects.toThrow('No tenant ID configured. Please run: inkeep init');
|
|
253
|
-
await expect(client.pushGraph({ id: 'test' })).rejects.toThrow('No tenant ID configured. Please run: inkeep init');
|
|
254
|
-
await expect(execClient.chatCompletion('test', [])).rejects.toThrow();
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
2
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = dirname(__filename);
|
|
8
|
-
// Path to the compiled CLI
|
|
9
|
-
const cliPath = join(__dirname, '..', '..', 'dist', 'index.js');
|
|
10
|
-
// Helper function to execute CLI commands
|
|
11
|
-
function runCli(args) {
|
|
12
|
-
// Check if CLI binary exists before attempting to run
|
|
13
|
-
if (!existsSync(cliPath)) {
|
|
14
|
-
return {
|
|
15
|
-
stdout: '',
|
|
16
|
-
stderr: `CLI binary not found at ${cliPath}`,
|
|
17
|
-
exitCode: 1,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
try {
|
|
21
|
-
const stdout = execSync(`node ${cliPath} ${args.join(' ')}`, {
|
|
22
|
-
encoding: 'utf8',
|
|
23
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
24
|
-
timeout: 15000, // Increased to 15 second timeout for CI
|
|
25
|
-
killSignal: 'SIGTERM', // Use SIGTERM first for cleaner shutdown
|
|
26
|
-
windowsHide: true, // Hide windows on Windows
|
|
27
|
-
env: {
|
|
28
|
-
...process.env,
|
|
29
|
-
NODE_ENV: 'test',
|
|
30
|
-
CI: 'true', // Signal to CLI that it's running in CI
|
|
31
|
-
NODE_OPTIONS: '--max-old-space-size=256', // Limit memory usage
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
return { stdout, stderr: '', exitCode: 0 };
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
// Handle timeout specifically
|
|
38
|
-
if (error.code === 'TIMEOUT') {
|
|
39
|
-
return {
|
|
40
|
-
stdout: error.stdout || '',
|
|
41
|
-
stderr: 'Command timed out',
|
|
42
|
-
exitCode: 124, // Standard timeout exit code
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
stdout: error.stdout || '',
|
|
47
|
-
stderr: error.stderr || error.message || 'Unknown error',
|
|
48
|
-
exitCode: error.status || 1,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
describe('Inkeep CLI', () => {
|
|
53
|
-
beforeEach(() => {
|
|
54
|
-
// Mock console methods to capture output during tests
|
|
55
|
-
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
56
|
-
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
57
|
-
});
|
|
58
|
-
afterEach(async () => {
|
|
59
|
-
vi.restoreAllMocks();
|
|
60
|
-
// Small delay to allow processes to fully terminate
|
|
61
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
62
|
-
// Force garbage collection to clean up any hanging references
|
|
63
|
-
if (global.gc) {
|
|
64
|
-
global.gc();
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
describe('--version command', () => {
|
|
68
|
-
it('should display the version number', () => {
|
|
69
|
-
const result = runCli(['--version']);
|
|
70
|
-
expect(result.exitCode).toBe(0);
|
|
71
|
-
expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
|
|
72
|
-
});
|
|
73
|
-
it('should match the version in package.json', () => {
|
|
74
|
-
const packageJsonPath = join(__dirname, '..', '..', 'package.json');
|
|
75
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
76
|
-
const expectedVersion = packageJson.version;
|
|
77
|
-
const result = runCli(['--version']);
|
|
78
|
-
expect(result.stdout.trim()).toBe(expectedVersion);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
describe('push command', () => {
|
|
82
|
-
it('should require config path argument', () => {
|
|
83
|
-
const result = runCli(['push']);
|
|
84
|
-
expect(result.exitCode).toBe(1);
|
|
85
|
-
expect(result.stderr).toContain("error: missing required argument 'graph-path'");
|
|
86
|
-
});
|
|
87
|
-
it('should accept --api-url option', () => {
|
|
88
|
-
const result = runCli(['push', 'non-existent.js', '--api-url', 'http://example.com']);
|
|
89
|
-
// Will fail because file doesn't exist, but should accept the option
|
|
90
|
-
expect(result.exitCode).toBe(1);
|
|
91
|
-
// Should fail for file not found, not for invalid option
|
|
92
|
-
expect(result.stderr).not.toContain('unknown option');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
describe('chat command', () => {
|
|
96
|
-
it('should accept optional graph-id argument', () => {
|
|
97
|
-
const result = runCli(['chat', '--help']);
|
|
98
|
-
expect(result.exitCode).toBe(0);
|
|
99
|
-
expect(result.stdout).toContain('[graph-id]');
|
|
100
|
-
expect(result.stdout).toContain('Start an interactive chat session');
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
describe('list-graphs command', () => {
|
|
104
|
-
it('should accept --url option', () => {
|
|
105
|
-
const result = runCli(['list-graphs', '--help']);
|
|
106
|
-
expect(result.exitCode).toBe(0);
|
|
107
|
-
expect(result.stdout).toContain('List all available graphs');
|
|
108
|
-
expect(result.stdout).toContain('--api-url');
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
describe('--help command', () => {
|
|
112
|
-
it('should display help information', () => {
|
|
113
|
-
const result = runCli(['--help']);
|
|
114
|
-
expect(result.exitCode).toBe(0);
|
|
115
|
-
expect(result.stdout).toContain('CLI tool for Inkeep Agent Framework');
|
|
116
|
-
expect(result.stdout).toContain('Commands:');
|
|
117
|
-
expect(result.stdout).toContain('push');
|
|
118
|
-
expect(result.stdout).toContain('chat');
|
|
119
|
-
expect(result.stdout).toContain('tenant');
|
|
120
|
-
expect(result.stdout).toContain('list-graphs');
|
|
121
|
-
});
|
|
122
|
-
it('should display help for push command', () => {
|
|
123
|
-
const result = runCli(['push', '--help']);
|
|
124
|
-
expect(result.exitCode).toBe(0);
|
|
125
|
-
expect(result.stdout).toContain('Push a graph configuration');
|
|
126
|
-
expect(result.stdout).toContain('--api-url');
|
|
127
|
-
});
|
|
128
|
-
it('should display help for chat command', () => {
|
|
129
|
-
const result = runCli(['chat', '--help']);
|
|
130
|
-
expect(result.exitCode).toBe(0);
|
|
131
|
-
expect(result.stdout).toContain('Start an interactive chat session');
|
|
132
|
-
expect(result.stdout).toContain('[graph-id]');
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
describe('invalid commands', () => {
|
|
136
|
-
it('should show error for unknown command', () => {
|
|
137
|
-
const result = runCli(['unknown-command']);
|
|
138
|
-
expect(result.exitCode).toBe(1);
|
|
139
|
-
expect(result.stderr).toContain('error: unknown command');
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
describe('CLI structure', () => {
|
|
143
|
-
it('should have correct CLI name', () => {
|
|
144
|
-
const result = runCli(['--help']);
|
|
145
|
-
expect(result.stdout).toContain('inkeep');
|
|
146
|
-
});
|
|
147
|
-
it('should be executable', () => {
|
|
148
|
-
// This test ensures the CLI can be executed without throwing
|
|
149
|
-
const result = runCli(['--version']);
|
|
150
|
-
expect(result.exitCode).toBe(0);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { configGetCommand, configListCommand, configSetCommand } from '../../commands/config.js';
|
|
4
|
-
// Mock fs functions
|
|
5
|
-
vi.mock('node:fs', async () => {
|
|
6
|
-
const actual = await vi.importActual('node:fs');
|
|
7
|
-
return {
|
|
8
|
-
...actual,
|
|
9
|
-
existsSync: vi.fn(),
|
|
10
|
-
readFileSync: vi.fn(),
|
|
11
|
-
writeFileSync: vi.fn(),
|
|
12
|
-
};
|
|
13
|
-
});
|
|
14
|
-
describe('Config Command', () => {
|
|
15
|
-
let consoleLogSpy;
|
|
16
|
-
let consoleErrorSpy;
|
|
17
|
-
let processExitSpy;
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
20
|
-
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
21
|
-
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
22
|
-
throw new Error('process.exit called');
|
|
23
|
-
});
|
|
24
|
-
vi.clearAllMocks();
|
|
25
|
-
});
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
consoleLogSpy.mockRestore();
|
|
28
|
-
consoleErrorSpy.mockRestore();
|
|
29
|
-
processExitSpy.mockRestore();
|
|
30
|
-
});
|
|
31
|
-
describe('configGetCommand', () => {
|
|
32
|
-
it('should display all config when no key is provided', async () => {
|
|
33
|
-
const mockConfig = `import { defineConfig } from '@inkeep/agents-cli';
|
|
34
|
-
|
|
35
|
-
export default defineConfig({
|
|
36
|
-
tenantId: 'test-tenant-123',
|
|
37
|
-
apiUrl: 'https://api.example.com',
|
|
38
|
-
});`;
|
|
39
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
40
|
-
vi.mocked(readFileSync).mockReturnValue(mockConfig);
|
|
41
|
-
await configGetCommand();
|
|
42
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Current configuration'));
|
|
43
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Tenant ID'), 'test-tenant-123');
|
|
44
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('API URL'), 'https://api.example.com');
|
|
45
|
-
});
|
|
46
|
-
it('should display specific config value when key is provided', async () => {
|
|
47
|
-
const mockConfig = `import { defineConfig } from '@inkeep/agents-cli';
|
|
48
|
-
|
|
49
|
-
export default defineConfig({
|
|
50
|
-
tenantId: 'test-tenant-123',
|
|
51
|
-
apiUrl: 'https://api.example.com',
|
|
52
|
-
});`;
|
|
53
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
54
|
-
vi.mocked(readFileSync).mockReturnValue(mockConfig);
|
|
55
|
-
await configGetCommand('tenantId');
|
|
56
|
-
expect(consoleLogSpy).toHaveBeenCalledWith('test-tenant-123');
|
|
57
|
-
});
|
|
58
|
-
it('should error when config file does not exist', async () => {
|
|
59
|
-
vi.mocked(existsSync).mockReturnValue(false);
|
|
60
|
-
await expect(configGetCommand()).rejects.toThrow('process.exit called');
|
|
61
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('No configuration file found'));
|
|
62
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
63
|
-
});
|
|
64
|
-
it('should error for unknown configuration key', async () => {
|
|
65
|
-
const mockConfig = `import { defineConfig } from '@inkeep/agents-cli';
|
|
66
|
-
|
|
67
|
-
export default defineConfig({
|
|
68
|
-
tenantId: 'test-tenant-123',
|
|
69
|
-
});`;
|
|
70
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
71
|
-
vi.mocked(readFileSync).mockReturnValue(mockConfig);
|
|
72
|
-
await expect(configGetCommand('unknownKey')).rejects.toThrow('process.exit called');
|
|
73
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown configuration key: unknownKey'));
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
describe('configSetCommand', () => {
|
|
77
|
-
it('should update tenantId in existing config file', async () => {
|
|
78
|
-
const originalConfig = `import { defineConfig } from '@inkeep/agents-cli';
|
|
79
|
-
|
|
80
|
-
export default defineConfig({
|
|
81
|
-
tenantId: 'old-tenant',
|
|
82
|
-
apiUrl: 'http://localhost:3002',
|
|
83
|
-
});`;
|
|
84
|
-
const expectedConfig = `import { defineConfig } from '@inkeep/agents-cli';
|
|
85
|
-
|
|
86
|
-
export default defineConfig({
|
|
87
|
-
tenantId: 'new-tenant-456',
|
|
88
|
-
apiUrl: 'http://localhost:3002',
|
|
89
|
-
});`;
|
|
90
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
91
|
-
vi.mocked(readFileSync).mockReturnValue(originalConfig);
|
|
92
|
-
await configSetCommand('tenantId', 'new-tenant-456');
|
|
93
|
-
expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('inkeep.config.ts'), expectedConfig);
|
|
94
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.any(String), // The checkmark
|
|
95
|
-
expect.stringContaining('Updated tenantId'), expect.stringContaining('new-tenant-456'));
|
|
96
|
-
});
|
|
97
|
-
it('should update apiUrl in existing config file', async () => {
|
|
98
|
-
const originalConfig = `import { defineConfig } from '@inkeep/agents-cli';
|
|
99
|
-
|
|
100
|
-
export default defineConfig({
|
|
101
|
-
tenantId: 'test-tenant',
|
|
102
|
-
apiUrl: 'http://localhost:3002',
|
|
103
|
-
});`;
|
|
104
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
105
|
-
vi.mocked(readFileSync).mockReturnValue(originalConfig);
|
|
106
|
-
await configSetCommand('apiUrl', 'https://api.example.com');
|
|
107
|
-
expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('inkeep.config.ts'), expect.stringContaining("apiUrl: 'https://api.example.com'"));
|
|
108
|
-
});
|
|
109
|
-
it('should create new config file if it does not exist', async () => {
|
|
110
|
-
vi.mocked(existsSync).mockReturnValue(false);
|
|
111
|
-
await configSetCommand('tenantId', 'new-tenant');
|
|
112
|
-
expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('inkeep.config.ts'), expect.stringContaining("tenantId: 'new-tenant'"));
|
|
113
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.any(String), // The checkmark
|
|
114
|
-
expect.stringContaining('Created config file and set tenantId to'), expect.any(String) // The value
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
it('should validate apiUrl format', async () => {
|
|
118
|
-
vi.mocked(existsSync).mockReturnValue(false);
|
|
119
|
-
await expect(configSetCommand('apiUrl', 'not-a-url')).rejects.toThrow('process.exit called');
|
|
120
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid URL format'));
|
|
121
|
-
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
122
|
-
});
|
|
123
|
-
it('should reject invalid configuration keys', async () => {
|
|
124
|
-
await expect(configSetCommand('invalidKey', 'value')).rejects.toThrow('process.exit called');
|
|
125
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid configuration key: invalidKey'));
|
|
126
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Available keys: tenantId, apiUrl'));
|
|
127
|
-
});
|
|
128
|
-
it('should add missing tenantId to config', async () => {
|
|
129
|
-
const originalConfig = `import { defineConfig } from '@inkeep/agents-cli';
|
|
130
|
-
|
|
131
|
-
export default defineConfig({
|
|
132
|
-
apiUrl: 'http://localhost:3002',
|
|
133
|
-
});`;
|
|
134
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
135
|
-
vi.mocked(readFileSync).mockReturnValue(originalConfig);
|
|
136
|
-
await configSetCommand('tenantId', 'new-tenant');
|
|
137
|
-
expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('inkeep.config.ts'), expect.stringContaining("tenantId: 'new-tenant'"));
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
describe('configListCommand', () => {
|
|
141
|
-
it('should call configGetCommand without a key', async () => {
|
|
142
|
-
const mockConfig = `import { defineConfig } from '@inkeep/agents-cli';
|
|
143
|
-
|
|
144
|
-
export default defineConfig({
|
|
145
|
-
tenantId: 'test-tenant',
|
|
146
|
-
apiUrl: 'http://localhost:3002',
|
|
147
|
-
});`;
|
|
148
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
149
|
-
vi.mocked(readFileSync).mockReturnValue(mockConfig);
|
|
150
|
-
await configListCommand();
|
|
151
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Current configuration'));
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|