@deimoscloud/coreai 0.1.0

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 (216) hide show
  1. package/.prettierrc +9 -0
  2. package/AGENT_SPEC.md +347 -0
  3. package/ARCHITECTURE.md +547 -0
  4. package/DRAFT_PRD.md +1440 -0
  5. package/IMPLEMENTATION_PLAN.md +256 -0
  6. package/PRODUCT.md +473 -0
  7. package/README.md +303 -0
  8. package/WORKFLOWS.md +295 -0
  9. package/agents/_templates/ic-engineer.md +185 -0
  10. package/agents/_templates/reviewer.md +182 -0
  11. package/agents/backend-engineer.yaml +72 -0
  12. package/agents/devops-engineer.yaml +72 -0
  13. package/agents/engineering-manager.yaml +70 -0
  14. package/agents/examples/android-engineer.md +302 -0
  15. package/agents/examples/backend-engineer.md +320 -0
  16. package/agents/examples/devops-engineer.md +742 -0
  17. package/agents/examples/engineering-manager.md +469 -0
  18. package/agents/examples/frontend-engineer.md +58 -0
  19. package/agents/examples/product-manager.md +315 -0
  20. package/agents/examples/qa-engineer.md +371 -0
  21. package/agents/examples/security-engineer.md +525 -0
  22. package/agents/examples/solutions-architect.md +351 -0
  23. package/agents/examples/wearos-engineer.md +359 -0
  24. package/agents/frontend-engineer.yaml +72 -0
  25. package/commands/core/check-inbox.md +34 -0
  26. package/commands/core/delegate.md +30 -0
  27. package/commands/core/git-commit.md +144 -0
  28. package/commands/core/pr-create.md +193 -0
  29. package/commands/core/review.md +56 -0
  30. package/commands/core/sprint-status.md +65 -0
  31. package/commands/optional/docs-update.md +200 -0
  32. package/commands/optional/jira-create.md +200 -0
  33. package/commands/optional/jira-transition.md +184 -0
  34. package/commands/optional/worktree-cleanup.md +167 -0
  35. package/commands/optional/worktree-setup.md +110 -0
  36. package/dist/cli/index.js +4037 -0
  37. package/dist/cli/index.js.map +1 -0
  38. package/dist/index.d.ts +2978 -0
  39. package/dist/index.js +3867 -0
  40. package/dist/index.js.map +1 -0
  41. package/eslint.config.js +29 -0
  42. package/jest.config.js +22 -0
  43. package/knowledge-library/README.md +118 -0
  44. package/knowledge-library/android-engineer/context/current.txt +42 -0
  45. package/knowledge-library/android-engineer/control/decisions.txt +9 -0
  46. package/knowledge-library/android-engineer/control/dependencies.txt +19 -0
  47. package/knowledge-library/android-engineer/control/objectives.txt +26 -0
  48. package/knowledge-library/android-engineer/history/.gitkeep +0 -0
  49. package/knowledge-library/android-engineer/inbox/processed/.gitkeep +0 -0
  50. package/knowledge-library/android-engineer/outbox/.gitkeep +0 -0
  51. package/knowledge-library/android-engineer/tech/.gitkeep +0 -0
  52. package/knowledge-library/architecture.txt +61 -0
  53. package/knowledge-library/backend-engineer/context/current.txt +42 -0
  54. package/knowledge-library/backend-engineer/control/decisions.txt +9 -0
  55. package/knowledge-library/backend-engineer/control/dependencies.txt +19 -0
  56. package/knowledge-library/backend-engineer/control/objectives.txt +26 -0
  57. package/knowledge-library/backend-engineer/history/.gitkeep +0 -0
  58. package/knowledge-library/backend-engineer/inbox/processed/.gitkeep +0 -0
  59. package/knowledge-library/backend-engineer/outbox/.gitkeep +0 -0
  60. package/knowledge-library/backend-engineer/tech/.gitkeep +0 -0
  61. package/knowledge-library/context.txt +52 -0
  62. package/knowledge-library/devops-engineer/context/current.txt +42 -0
  63. package/knowledge-library/devops-engineer/control/decisions.txt +9 -0
  64. package/knowledge-library/devops-engineer/control/dependencies.txt +19 -0
  65. package/knowledge-library/devops-engineer/control/objectives.txt +26 -0
  66. package/knowledge-library/devops-engineer/history/.gitkeep +0 -0
  67. package/knowledge-library/devops-engineer/inbox/processed/.gitkeep +0 -0
  68. package/knowledge-library/devops-engineer/outbox/.gitkeep +0 -0
  69. package/knowledge-library/devops-engineer/tech/.gitkeep +0 -0
  70. package/knowledge-library/engineering-manager/context/current.txt +40 -0
  71. package/knowledge-library/engineering-manager/control/decisions.txt +9 -0
  72. package/knowledge-library/engineering-manager/control/objectives.txt +27 -0
  73. package/knowledge-library/engineering-manager/history/.gitkeep +0 -0
  74. package/knowledge-library/engineering-manager/inbox/processed/.gitkeep +0 -0
  75. package/knowledge-library/engineering-manager/outbox/.gitkeep +0 -0
  76. package/knowledge-library/engineering-manager/tech/.gitkeep +0 -0
  77. package/knowledge-library/prd.txt +81 -0
  78. package/knowledge-library/product-manager/context/current.txt +42 -0
  79. package/knowledge-library/product-manager/control/decisions.txt +9 -0
  80. package/knowledge-library/product-manager/control/dependencies.txt +19 -0
  81. package/knowledge-library/product-manager/control/objectives.txt +26 -0
  82. package/knowledge-library/product-manager/history/.gitkeep +0 -0
  83. package/knowledge-library/product-manager/inbox/processed/.gitkeep +0 -0
  84. package/knowledge-library/product-manager/outbox/.gitkeep +0 -0
  85. package/knowledge-library/product-manager/tech/.gitkeep +0 -0
  86. package/knowledge-library/qa-engineer/context/current.txt +42 -0
  87. package/knowledge-library/qa-engineer/control/decisions.txt +9 -0
  88. package/knowledge-library/qa-engineer/control/dependencies.txt +19 -0
  89. package/knowledge-library/qa-engineer/control/objectives.txt +26 -0
  90. package/knowledge-library/qa-engineer/history/.gitkeep +0 -0
  91. package/knowledge-library/qa-engineer/inbox/processed/.gitkeep +0 -0
  92. package/knowledge-library/qa-engineer/outbox/.gitkeep +0 -0
  93. package/knowledge-library/qa-engineer/tech/.gitkeep +0 -0
  94. package/knowledge-library/security-engineer/context/current.txt +42 -0
  95. package/knowledge-library/security-engineer/control/decisions.txt +9 -0
  96. package/knowledge-library/security-engineer/control/dependencies.txt +19 -0
  97. package/knowledge-library/security-engineer/control/objectives.txt +26 -0
  98. package/knowledge-library/security-engineer/history/.gitkeep +0 -0
  99. package/knowledge-library/security-engineer/inbox/processed/.gitkeep +0 -0
  100. package/knowledge-library/security-engineer/outbox/.gitkeep +0 -0
  101. package/knowledge-library/security-engineer/tech/.gitkeep +0 -0
  102. package/knowledge-library/solutions-architect/context/current.txt +42 -0
  103. package/knowledge-library/solutions-architect/control/decisions.txt +9 -0
  104. package/knowledge-library/solutions-architect/control/dependencies.txt +19 -0
  105. package/knowledge-library/solutions-architect/control/objectives.txt +26 -0
  106. package/knowledge-library/solutions-architect/history/.gitkeep +0 -0
  107. package/knowledge-library/solutions-architect/inbox/processed/.gitkeep +0 -0
  108. package/knowledge-library/solutions-architect/outbox/.gitkeep +0 -0
  109. package/knowledge-library/solutions-architect/tech/.gitkeep +0 -0
  110. package/knowledge-library/wearos-engineer/context/current.txt +42 -0
  111. package/knowledge-library/wearos-engineer/control/decisions.txt +9 -0
  112. package/knowledge-library/wearos-engineer/control/dependencies.txt +19 -0
  113. package/knowledge-library/wearos-engineer/control/objectives.txt +26 -0
  114. package/knowledge-library/wearos-engineer/history/.gitkeep +0 -0
  115. package/knowledge-library/wearos-engineer/inbox/processed/.gitkeep +0 -0
  116. package/knowledge-library/wearos-engineer/outbox/.gitkeep +0 -0
  117. package/knowledge-library/wearos-engineer/tech/.gitkeep +0 -0
  118. package/package.json +66 -0
  119. package/schemas/agent.schema.json +171 -0
  120. package/schemas/coreai.config.schema.json +257 -0
  121. package/scripts/add-agent.sh +323 -0
  122. package/scripts/install.sh +354 -0
  123. package/src/adapters/factory.test.ts +386 -0
  124. package/src/adapters/factory.ts +305 -0
  125. package/src/adapters/index.ts +113 -0
  126. package/src/adapters/interfaces.ts +268 -0
  127. package/src/adapters/mcp/client.test.ts +130 -0
  128. package/src/adapters/mcp/client.ts +451 -0
  129. package/src/adapters/mcp/discovery.test.ts +315 -0
  130. package/src/adapters/mcp/discovery.ts +340 -0
  131. package/src/adapters/mcp/index.ts +66 -0
  132. package/src/adapters/mcp/mapper.test.ts +218 -0
  133. package/src/adapters/mcp/mapper.ts +536 -0
  134. package/src/adapters/mcp/registry.test.ts +433 -0
  135. package/src/adapters/mcp/registry.ts +550 -0
  136. package/src/adapters/mcp/types.ts +258 -0
  137. package/src/adapters/native/filesystem.test.ts +350 -0
  138. package/src/adapters/native/filesystem.ts +393 -0
  139. package/src/adapters/native/github.test.ts +173 -0
  140. package/src/adapters/native/github.ts +627 -0
  141. package/src/adapters/native/index.ts +22 -0
  142. package/src/adapters/native/selector.test.ts +224 -0
  143. package/src/adapters/native/selector.ts +150 -0
  144. package/src/adapters/types.ts +270 -0
  145. package/src/agents/compiler.test.ts +399 -0
  146. package/src/agents/compiler.ts +359 -0
  147. package/src/agents/index.ts +36 -0
  148. package/src/agents/loader.test.ts +319 -0
  149. package/src/agents/loader.ts +143 -0
  150. package/src/agents/resolver.test.ts +282 -0
  151. package/src/agents/resolver.ts +262 -0
  152. package/src/agents/types.ts +87 -0
  153. package/src/cache/index.ts +38 -0
  154. package/src/cache/interfaces.ts +283 -0
  155. package/src/cache/manager.test.ts +266 -0
  156. package/src/cache/manager.ts +388 -0
  157. package/src/cache/provider.test.ts +485 -0
  158. package/src/cache/provider.ts +745 -0
  159. package/src/cache/types.test.ts +192 -0
  160. package/src/cache/types.ts +313 -0
  161. package/src/cli/commands/build.test.ts +248 -0
  162. package/src/cli/commands/build.ts +244 -0
  163. package/src/cli/commands/cache.test.ts +221 -0
  164. package/src/cli/commands/cache.ts +229 -0
  165. package/src/cli/commands/index.ts +63 -0
  166. package/src/cli/commands/init.test.ts +173 -0
  167. package/src/cli/commands/init.ts +296 -0
  168. package/src/cli/commands/skills.test.ts +272 -0
  169. package/src/cli/commands/skills.ts +348 -0
  170. package/src/cli/commands/status.test.ts +392 -0
  171. package/src/cli/commands/status.ts +332 -0
  172. package/src/cli/commands/sync.test.ts +213 -0
  173. package/src/cli/commands/sync.ts +251 -0
  174. package/src/cli/commands/validate.test.ts +216 -0
  175. package/src/cli/commands/validate.ts +340 -0
  176. package/src/cli/index.test.ts +190 -0
  177. package/src/cli/index.ts +493 -0
  178. package/src/commands/context.test.ts +163 -0
  179. package/src/commands/context.ts +111 -0
  180. package/src/commands/index.ts +56 -0
  181. package/src/commands/loader.test.ts +273 -0
  182. package/src/commands/loader.ts +355 -0
  183. package/src/commands/registry.test.ts +384 -0
  184. package/src/commands/registry.ts +248 -0
  185. package/src/commands/runner.test.ts +297 -0
  186. package/src/commands/runner.ts +222 -0
  187. package/src/commands/types.ts +361 -0
  188. package/src/config/index.ts +19 -0
  189. package/src/config/loader.test.ts +262 -0
  190. package/src/config/loader.ts +188 -0
  191. package/src/config/types.ts +154 -0
  192. package/src/context/index.ts +14 -0
  193. package/src/context/loader.test.ts +334 -0
  194. package/src/context/loader.ts +357 -0
  195. package/src/index.test.ts +13 -0
  196. package/src/index.ts +244 -0
  197. package/src/knowledge-library/index.ts +44 -0
  198. package/src/knowledge-library/manager.test.ts +536 -0
  199. package/src/knowledge-library/manager.ts +804 -0
  200. package/src/knowledge-library/types.ts +432 -0
  201. package/src/skills/generator.test.ts +602 -0
  202. package/src/skills/generator.ts +491 -0
  203. package/src/skills/index.ts +27 -0
  204. package/src/skills/templates.ts +520 -0
  205. package/src/skills/types.ts +251 -0
  206. package/templates/completion-report.md +72 -0
  207. package/templates/feedback.md +56 -0
  208. package/templates/project-files/CLAUDE.md.template +109 -0
  209. package/templates/project-files/coreai.json.example +47 -0
  210. package/templates/project-files/mcp.json.template +20 -0
  211. package/templates/review-complete.md +64 -0
  212. package/templates/review-request.md +67 -0
  213. package/templates/task-assignment.md +51 -0
  214. package/tsconfig.build.json +4 -0
  215. package/tsconfig.json +26 -0
  216. package/tsup.config.ts +23 -0
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Status Command
3
+ *
4
+ * Shows agent states and pending messages.
5
+ */
6
+
7
+ import { configExists, loadConfig, ConfigError } from '../../config/loader.js';
8
+ import type { ResolvedCoreAIConfig } from '../../config/types.js';
9
+ import {
10
+ getKnowledgeLibraryState,
11
+ getAgentKnowledgeState,
12
+ initKnowledgeLibrary,
13
+ type KnowledgeLibraryState,
14
+ } from '../../knowledge-library/index.js';
15
+
16
+ /**
17
+ * Options for status command
18
+ */
19
+ export interface StatusCommandOptions {
20
+ /**
21
+ * Project root directory
22
+ */
23
+ projectRoot?: string;
24
+
25
+ /**
26
+ * Show status for specific agent only
27
+ */
28
+ agent?: string;
29
+
30
+ /**
31
+ * Show detailed status including message subjects
32
+ */
33
+ detailed?: boolean;
34
+
35
+ /**
36
+ * Initialize KnowledgeLibrary if not exists
37
+ */
38
+ init?: boolean;
39
+ }
40
+
41
+ /**
42
+ * Agent status info
43
+ */
44
+ export interface AgentStatus {
45
+ /**
46
+ * Agent name/role
47
+ */
48
+ name: string;
49
+
50
+ /**
51
+ * Whether the agent's KnowledgeLibrary is initialized
52
+ */
53
+ initialized: boolean;
54
+
55
+ /**
56
+ * Current status (e.g., "idle", "working")
57
+ */
58
+ status?: string;
59
+
60
+ /**
61
+ * Current task
62
+ */
63
+ currentTask?: string;
64
+
65
+ /**
66
+ * Current ticket
67
+ */
68
+ currentTicket?: string;
69
+
70
+ /**
71
+ * Number of pending inbox messages
72
+ */
73
+ pendingMessages: number;
74
+
75
+ /**
76
+ * Last activity timestamp
77
+ */
78
+ lastActivity?: Date;
79
+
80
+ /**
81
+ * Message details (if detailed mode)
82
+ */
83
+ messageDetails?: {
84
+ type: string;
85
+ from: string;
86
+ subject: string;
87
+ date?: Date;
88
+ }[];
89
+ }
90
+
91
+ /**
92
+ * Result of status command
93
+ */
94
+ export interface StatusCommandResult {
95
+ success: boolean;
96
+ knowledgeLibrary?: KnowledgeLibraryState;
97
+ agents: AgentStatus[];
98
+ error?: string;
99
+ warnings?: string[];
100
+ }
101
+
102
+ /**
103
+ * Get status of agents and KnowledgeLibrary
104
+ */
105
+ export function status(options: StatusCommandOptions = {}): StatusCommandResult {
106
+ const projectRoot = options.projectRoot ?? process.cwd();
107
+ const warnings: string[] = [];
108
+
109
+ // Load config if available to get team agents
110
+ let config: ResolvedCoreAIConfig | undefined;
111
+ let configuredAgents: string[] = [];
112
+
113
+ if (configExists(projectRoot)) {
114
+ try {
115
+ config = loadConfig(projectRoot);
116
+ if (config.team?.agents) {
117
+ configuredAgents = config.team.agents;
118
+ }
119
+ } catch (error) {
120
+ if (error instanceof ConfigError) {
121
+ warnings.push(`Configuration error: ${error.message}`);
122
+ } else {
123
+ warnings.push(
124
+ `Failed to load config: ${error instanceof Error ? error.message : String(error)}`
125
+ );
126
+ }
127
+ }
128
+ } else {
129
+ warnings.push('No configuration file found.');
130
+ }
131
+
132
+ // Get KnowledgeLibrary state
133
+ let klState = getKnowledgeLibraryState({ projectRoot });
134
+
135
+ // Initialize if requested and doesn't exist
136
+ if (!klState && options.init) {
137
+ const initResult = initKnowledgeLibrary({ projectRoot, createDefaults: true });
138
+ if (initResult.success) {
139
+ klState = getKnowledgeLibraryState({ projectRoot });
140
+ } else {
141
+ return {
142
+ success: false,
143
+ agents: [],
144
+ error: `Failed to initialize KnowledgeLibrary: ${initResult.error}`,
145
+ warnings: warnings.length > 0 ? warnings : undefined,
146
+ };
147
+ }
148
+ }
149
+
150
+ if (!klState) {
151
+ return {
152
+ success: true,
153
+ agents: [],
154
+ warnings: [
155
+ ...warnings,
156
+ 'KnowledgeLibrary not initialized. Run `coreai status --init` to create it.',
157
+ ],
158
+ };
159
+ }
160
+
161
+ // Determine which agents to show status for
162
+ let agentsToCheck: string[];
163
+ if (options.agent) {
164
+ agentsToCheck = [options.agent];
165
+ } else {
166
+ // Combine configured agents and discovered agents
167
+ agentsToCheck = [...new Set([...configuredAgents, ...klState.agents])];
168
+ }
169
+
170
+ // Get status for each agent
171
+ const agentStatuses: AgentStatus[] = [];
172
+
173
+ for (const agentName of agentsToCheck) {
174
+ const agentState = getAgentKnowledgeState(agentName, { projectRoot });
175
+
176
+ const agentStatus: AgentStatus = {
177
+ name: agentName,
178
+ initialized: agentState.initialized,
179
+ pendingMessages: agentState.pendingMessages.length,
180
+ };
181
+
182
+ if (agentState.context) {
183
+ agentStatus.status = agentState.context.status;
184
+ agentStatus.currentTask = agentState.context.currentTask;
185
+ agentStatus.currentTicket = agentState.context.currentTicket;
186
+ agentStatus.lastActivity = agentState.context.lastUpdated;
187
+ }
188
+
189
+ // Add message details if detailed mode
190
+ if (options.detailed && agentState.pendingMessages.length > 0) {
191
+ agentStatus.messageDetails = agentState.pendingMessages.map((msg) => ({
192
+ type: msg.metadata.type ?? 'unknown',
193
+ from: msg.metadata.from ?? 'unknown',
194
+ subject: msg.metadata.subject ?? msg.filename,
195
+ date: msg.metadata.date,
196
+ }));
197
+ }
198
+
199
+ agentStatuses.push(agentStatus);
200
+ }
201
+
202
+ // Sort by pending messages (most first), then by name
203
+ agentStatuses.sort((a, b) => {
204
+ if (b.pendingMessages !== a.pendingMessages) {
205
+ return b.pendingMessages - a.pendingMessages;
206
+ }
207
+ return a.name.localeCompare(b.name);
208
+ });
209
+
210
+ return {
211
+ success: true,
212
+ knowledgeLibrary: klState,
213
+ agents: agentStatuses,
214
+ warnings: warnings.length > 0 ? warnings : undefined,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Format status result for display
220
+ */
221
+ export function formatStatusResult(result: StatusCommandResult): string {
222
+ const lines: string[] = [];
223
+
224
+ // Warnings
225
+ if (result.warnings && result.warnings.length > 0) {
226
+ for (const warning of result.warnings) {
227
+ lines.push(`⚠ ${warning}`);
228
+ }
229
+ lines.push('');
230
+ }
231
+
232
+ if (!result.success) {
233
+ lines.push(`Error: ${result.error}`);
234
+ return lines.join('\n');
235
+ }
236
+
237
+ if (result.agents.length === 0) {
238
+ lines.push('No agents found.');
239
+ return lines.join('\n');
240
+ }
241
+
242
+ lines.push('Agent Status\n');
243
+
244
+ // Summary table header
245
+ const activeAgents = result.agents.filter((a) => a.status === 'working');
246
+ const idleAgents = result.agents.filter(
247
+ (a) => a.initialized && (!a.status || a.status === 'idle')
248
+ );
249
+ const uninitializedAgents = result.agents.filter((a) => !a.initialized);
250
+ const totalPending = result.agents.reduce((sum, a) => sum + a.pendingMessages, 0);
251
+
252
+ lines.push(`Total agents: ${result.agents.length}`);
253
+ if (activeAgents.length > 0) {
254
+ lines.push(` Active: ${activeAgents.length}`);
255
+ }
256
+ if (idleAgents.length > 0) {
257
+ lines.push(` Idle: ${idleAgents.length}`);
258
+ }
259
+ if (uninitializedAgents.length > 0) {
260
+ lines.push(` Not initialized: ${uninitializedAgents.length}`);
261
+ }
262
+ if (totalPending > 0) {
263
+ lines.push(` Total pending messages: ${totalPending}`);
264
+ }
265
+ lines.push('');
266
+
267
+ // Detailed agent list
268
+ for (const agent of result.agents) {
269
+ const statusIcon = !agent.initialized ? '○' : agent.pendingMessages > 0 ? '●' : '◎';
270
+ const statusText = !agent.initialized ? 'not initialized' : (agent.status ?? 'idle');
271
+
272
+ lines.push(`${statusIcon} ${agent.name}`);
273
+ lines.push(` Status: ${statusText}`);
274
+
275
+ if (agent.currentTask) {
276
+ lines.push(` Task: ${agent.currentTask}`);
277
+ }
278
+
279
+ if (agent.currentTicket) {
280
+ lines.push(` Ticket: ${agent.currentTicket}`);
281
+ }
282
+
283
+ if (agent.pendingMessages > 0) {
284
+ lines.push(` Pending messages: ${agent.pendingMessages}`);
285
+
286
+ // Show message details if available
287
+ if (agent.messageDetails && agent.messageDetails.length > 0) {
288
+ for (const msg of agent.messageDetails) {
289
+ const dateStr = msg.date ? msg.date.toISOString().slice(0, 16).replace('T', ' ') : '';
290
+ lines.push(` - [${msg.type}] from ${msg.from}: ${msg.subject}`);
291
+ if (dateStr) {
292
+ lines.push(` ${dateStr}`);
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ if (agent.lastActivity) {
299
+ const activityStr = agent.lastActivity.toISOString().slice(0, 16).replace('T', ' ');
300
+ lines.push(` Last activity: ${activityStr}`);
301
+ }
302
+
303
+ lines.push('');
304
+ }
305
+
306
+ return lines.join('\n');
307
+ }
308
+
309
+ /**
310
+ * Format a compact status summary
311
+ */
312
+ export function formatStatusSummary(result: StatusCommandResult): string {
313
+ if (!result.success) {
314
+ return `Error: ${result.error}`;
315
+ }
316
+
317
+ const activeCount = result.agents.filter((a) => a.status === 'working').length;
318
+ const pendingCount = result.agents.reduce((sum, a) => sum + a.pendingMessages, 0);
319
+
320
+ const parts: string[] = [];
321
+ parts.push(`${result.agents.length} agents`);
322
+
323
+ if (activeCount > 0) {
324
+ parts.push(`${activeCount} active`);
325
+ }
326
+
327
+ if (pendingCount > 0) {
328
+ parts.push(`${pendingCount} pending messages`);
329
+ }
330
+
331
+ return parts.join(', ');
332
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Sync Command Tests
3
+ */
4
+
5
+ import { promises as fs } from 'fs';
6
+ import { join } from 'path';
7
+ import { tmpdir } from 'os';
8
+ import { FileCacheProvider } from '../../cache/provider.js';
9
+ import { sync, formatSyncResult } from './sync.js';
10
+
11
+ describe('Sync Command', () => {
12
+ let testDir: string;
13
+
14
+ beforeEach(async () => {
15
+ testDir = join(tmpdir(), `sync-cmd-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
16
+ await fs.mkdir(testDir, { recursive: true });
17
+ });
18
+
19
+ afterEach(async () => {
20
+ try {
21
+ await fs.rm(testDir, { recursive: true, force: true });
22
+ } catch {
23
+ // Ignore cleanup errors
24
+ }
25
+ });
26
+
27
+ describe('sync', () => {
28
+ it('should return success for empty cache', async () => {
29
+ const provider = new FileCacheProvider({ basePath: testDir });
30
+ await provider.initialize();
31
+
32
+ const result = await sync({ cachePath: testDir });
33
+
34
+ expect(result.success).toBe(true);
35
+ expect(result.result).not.toBeNull();
36
+ expect(result.result?.added).toBe(0);
37
+ expect(result.result?.updated).toBe(0);
38
+ expect(result.result?.failed).toBe(0);
39
+ });
40
+
41
+ it('should attempt to sync cached entries', async () => {
42
+ const provider = new FileCacheProvider({ basePath: testDir });
43
+ await provider.initialize();
44
+ await provider.set('cached-entry', 'content', {
45
+ source: 'github',
46
+ sourceUrl: 'https://github.com/test/repo',
47
+ });
48
+
49
+ const result = await sync({
50
+ cachePath: testDir,
51
+ continueOnError: true,
52
+ });
53
+
54
+ // Should fail because there's no real fetcher configured
55
+ expect(result.sources).toContain('github');
56
+ // The placeholder fetcher will fail
57
+ expect(result.result?.failed).toBeGreaterThanOrEqual(0);
58
+ });
59
+
60
+ it('should filter by source', async () => {
61
+ const provider = new FileCacheProvider({ basePath: testDir });
62
+ await provider.initialize();
63
+ await provider.set('github-entry', 'content', {
64
+ source: 'github',
65
+ sourceUrl: 'https://github.com/test',
66
+ });
67
+ await provider.set('confluence-entry', 'content', {
68
+ source: 'confluence',
69
+ sourceUrl: 'https://confluence.test.com/page',
70
+ });
71
+
72
+ const result = await sync({
73
+ cachePath: testDir,
74
+ source: 'github',
75
+ continueOnError: true,
76
+ });
77
+
78
+ expect(result.sources).toContain('github');
79
+ expect(result.sources).not.toContain('confluence');
80
+ });
81
+
82
+ it('should handle uninitialized cache', async () => {
83
+ const result = await sync({ cachePath: join(testDir, 'new-cache') });
84
+
85
+ // Should initialize and return empty result
86
+ expect(result.success).toBe(true);
87
+ expect(result.result?.added).toBe(0);
88
+ });
89
+
90
+ it('should call progress callback', async () => {
91
+ const provider = new FileCacheProvider({ basePath: testDir });
92
+ await provider.initialize();
93
+ await provider.set('entry', 'content', {
94
+ source: 'github',
95
+ sourceUrl: 'https://github.com/test',
96
+ });
97
+
98
+ const progressCalls: { completed: number; total: number; message: string }[] = [];
99
+
100
+ await sync({
101
+ cachePath: testDir,
102
+ continueOnError: true,
103
+ onProgress: (completed, total, message) => {
104
+ progressCalls.push({ completed, total, message });
105
+ },
106
+ });
107
+
108
+ expect(progressCalls.length).toBeGreaterThan(0);
109
+ expect(progressCalls[0].message).toContain('Starting');
110
+ });
111
+ });
112
+
113
+ describe('formatSyncResult', () => {
114
+ it('should format error result', () => {
115
+ const result = formatSyncResult({
116
+ success: false,
117
+ result: null,
118
+ error: 'Connection failed',
119
+ sources: [],
120
+ });
121
+
122
+ expect(result).toContain('Error');
123
+ expect(result).toContain('Connection failed');
124
+ });
125
+
126
+ it('should format empty cache result', () => {
127
+ const result = formatSyncResult({
128
+ success: true,
129
+ result: {
130
+ added: 0,
131
+ updated: 0,
132
+ removed: 0,
133
+ failed: 0,
134
+ errors: [],
135
+ duration: 0,
136
+ },
137
+ sources: [],
138
+ });
139
+
140
+ expect(result).toContain('empty');
141
+ expect(result).toContain('Nothing to sync');
142
+ });
143
+
144
+ it('should format successful sync', () => {
145
+ const result = formatSyncResult({
146
+ success: true,
147
+ result: {
148
+ added: 2,
149
+ updated: 3,
150
+ removed: 1,
151
+ failed: 0,
152
+ errors: [],
153
+ duration: 1500,
154
+ },
155
+ sources: ['github', 'confluence'],
156
+ });
157
+
158
+ expect(result).toContain('Duration: 1500ms');
159
+ expect(result).toContain('Added: 2');
160
+ expect(result).toContain('Updated: 3');
161
+ expect(result).toContain('Removed: 1');
162
+ expect(result).toContain('github');
163
+ expect(result).toContain('confluence');
164
+ });
165
+
166
+ it('should format sync with errors', () => {
167
+ const result = formatSyncResult({
168
+ success: false,
169
+ result: {
170
+ added: 1,
171
+ updated: 0,
172
+ removed: 0,
173
+ failed: 2,
174
+ errors: [
175
+ { key: 'entry1', error: 'Network error' },
176
+ { key: 'entry2', error: 'Not found' },
177
+ ],
178
+ duration: 500,
179
+ },
180
+ sources: ['github'],
181
+ });
182
+
183
+ expect(result).toContain('Failed: 2');
184
+ expect(result).toContain('Errors:');
185
+ expect(result).toContain('entry1');
186
+ expect(result).toContain('Network error');
187
+ expect(result).toContain('entry2');
188
+ expect(result).toContain('Not found');
189
+ });
190
+
191
+ it('should truncate long error lists', () => {
192
+ const errors = Array.from({ length: 15 }, (_, i) => ({
193
+ key: `entry${i}`,
194
+ error: `Error ${i}`,
195
+ }));
196
+
197
+ const result = formatSyncResult({
198
+ success: false,
199
+ result: {
200
+ added: 0,
201
+ updated: 0,
202
+ removed: 0,
203
+ failed: 15,
204
+ errors,
205
+ duration: 100,
206
+ },
207
+ sources: ['github'],
208
+ });
209
+
210
+ expect(result).toContain('and 5 more errors');
211
+ });
212
+ });
213
+ });