@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,393 @@
1
+ /**
2
+ * Native Filesystem Adapter
3
+ *
4
+ * Implements StateProviderAdapter using the Node.js filesystem APIs.
5
+ * Used for local KnowledgeLibrary storage when no MCP server is available.
6
+ */
7
+
8
+ import { promises as fs } from 'fs';
9
+ import { join, dirname, resolve } from 'path';
10
+ import type { StateProviderAdapter } from '../interfaces.js';
11
+ import type { AdapterInfo, StateEntry, StateOptions } from '../types.js';
12
+ import { AdapterError } from '../types.js';
13
+
14
+ /**
15
+ * Options for creating a filesystem adapter
16
+ */
17
+ export interface FilesystemAdapterOptions {
18
+ /**
19
+ * Base directory for all operations (paths are relative to this)
20
+ */
21
+ basePath: string;
22
+
23
+ /**
24
+ * Default encoding for read/write operations
25
+ */
26
+ defaultEncoding?: BufferEncoding;
27
+ }
28
+
29
+ /**
30
+ * Native filesystem adapter for local state management
31
+ */
32
+ export class FilesystemAdapter implements StateProviderAdapter {
33
+ private basePath: string;
34
+ private defaultEncoding: BufferEncoding;
35
+ private connected = false;
36
+
37
+ constructor(options: FilesystemAdapterOptions) {
38
+ this.basePath = resolve(options.basePath);
39
+ this.defaultEncoding = options.defaultEncoding ?? 'utf-8';
40
+ }
41
+
42
+ /**
43
+ * Get adapter info
44
+ */
45
+ getInfo(): AdapterInfo {
46
+ return {
47
+ type: 'state',
48
+ provider: 'filesystem',
49
+ implementation: 'native',
50
+ connected: this.connected,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Check if connected (base path exists and is accessible)
56
+ */
57
+ isConnected(): boolean {
58
+ return this.connected;
59
+ }
60
+
61
+ /**
62
+ * Connect to the filesystem (verify base path exists)
63
+ */
64
+ async connect(): Promise<void> {
65
+ try {
66
+ const stat = await fs.stat(this.basePath);
67
+ if (!stat.isDirectory()) {
68
+ throw new AdapterError(
69
+ `Base path is not a directory: ${this.basePath}`,
70
+ 'connection_failed',
71
+ this.getInfo()
72
+ );
73
+ }
74
+ this.connected = true;
75
+ } catch (error) {
76
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
77
+ // Create the base directory
78
+ await fs.mkdir(this.basePath, { recursive: true });
79
+ this.connected = true;
80
+ } else {
81
+ throw new AdapterError(
82
+ `Failed to connect to filesystem: ${error instanceof Error ? error.message : String(error)}`,
83
+ 'connection_failed',
84
+ this.getInfo(),
85
+ error instanceof Error ? error : undefined
86
+ );
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Disconnect (no-op for filesystem)
93
+ */
94
+ async disconnect(): Promise<void> {
95
+ this.connected = false;
96
+ }
97
+
98
+ /**
99
+ * Read content from a file
100
+ */
101
+ async read(path: string, options?: StateOptions): Promise<string> {
102
+ this.ensureConnected();
103
+ const fullPath = this.resolvePath(path);
104
+ const encoding = options?.encoding ?? this.defaultEncoding;
105
+
106
+ try {
107
+ return await fs.readFile(fullPath, { encoding });
108
+ } catch (error) {
109
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
110
+ throw new AdapterError(`File not found: ${path}`, 'not_found', this.getInfo());
111
+ }
112
+ throw new AdapterError(
113
+ `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
114
+ 'operation_failed',
115
+ this.getInfo(),
116
+ error instanceof Error ? error : undefined
117
+ );
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Write content to a file
123
+ */
124
+ async write(path: string, content: string, options?: StateOptions): Promise<void> {
125
+ this.ensureConnected();
126
+ const fullPath = this.resolvePath(path);
127
+ const encoding = options?.encoding ?? this.defaultEncoding;
128
+
129
+ try {
130
+ if (options?.recursive) {
131
+ await fs.mkdir(dirname(fullPath), { recursive: true });
132
+ }
133
+ await fs.writeFile(fullPath, content, { encoding });
134
+ } catch (error) {
135
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
136
+ throw new AdapterError(
137
+ `Parent directory does not exist: ${dirname(path)}`,
138
+ 'not_found',
139
+ this.getInfo()
140
+ );
141
+ }
142
+ throw new AdapterError(
143
+ `Failed to write file: ${error instanceof Error ? error.message : String(error)}`,
144
+ 'operation_failed',
145
+ this.getInfo(),
146
+ error instanceof Error ? error : undefined
147
+ );
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Check if a path exists
153
+ */
154
+ async exists(path: string): Promise<boolean> {
155
+ this.ensureConnected();
156
+ const fullPath = this.resolvePath(path);
157
+
158
+ try {
159
+ await fs.access(fullPath);
160
+ return true;
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * List entries in a directory
168
+ */
169
+ async list(path: string): Promise<StateEntry[]> {
170
+ this.ensureConnected();
171
+ const fullPath = this.resolvePath(path);
172
+
173
+ try {
174
+ const entries = await fs.readdir(fullPath, { withFileTypes: true });
175
+ const results: StateEntry[] = [];
176
+
177
+ for (const entry of entries) {
178
+ const entryPath = join(path, entry.name);
179
+ const stat = await fs.stat(join(fullPath, entry.name));
180
+
181
+ const stateEntry: StateEntry = {
182
+ path: entryPath,
183
+ name: entry.name,
184
+ type: entry.isDirectory() ? 'directory' : 'file',
185
+ };
186
+
187
+ if (!entry.isDirectory()) {
188
+ stateEntry.size = stat.size;
189
+ }
190
+
191
+ stateEntry.modified_at = stat.mtime.toISOString();
192
+
193
+ results.push(stateEntry);
194
+ }
195
+
196
+ return results;
197
+ } catch (error) {
198
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
199
+ throw new AdapterError(`Directory not found: ${path}`, 'not_found', this.getInfo());
200
+ }
201
+ if ((error as NodeJS.ErrnoException).code === 'ENOTDIR') {
202
+ throw new AdapterError(
203
+ `Path is not a directory: ${path}`,
204
+ 'invalid_operation',
205
+ this.getInfo()
206
+ );
207
+ }
208
+ throw new AdapterError(
209
+ `Failed to list directory: ${error instanceof Error ? error.message : String(error)}`,
210
+ 'operation_failed',
211
+ this.getInfo(),
212
+ error instanceof Error ? error : undefined
213
+ );
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Delete a file or directory
219
+ */
220
+ async delete(path: string, options?: StateOptions): Promise<void> {
221
+ this.ensureConnected();
222
+ const fullPath = this.resolvePath(path);
223
+
224
+ try {
225
+ const stat = await fs.stat(fullPath);
226
+
227
+ if (stat.isDirectory()) {
228
+ if (options?.recursive) {
229
+ await fs.rm(fullPath, { recursive: true, force: true });
230
+ } else {
231
+ await fs.rmdir(fullPath);
232
+ }
233
+ } else {
234
+ await fs.unlink(fullPath);
235
+ }
236
+ } catch (error) {
237
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
238
+ // Already deleted, not an error
239
+ return;
240
+ }
241
+ if ((error as NodeJS.ErrnoException).code === 'ENOTEMPTY') {
242
+ throw new AdapterError(
243
+ `Directory not empty: ${path}. Use recursive option to delete non-empty directories.`,
244
+ 'invalid_operation',
245
+ this.getInfo()
246
+ );
247
+ }
248
+ throw new AdapterError(
249
+ `Failed to delete: ${error instanceof Error ? error.message : String(error)}`,
250
+ 'operation_failed',
251
+ this.getInfo(),
252
+ error instanceof Error ? error : undefined
253
+ );
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Create a directory
259
+ */
260
+ async mkdir(path: string, options?: StateOptions): Promise<void> {
261
+ this.ensureConnected();
262
+ const fullPath = this.resolvePath(path);
263
+
264
+ try {
265
+ await fs.mkdir(fullPath, { recursive: options?.recursive ?? false });
266
+ } catch (error) {
267
+ if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
268
+ // Directory already exists, not an error
269
+ return;
270
+ }
271
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
272
+ throw new AdapterError(
273
+ `Parent directory does not exist: ${dirname(path)}. Use recursive option.`,
274
+ 'not_found',
275
+ this.getInfo()
276
+ );
277
+ }
278
+ throw new AdapterError(
279
+ `Failed to create directory: ${error instanceof Error ? error.message : String(error)}`,
280
+ 'operation_failed',
281
+ this.getInfo(),
282
+ error instanceof Error ? error : undefined
283
+ );
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Move/rename a file or directory
289
+ */
290
+ async move(source: string, destination: string): Promise<void> {
291
+ this.ensureConnected();
292
+ const sourcePath = this.resolvePath(source);
293
+ const destPath = this.resolvePath(destination);
294
+
295
+ try {
296
+ await fs.rename(sourcePath, destPath);
297
+ } catch (error) {
298
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
299
+ throw new AdapterError(`Source not found: ${source}`, 'not_found', this.getInfo());
300
+ }
301
+ throw new AdapterError(
302
+ `Failed to move: ${error instanceof Error ? error.message : String(error)}`,
303
+ 'operation_failed',
304
+ this.getInfo(),
305
+ error instanceof Error ? error : undefined
306
+ );
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Copy a file or directory
312
+ */
313
+ async copy(source: string, destination: string): Promise<void> {
314
+ this.ensureConnected();
315
+ const sourcePath = this.resolvePath(source);
316
+ const destPath = this.resolvePath(destination);
317
+
318
+ try {
319
+ const stat = await fs.stat(sourcePath);
320
+
321
+ if (stat.isDirectory()) {
322
+ await this.copyDirectory(sourcePath, destPath);
323
+ } else {
324
+ await fs.copyFile(sourcePath, destPath);
325
+ }
326
+ } catch (error) {
327
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
328
+ throw new AdapterError(`Source not found: ${source}`, 'not_found', this.getInfo());
329
+ }
330
+ throw new AdapterError(
331
+ `Failed to copy: ${error instanceof Error ? error.message : String(error)}`,
332
+ 'operation_failed',
333
+ this.getInfo(),
334
+ error instanceof Error ? error : undefined
335
+ );
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Resolve a relative path to the full filesystem path
341
+ */
342
+ private resolvePath(path: string): string {
343
+ // Prevent path traversal attacks
344
+ const resolved = resolve(this.basePath, path);
345
+ if (!resolved.startsWith(this.basePath)) {
346
+ throw new AdapterError(
347
+ `Invalid path: ${path} (path traversal not allowed)`,
348
+ 'invalid_operation',
349
+ this.getInfo()
350
+ );
351
+ }
352
+ return resolved;
353
+ }
354
+
355
+ /**
356
+ * Ensure the adapter is connected
357
+ */
358
+ private ensureConnected(): void {
359
+ if (!this.connected) {
360
+ throw new AdapterError(
361
+ 'Filesystem adapter is not connected. Call connect() first.',
362
+ 'not_connected',
363
+ this.getInfo()
364
+ );
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Recursively copy a directory
370
+ */
371
+ private async copyDirectory(source: string, dest: string): Promise<void> {
372
+ await fs.mkdir(dest, { recursive: true });
373
+ const entries = await fs.readdir(source, { withFileTypes: true });
374
+
375
+ for (const entry of entries) {
376
+ const srcPath = join(source, entry.name);
377
+ const destPath = join(dest, entry.name);
378
+
379
+ if (entry.isDirectory()) {
380
+ await this.copyDirectory(srcPath, destPath);
381
+ } else {
382
+ await fs.copyFile(srcPath, destPath);
383
+ }
384
+ }
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Create a filesystem adapter
390
+ */
391
+ export function createFilesystemAdapter(options: FilesystemAdapterOptions): FilesystemAdapter {
392
+ return new FilesystemAdapter(options);
393
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Native GitHub Adapter Tests
3
+ */
4
+
5
+ import { GitHubAdapter, createGitHubAdapter } from './github.js';
6
+ import type { GitHubAdapterOptions } from './github.js';
7
+ import { AdapterError } from '../types.js';
8
+
9
+ describe('GitHubAdapter', () => {
10
+ describe('constructor', () => {
11
+ it('should create adapter with valid repository', () => {
12
+ const adapter = new GitHubAdapter({ repository: 'owner/repo' });
13
+ expect(adapter).toBeInstanceOf(GitHubAdapter);
14
+ });
15
+
16
+ it('should use default gh path', () => {
17
+ const adapter = new GitHubAdapter({ repository: 'owner/repo' });
18
+ const info = adapter.getInfo();
19
+ expect(info.provider).toBe('github');
20
+ });
21
+
22
+ it('should accept custom gh path', () => {
23
+ const adapter = new GitHubAdapter({
24
+ repository: 'owner/repo',
25
+ ghPath: '/usr/local/bin/gh',
26
+ });
27
+ expect(adapter).toBeInstanceOf(GitHubAdapter);
28
+ });
29
+
30
+ it('should throw for invalid repository format', () => {
31
+ expect(() => new GitHubAdapter({ repository: 'invalid' })).toThrow(AdapterError);
32
+ });
33
+
34
+ it('should throw for empty owner', () => {
35
+ expect(() => new GitHubAdapter({ repository: '/repo' })).toThrow(AdapterError);
36
+ });
37
+
38
+ it('should throw for empty repo', () => {
39
+ expect(() => new GitHubAdapter({ repository: 'owner/' })).toThrow(AdapterError);
40
+ });
41
+ });
42
+
43
+ describe('getInfo', () => {
44
+ it('should return adapter info', () => {
45
+ const adapter = new GitHubAdapter({ repository: 'owner/repo' });
46
+ const info = adapter.getInfo();
47
+
48
+ expect(info.type).toBe('git');
49
+ expect(info.provider).toBe('github');
50
+ expect(info.implementation).toBe('native');
51
+ expect(info.connected).toBe(false);
52
+ });
53
+
54
+ it('should reflect connection status', () => {
55
+ const adapter = new GitHubAdapter({ repository: 'owner/repo' });
56
+
57
+ expect(adapter.getInfo().connected).toBe(false);
58
+ expect(adapter.isConnected()).toBe(false);
59
+ });
60
+ });
61
+
62
+ describe('isConnected', () => {
63
+ it('should return false initially', () => {
64
+ const adapter = new GitHubAdapter({ repository: 'owner/repo' });
65
+ expect(adapter.isConnected()).toBe(false);
66
+ });
67
+ });
68
+
69
+ describe('disconnect', () => {
70
+ it('should disconnect successfully', async () => {
71
+ const adapter = new GitHubAdapter({ repository: 'owner/repo' });
72
+ await adapter.disconnect();
73
+ expect(adapter.isConnected()).toBe(false);
74
+ });
75
+ });
76
+
77
+ describe('operations without connection', () => {
78
+ let adapter: GitHubAdapter;
79
+
80
+ beforeEach(() => {
81
+ adapter = new GitHubAdapter({ repository: 'owner/repo' });
82
+ });
83
+
84
+ it('should throw on getPullRequest without connection', async () => {
85
+ await expect(adapter.getPullRequest(1)).rejects.toThrow(AdapterError);
86
+ });
87
+
88
+ it('should throw on listPullRequests without connection', async () => {
89
+ await expect(adapter.listPullRequests()).rejects.toThrow(AdapterError);
90
+ });
91
+
92
+ it('should throw on createPullRequest without connection', async () => {
93
+ await expect(
94
+ adapter.createPullRequest({
95
+ title: 'Test PR',
96
+ source_branch: 'feature',
97
+ target_branch: 'main',
98
+ })
99
+ ).rejects.toThrow(AdapterError);
100
+ });
101
+
102
+ it('should throw on updatePullRequest without connection', async () => {
103
+ await expect(adapter.updatePullRequest(1, { title: 'New title' })).rejects.toThrow(
104
+ AdapterError
105
+ );
106
+ });
107
+
108
+ it('should throw on mergePullRequest without connection', async () => {
109
+ await expect(adapter.mergePullRequest(1)).rejects.toThrow(AdapterError);
110
+ });
111
+
112
+ it('should throw on closePullRequest without connection', async () => {
113
+ await expect(adapter.closePullRequest(1)).rejects.toThrow(AdapterError);
114
+ });
115
+
116
+ it('should throw on addReview without connection', async () => {
117
+ await expect(adapter.addReview(1, { decision: 'approved' })).rejects.toThrow(AdapterError);
118
+ });
119
+
120
+ it('should throw on getReviews without connection', async () => {
121
+ await expect(adapter.getReviews(1)).rejects.toThrow(AdapterError);
122
+ });
123
+
124
+ it('should throw on requestReviewers without connection', async () => {
125
+ await expect(adapter.requestReviewers(1, ['reviewer'])).rejects.toThrow(AdapterError);
126
+ });
127
+
128
+ it('should throw on addPullRequestComment without connection', async () => {
129
+ await expect(adapter.addPullRequestComment(1, 'comment')).rejects.toThrow(AdapterError);
130
+ });
131
+
132
+ it('should throw on getPullRequestDiff without connection', async () => {
133
+ await expect(adapter.getPullRequestDiff(1)).rejects.toThrow(AdapterError);
134
+ });
135
+
136
+ it('should throw on getPullRequestFiles without connection', async () => {
137
+ await expect(adapter.getPullRequestFiles(1)).rejects.toThrow(AdapterError);
138
+ });
139
+ });
140
+ });
141
+
142
+ describe('createGitHubAdapter', () => {
143
+ it('should create adapter instance', () => {
144
+ const adapter = createGitHubAdapter({ repository: 'owner/repo' });
145
+ expect(adapter).toBeInstanceOf(GitHubAdapter);
146
+ });
147
+
148
+ it('should accept all options', () => {
149
+ const options: GitHubAdapterOptions = {
150
+ repository: 'owner/repo',
151
+ ghPath: '/custom/gh',
152
+ };
153
+
154
+ const adapter = createGitHubAdapter(options);
155
+ expect(adapter).toBeInstanceOf(GitHubAdapter);
156
+ });
157
+ });
158
+
159
+ describe('GitHubAdapter internal mapping', () => {
160
+ // These tests verify the internal state mapping logic by checking
161
+ // the adapter info since we can't easily test the gh CLI calls
162
+
163
+ it('should handle string PR number', () => {
164
+ const adapter = new GitHubAdapter({ repository: 'owner/repo' });
165
+ // Verify adapter was created - the actual PR operations would need gh CLI
166
+ expect(adapter.getInfo().type).toBe('git');
167
+ });
168
+
169
+ it('should handle numeric PR number', () => {
170
+ const adapter = new GitHubAdapter({ repository: 'owner/repo' });
171
+ expect(adapter.getInfo().type).toBe('git');
172
+ });
173
+ });