@vfarcic/dot-ai 0.5.0 → 0.5.1

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 (145) hide show
  1. package/.claude/commands/context-load.md +11 -0
  2. package/.claude/commands/context-save.md +16 -0
  3. package/.claude/commands/prd-done.md +115 -0
  4. package/.claude/commands/prd-get.md +25 -0
  5. package/.claude/commands/prd-start.md +87 -0
  6. package/.claude/commands/task-done.md +77 -0
  7. package/.claude/commands/tests-reminder.md +32 -0
  8. package/.claude/settings.local.json +20 -0
  9. package/.eslintrc.json +25 -0
  10. package/.github/workflows/ci.yml +170 -0
  11. package/.prettierrc.json +10 -0
  12. package/.teller.yml +8 -0
  13. package/CLAUDE.md +162 -0
  14. package/assets/images/logo.png +0 -0
  15. package/bin/dot-ai.ts +47 -0
  16. package/destroy.sh +45 -0
  17. package/devbox.json +13 -0
  18. package/devbox.lock +225 -0
  19. package/docs/API.md +449 -0
  20. package/docs/CONTEXT.md +49 -0
  21. package/docs/DEVELOPMENT.md +203 -0
  22. package/docs/NEXT_STEPS.md +97 -0
  23. package/docs/STAGE_BASED_API.md +97 -0
  24. package/docs/cli-guide.md +798 -0
  25. package/docs/design.md +750 -0
  26. package/docs/discovery-engine.md +515 -0
  27. package/docs/error-handling.md +429 -0
  28. package/docs/function-registration.md +157 -0
  29. package/docs/mcp-guide.md +416 -0
  30. package/package.json +2 -121
  31. package/renovate.json +51 -0
  32. package/setup.sh +111 -0
  33. package/{dist/cli.js → src/cli.ts} +26 -19
  34. package/src/core/claude.ts +280 -0
  35. package/src/core/deploy-operation.ts +127 -0
  36. package/src/core/discovery.ts +900 -0
  37. package/src/core/error-handling.ts +562 -0
  38. package/src/core/index.ts +143 -0
  39. package/src/core/kubernetes-utils.ts +218 -0
  40. package/src/core/memory.ts +148 -0
  41. package/src/core/schema.ts +830 -0
  42. package/src/core/session-utils.ts +97 -0
  43. package/src/core/workflow.ts +234 -0
  44. package/src/index.ts +18 -0
  45. package/src/interfaces/cli.ts +872 -0
  46. package/src/interfaces/mcp.ts +183 -0
  47. package/src/mcp/server.ts +131 -0
  48. package/src/tools/answer-question.ts +807 -0
  49. package/src/tools/choose-solution.ts +169 -0
  50. package/src/tools/deploy-manifests.ts +94 -0
  51. package/src/tools/generate-manifests.ts +502 -0
  52. package/src/tools/index.ts +41 -0
  53. package/src/tools/recommend.ts +370 -0
  54. package/tests/__mocks__/@kubernetes/client-node.ts +106 -0
  55. package/tests/build-system.test.ts +345 -0
  56. package/tests/configuration.test.ts +226 -0
  57. package/tests/core/deploy-operation.test.ts +38 -0
  58. package/tests/core/discovery.test.ts +1648 -0
  59. package/tests/core/error-handling.test.ts +632 -0
  60. package/tests/core/schema.test.ts +1658 -0
  61. package/tests/core/session-utils.test.ts +245 -0
  62. package/tests/core.test.ts +439 -0
  63. package/tests/fixtures/configmap-no-labels.yaml +8 -0
  64. package/tests/fixtures/crossplane-app-configuration.yaml +6 -0
  65. package/tests/fixtures/crossplane-providers.yaml +45 -0
  66. package/tests/fixtures/crossplane-rbac.yaml +48 -0
  67. package/tests/fixtures/invalid-configmap.yaml +8 -0
  68. package/tests/fixtures/invalid-deployment.yaml +17 -0
  69. package/tests/fixtures/test-deployment.yaml +28 -0
  70. package/tests/fixtures/valid-configmap.yaml +15 -0
  71. package/tests/infrastructure.test.ts +426 -0
  72. package/tests/interfaces/cli.test.ts +1036 -0
  73. package/tests/interfaces/mcp.test.ts +139 -0
  74. package/tests/kubernetes-utils.test.ts +200 -0
  75. package/tests/mcp/server.test.ts +126 -0
  76. package/tests/setup.ts +31 -0
  77. package/tests/tools/answer-question.test.ts +367 -0
  78. package/tests/tools/choose-solution.test.ts +481 -0
  79. package/tests/tools/deploy-manifests.test.ts +185 -0
  80. package/tests/tools/generate-manifests.test.ts +441 -0
  81. package/tests/tools/index.test.ts +111 -0
  82. package/tests/tools/recommend.test.ts +180 -0
  83. package/tsconfig.json +34 -0
  84. package/dist/cli.d.ts +0 -3
  85. package/dist/cli.d.ts.map +0 -1
  86. package/dist/core/claude.d.ts +0 -42
  87. package/dist/core/claude.d.ts.map +0 -1
  88. package/dist/core/claude.js +0 -229
  89. package/dist/core/deploy-operation.d.ts +0 -38
  90. package/dist/core/deploy-operation.d.ts.map +0 -1
  91. package/dist/core/deploy-operation.js +0 -101
  92. package/dist/core/discovery.d.ts +0 -162
  93. package/dist/core/discovery.d.ts.map +0 -1
  94. package/dist/core/discovery.js +0 -758
  95. package/dist/core/error-handling.d.ts +0 -167
  96. package/dist/core/error-handling.d.ts.map +0 -1
  97. package/dist/core/error-handling.js +0 -399
  98. package/dist/core/index.d.ts +0 -42
  99. package/dist/core/index.d.ts.map +0 -1
  100. package/dist/core/index.js +0 -123
  101. package/dist/core/kubernetes-utils.d.ts +0 -38
  102. package/dist/core/kubernetes-utils.d.ts.map +0 -1
  103. package/dist/core/kubernetes-utils.js +0 -177
  104. package/dist/core/memory.d.ts +0 -45
  105. package/dist/core/memory.d.ts.map +0 -1
  106. package/dist/core/memory.js +0 -113
  107. package/dist/core/schema.d.ts +0 -187
  108. package/dist/core/schema.d.ts.map +0 -1
  109. package/dist/core/schema.js +0 -655
  110. package/dist/core/session-utils.d.ts +0 -29
  111. package/dist/core/session-utils.d.ts.map +0 -1
  112. package/dist/core/session-utils.js +0 -121
  113. package/dist/core/workflow.d.ts +0 -70
  114. package/dist/core/workflow.d.ts.map +0 -1
  115. package/dist/core/workflow.js +0 -161
  116. package/dist/index.d.ts +0 -15
  117. package/dist/index.d.ts.map +0 -1
  118. package/dist/index.js +0 -32
  119. package/dist/interfaces/cli.d.ts +0 -74
  120. package/dist/interfaces/cli.d.ts.map +0 -1
  121. package/dist/interfaces/cli.js +0 -769
  122. package/dist/interfaces/mcp.d.ts +0 -30
  123. package/dist/interfaces/mcp.d.ts.map +0 -1
  124. package/dist/interfaces/mcp.js +0 -105
  125. package/dist/mcp/server.d.ts +0 -9
  126. package/dist/mcp/server.d.ts.map +0 -1
  127. package/dist/mcp/server.js +0 -151
  128. package/dist/tools/answer-question.d.ts +0 -27
  129. package/dist/tools/answer-question.d.ts.map +0 -1
  130. package/dist/tools/answer-question.js +0 -696
  131. package/dist/tools/choose-solution.d.ts +0 -23
  132. package/dist/tools/choose-solution.d.ts.map +0 -1
  133. package/dist/tools/choose-solution.js +0 -171
  134. package/dist/tools/deploy-manifests.d.ts +0 -25
  135. package/dist/tools/deploy-manifests.d.ts.map +0 -1
  136. package/dist/tools/deploy-manifests.js +0 -74
  137. package/dist/tools/generate-manifests.d.ts +0 -23
  138. package/dist/tools/generate-manifests.d.ts.map +0 -1
  139. package/dist/tools/generate-manifests.js +0 -424
  140. package/dist/tools/index.d.ts +0 -11
  141. package/dist/tools/index.d.ts.map +0 -1
  142. package/dist/tools/index.js +0 -34
  143. package/dist/tools/recommend.d.ts +0 -23
  144. package/dist/tools/recommend.d.ts.map +0 -1
  145. package/dist/tools/recommend.js +0 -332
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Answer Question Tool Tests - Stage-Based Implementation
3
+ */
4
+
5
+ import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import {
9
+ ANSWERQUESTION_TOOL_NAME,
10
+ ANSWERQUESTION_TOOL_DESCRIPTION,
11
+ ANSWERQUESTION_TOOL_INPUT_SCHEMA,
12
+ handleAnswerQuestionTool
13
+ } from '../../src/tools/answer-question';
14
+
15
+ // Mock fs module
16
+ jest.mock('fs');
17
+ const mockFs = fs as jest.Mocked<typeof fs>;
18
+
19
+ // Test constants
20
+ const TEST_SESSION_DIR = '/test/session';
21
+ const TEST_SOLUTION_ID = 'sol_2025-01-01T123456_abcdef';
22
+ const TEST_SOLUTION_PATH = path.join(TEST_SESSION_DIR, `${TEST_SOLUTION_ID}.json`);
23
+
24
+ // Test data
25
+ const TEST_SOLUTION = {
26
+ solutionId: TEST_SOLUTION_ID,
27
+ intent: 'deploy a web application',
28
+ type: 'single',
29
+ questions: {
30
+ required: [
31
+ {
32
+ id: 'name',
33
+ question: 'What name would you like to give to your application?',
34
+ type: 'text',
35
+ validation: {
36
+ required: true,
37
+ pattern: '^[a-z0-9-]+$',
38
+ message: 'Name must consist of lowercase letters, numbers, and hyphens'
39
+ }
40
+ },
41
+ {
42
+ id: 'port',
43
+ question: 'What port does your application listen on?',
44
+ type: 'number',
45
+ validation: {
46
+ min: 1,
47
+ max: 65535
48
+ }
49
+ }
50
+ ],
51
+ basic: [
52
+ {
53
+ id: 'replicas',
54
+ question: 'How many replicas do you need?',
55
+ type: 'number',
56
+ default: 3,
57
+ validation: {
58
+ min: 1
59
+ }
60
+ }
61
+ ],
62
+ advanced: [
63
+ {
64
+ id: 'scaling-enabled',
65
+ question: 'Enable auto-scaling?',
66
+ type: 'boolean',
67
+ default: false
68
+ }
69
+ ],
70
+ open: {
71
+ question: 'Any additional requirements?',
72
+ placeholder: 'e.g., specific security requirements...'
73
+ }
74
+ }
75
+ };
76
+
77
+ const createMockToolContext = () => ({
78
+ requestId: 'test-request-123',
79
+ logger: {
80
+ debug: jest.fn(),
81
+ info: jest.fn(),
82
+ warn: jest.fn(),
83
+ error: jest.fn(),
84
+ fatal: jest.fn()
85
+ },
86
+ dotAI: {} as any // Mock DotAI object
87
+ });
88
+
89
+ // Helper to create solution with answers
90
+ const createSolutionWithAnswers = (answers: Record<string, any> = {}) => {
91
+ const solution = JSON.parse(JSON.stringify(TEST_SOLUTION));
92
+
93
+ // Apply answers to questions
94
+ const allQuestions = [
95
+ ...solution.questions.required,
96
+ ...solution.questions.basic,
97
+ ...solution.questions.advanced
98
+ ];
99
+
100
+ for (const [questionId, answer] of Object.entries(answers)) {
101
+ const question = allQuestions.find(q => q.id === questionId);
102
+ if (question) {
103
+ question.answer = answer;
104
+ }
105
+ }
106
+
107
+ return solution;
108
+ };
109
+
110
+ describe('Answer Question Tool Metadata', () => {
111
+ test('should have correct tool metadata', () => {
112
+ expect(ANSWERQUESTION_TOOL_NAME).toBe('answerQuestion');
113
+ expect(ANSWERQUESTION_TOOL_DESCRIPTION).toContain('Process user answers');
114
+ expect(ANSWERQUESTION_TOOL_INPUT_SCHEMA.solutionId).toBeDefined();
115
+ expect(ANSWERQUESTION_TOOL_INPUT_SCHEMA.stage).toBeDefined();
116
+ expect(ANSWERQUESTION_TOOL_INPUT_SCHEMA.answers).toBeDefined();
117
+ });
118
+ });
119
+
120
+ describe('Answer Question Tool Handler - Stage-Based Implementation', () => {
121
+ beforeEach(() => {
122
+ jest.clearAllMocks();
123
+
124
+ // Default fs mocks - set up for successful operation by default
125
+ mockFs.existsSync.mockImplementation((filePath) => {
126
+ if (typeof filePath === 'string') {
127
+ if (filePath === TEST_SESSION_DIR) return true;
128
+ if (filePath === TEST_SOLUTION_PATH) return true;
129
+ return false;
130
+ }
131
+ return false;
132
+ });
133
+ mockFs.statSync.mockReturnValue({ isDirectory: () => true } as any);
134
+ mockFs.readdirSync.mockReturnValue([]);
135
+ mockFs.writeFileSync.mockImplementation(() => {});
136
+ mockFs.unlinkSync.mockImplementation(() => {});
137
+ mockFs.renameSync.mockImplementation(() => {});
138
+
139
+ // Default solution file mock
140
+ mockFs.readFileSync.mockReturnValue(JSON.stringify(TEST_SOLUTION));
141
+
142
+ // Reset environment
143
+ process.env.DOT_AI_SESSION_DIR = TEST_SESSION_DIR;
144
+ });
145
+
146
+ afterEach(() => {
147
+ jest.resetAllMocks();
148
+ });
149
+
150
+ // Stage-Based Workflow Tests - Core validation for new implementation
151
+ describe('Stage-Based Workflow Validation', () => {
152
+ test('should handle required stage correctly', async () => {
153
+ const context = createMockToolContext();
154
+
155
+ const result = await handleAnswerQuestionTool({
156
+ solutionId: TEST_SOLUTION_ID,
157
+ stage: 'required',
158
+ answers: {
159
+ name: 'my-app',
160
+ port: 8080 // Both required questions need to be answered
161
+ }
162
+ }, context.dotAI, context.logger, context.requestId);
163
+
164
+ const response = JSON.parse(result.content[0].text);
165
+ expect(response.status).toBe('stage_questions');
166
+ expect(response.currentStage).toBe('basic'); // Should progress to basic stage after completing required
167
+ expect(response.solutionId).toBe(TEST_SOLUTION_ID);
168
+ });
169
+
170
+ test('should reject invalid stage transition', async () => {
171
+ const context = createMockToolContext();
172
+
173
+ // Try to jump directly to advanced stage without completing required
174
+ const result = await handleAnswerQuestionTool({
175
+ solutionId: TEST_SOLUTION_ID,
176
+ stage: 'advanced',
177
+ answers: {
178
+ 'scaling-enabled': true
179
+ }
180
+ }, context.dotAI, context.logger, context.requestId);
181
+
182
+ const response = JSON.parse(result.content[0].text);
183
+ expect(response.status).toBe('stage_error');
184
+ expect(response.error).toBe('invalid_transition');
185
+ expect(response.expected).toBe('required');
186
+ expect(response.received).toBe('advanced');
187
+ });
188
+
189
+ test('should handle open stage completion', async () => {
190
+ const context = createMockToolContext();
191
+
192
+ // Mock solution with all previous stages complete
193
+ const completeSolution = createSolutionWithAnswers({
194
+ name: 'my-app',
195
+ port: 8080,
196
+ replicas: 3,
197
+ 'scaling-enabled': false
198
+ });
199
+ mockFs.readFileSync.mockReturnValue(JSON.stringify(completeSolution));
200
+
201
+ const result = await handleAnswerQuestionTool({
202
+ solutionId: TEST_SOLUTION_ID,
203
+ stage: 'open',
204
+ answers: {
205
+ open: 'N/A'
206
+ }
207
+ }, context.dotAI, context.logger, context.requestId);
208
+
209
+ const response = JSON.parse(result.content[0].text);
210
+ expect(response.status).toBe('ready_for_manifest_generation');
211
+ expect(response.solutionData.userAnswers).toMatchObject({
212
+ name: 'my-app',
213
+ port: 8080,
214
+ replicas: 3,
215
+ 'scaling-enabled': false,
216
+ open: 'N/A'
217
+ });
218
+ });
219
+
220
+ test('should validate answers against current stage questions only', async () => {
221
+ const context = createMockToolContext();
222
+
223
+ const result = await handleAnswerQuestionTool({
224
+ solutionId: TEST_SOLUTION_ID,
225
+ stage: 'required',
226
+ answers: {
227
+ replicas: 3 // Wrong stage - replicas is basic, not required
228
+ }
229
+ }, context.dotAI, context.logger, context.requestId);
230
+
231
+ const response = JSON.parse(result.content[0].text);
232
+ expect(response.status).toBe('stage_error');
233
+ expect(response.error).toBe('validation_failed');
234
+ expect(response.validationErrors[0]).toContain("Unknown question ID 'replicas' for stage 'required'");
235
+ });
236
+
237
+ test('should include nextAction: answerQuestion for intermediate stages', async () => {
238
+ const context = createMockToolContext();
239
+
240
+ // Test required stage with some answers
241
+ const result = await handleAnswerQuestionTool({
242
+ solutionId: TEST_SOLUTION_ID,
243
+ stage: 'required',
244
+ answers: {
245
+ name: 'my-app',
246
+ port: 8080
247
+ }
248
+ }, context.dotAI, context.logger, context.requestId);
249
+
250
+ const response = JSON.parse(result.content[0].text);
251
+ expect(response.status).toBe('stage_questions');
252
+ expect(response.currentStage).toBe('basic');
253
+ expect(response.nextAction).toBe('answerQuestion');
254
+ expect(response.nextAction).not.toBe('generateManifests');
255
+ });
256
+
257
+ test('should only mention generateManifests when workflow complete', async () => {
258
+ const context = createMockToolContext();
259
+
260
+ // Mock solution with all previous stages complete
261
+ const completeSolution = createSolutionWithAnswers({
262
+ name: 'my-app',
263
+ port: 8080,
264
+ replicas: 3,
265
+ 'scaling-enabled': false
266
+ });
267
+ mockFs.readFileSync.mockReturnValue(JSON.stringify(completeSolution));
268
+
269
+ const result = await handleAnswerQuestionTool({
270
+ solutionId: TEST_SOLUTION_ID,
271
+ stage: 'open',
272
+ answers: {
273
+ open: 'N/A'
274
+ }
275
+ }, context.dotAI, context.logger, context.requestId);
276
+
277
+ const response = JSON.parse(result.content[0].text);
278
+ expect(response.status).toBe('ready_for_manifest_generation');
279
+ expect(response.nextAction).toBe('generateManifests');
280
+ });
281
+
282
+ test('should advance stage when empty answers provided for basic stage', async () => {
283
+ const context = createMockToolContext();
284
+
285
+ // Mock solution with required questions answered but basic questions unanswered
286
+ const solutionWithRequired = createSolutionWithAnswers({
287
+ name: 'my-app',
288
+ port: 8080
289
+ });
290
+ mockFs.readFileSync.mockReturnValue(JSON.stringify(solutionWithRequired));
291
+
292
+ const result = await handleAnswerQuestionTool({
293
+ solutionId: TEST_SOLUTION_ID,
294
+ stage: 'basic',
295
+ answers: {} // Empty answers = skip stage
296
+ }, context.dotAI, context.logger, context.requestId);
297
+
298
+ const response = JSON.parse(result.content[0].text);
299
+ expect(response.status).toBe('stage_questions');
300
+ expect(response.currentStage).toBe('advanced'); // Should advance to next stage
301
+ expect(response.nextAction).toBe('answerQuestion');
302
+ });
303
+
304
+ test('should advance stage when empty answers provided for advanced stage', async () => {
305
+ const context = createMockToolContext();
306
+
307
+ // Mock solution with required and basic questions answered but advanced questions unanswered
308
+ const solutionWithBasic = createSolutionWithAnswers({
309
+ name: 'my-app',
310
+ port: 8080,
311
+ replicas: 3
312
+ });
313
+ mockFs.readFileSync.mockReturnValue(JSON.stringify(solutionWithBasic));
314
+
315
+ const result = await handleAnswerQuestionTool({
316
+ solutionId: TEST_SOLUTION_ID,
317
+ stage: 'advanced',
318
+ answers: {} // Empty answers = skip stage
319
+ }, context.dotAI, context.logger, context.requestId);
320
+
321
+ const response = JSON.parse(result.content[0].text);
322
+ expect(response.status).toBe('stage_questions');
323
+ expect(response.currentStage).toBe('open'); // Should advance to open stage
324
+ expect(response.nextAction).toBe('answerQuestion');
325
+ });
326
+
327
+ test('should mark skipped questions with null answers', async () => {
328
+ const context = createMockToolContext();
329
+
330
+ // Mock solution with required questions answered
331
+ const solutionWithRequired = createSolutionWithAnswers({
332
+ name: 'my-app',
333
+ port: 8080
334
+ });
335
+ mockFs.readFileSync.mockReturnValue(JSON.stringify(solutionWithRequired));
336
+
337
+ // Capture what gets written to the file
338
+ let savedSolution: any;
339
+ mockFs.writeFileSync.mockImplementation((filePath, data) => {
340
+ if (typeof filePath === 'string' && (filePath === TEST_SOLUTION_PATH || filePath.includes('.tmp'))) {
341
+ if (typeof data === 'string' && data.includes('"solutionId"')) {
342
+ savedSolution = JSON.parse(data);
343
+ }
344
+ }
345
+ });
346
+
347
+ await handleAnswerQuestionTool({
348
+ solutionId: TEST_SOLUTION_ID,
349
+ stage: 'basic',
350
+ answers: {} // Empty answers = skip stage
351
+ }, context.dotAI, context.logger, context.requestId);
352
+
353
+ // Check that writeFileSync was called for the solution file
354
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
355
+ expect.stringContaining(TEST_SOLUTION_ID),
356
+ expect.any(String),
357
+ expect.any(String)
358
+ );
359
+
360
+ // Verify all basic questions have null answers (marked as skipped)
361
+ expect(savedSolution).toBeDefined();
362
+ for (const question of savedSolution.questions.basic) {
363
+ expect(question.answer).toBe(null);
364
+ }
365
+ });
366
+ });
367
+ });