@yeseh/cortex-cli 0.6.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 (189) hide show
  1. package/README.md +144 -0
  2. package/dist/category/commands/create.d.ts +44 -0
  3. package/dist/category/commands/create.d.ts.map +1 -0
  4. package/dist/category/commands/create.spec.d.ts +7 -0
  5. package/dist/category/commands/create.spec.d.ts.map +1 -0
  6. package/dist/category/index.d.ts +19 -0
  7. package/dist/category/index.d.ts.map +1 -0
  8. package/dist/commands/init.d.ts +58 -0
  9. package/dist/commands/init.d.ts.map +1 -0
  10. package/dist/commands/init.spec.d.ts +2 -0
  11. package/dist/commands/init.spec.d.ts.map +1 -0
  12. package/dist/context.d.ts +18 -0
  13. package/dist/context.d.ts.map +1 -0
  14. package/dist/context.spec.d.ts +2 -0
  15. package/dist/context.spec.d.ts.map +1 -0
  16. package/dist/create-cli-command.d.ts +23 -0
  17. package/dist/create-cli-command.d.ts.map +1 -0
  18. package/dist/create-cli-command.spec.d.ts +10 -0
  19. package/dist/create-cli-command.spec.d.ts.map +1 -0
  20. package/dist/errors.d.ts +57 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.spec.d.ts +2 -0
  23. package/dist/errors.spec.d.ts.map +1 -0
  24. package/dist/input.d.ts +42 -0
  25. package/dist/input.d.ts.map +1 -0
  26. package/dist/input.spec.d.ts +2 -0
  27. package/dist/input.spec.d.ts.map +1 -0
  28. package/dist/memory/commands/add.d.ts +62 -0
  29. package/dist/memory/commands/add.d.ts.map +1 -0
  30. package/dist/memory/commands/add.spec.d.ts +7 -0
  31. package/dist/memory/commands/add.spec.d.ts.map +1 -0
  32. package/dist/memory/commands/definitions.spec.d.ts +10 -0
  33. package/dist/memory/commands/definitions.spec.d.ts.map +1 -0
  34. package/dist/memory/commands/handlers.spec.d.ts +2 -0
  35. package/dist/memory/commands/handlers.spec.d.ts.map +1 -0
  36. package/dist/memory/commands/list.d.ts +119 -0
  37. package/dist/memory/commands/list.d.ts.map +1 -0
  38. package/dist/memory/commands/list.spec.d.ts +2 -0
  39. package/dist/memory/commands/list.spec.d.ts.map +1 -0
  40. package/dist/memory/commands/move.d.ts +42 -0
  41. package/dist/memory/commands/move.d.ts.map +1 -0
  42. package/dist/memory/commands/move.spec.d.ts +2 -0
  43. package/dist/memory/commands/move.spec.d.ts.map +1 -0
  44. package/dist/memory/commands/remove.d.ts +41 -0
  45. package/dist/memory/commands/remove.d.ts.map +1 -0
  46. package/dist/memory/commands/remove.spec.d.ts +2 -0
  47. package/dist/memory/commands/remove.spec.d.ts.map +1 -0
  48. package/dist/memory/commands/show.d.ts +81 -0
  49. package/dist/memory/commands/show.d.ts.map +1 -0
  50. package/dist/memory/commands/show.spec.d.ts +2 -0
  51. package/dist/memory/commands/show.spec.d.ts.map +1 -0
  52. package/dist/memory/commands/test-helpers.spec.d.ts +19 -0
  53. package/dist/memory/commands/test-helpers.spec.d.ts.map +1 -0
  54. package/dist/memory/commands/update.d.ts +73 -0
  55. package/dist/memory/commands/update.d.ts.map +1 -0
  56. package/dist/memory/commands/update.spec.d.ts +2 -0
  57. package/dist/memory/commands/update.spec.d.ts.map +1 -0
  58. package/dist/memory/index.d.ts +29 -0
  59. package/dist/memory/index.d.ts.map +1 -0
  60. package/dist/memory/index.spec.d.ts +10 -0
  61. package/dist/memory/index.spec.d.ts.map +1 -0
  62. package/dist/memory/parsing.d.ts +3 -0
  63. package/dist/memory/parsing.d.ts.map +1 -0
  64. package/dist/memory/parsing.spec.d.ts +7 -0
  65. package/dist/memory/parsing.spec.d.ts.map +1 -0
  66. package/dist/output.d.ts +87 -0
  67. package/dist/output.d.ts.map +1 -0
  68. package/dist/output.spec.d.ts +2 -0
  69. package/dist/output.spec.d.ts.map +1 -0
  70. package/dist/paths.d.ts +27 -0
  71. package/dist/paths.d.ts.map +1 -0
  72. package/dist/paths.spec.d.ts +7 -0
  73. package/dist/paths.spec.d.ts.map +1 -0
  74. package/dist/program.d.ts +41 -0
  75. package/dist/program.d.ts.map +1 -0
  76. package/dist/program.spec.d.ts +11 -0
  77. package/dist/program.spec.d.ts.map +1 -0
  78. package/dist/run.d.ts +7 -0
  79. package/dist/run.d.ts.map +1 -0
  80. package/dist/run.spec.d.ts +12 -0
  81. package/dist/run.spec.d.ts.map +1 -0
  82. package/dist/store/commands/add.d.ts +73 -0
  83. package/dist/store/commands/add.d.ts.map +1 -0
  84. package/dist/store/commands/add.spec.d.ts +17 -0
  85. package/dist/store/commands/add.spec.d.ts.map +1 -0
  86. package/dist/store/commands/init.d.ts +75 -0
  87. package/dist/store/commands/init.d.ts.map +1 -0
  88. package/dist/store/commands/init.spec.d.ts +7 -0
  89. package/dist/store/commands/init.spec.d.ts.map +1 -0
  90. package/dist/store/commands/list.d.ts +62 -0
  91. package/dist/store/commands/list.d.ts.map +1 -0
  92. package/dist/store/commands/list.spec.d.ts +7 -0
  93. package/dist/store/commands/list.spec.d.ts.map +1 -0
  94. package/dist/store/commands/prune.d.ts +92 -0
  95. package/dist/store/commands/prune.d.ts.map +1 -0
  96. package/dist/store/commands/prune.spec.d.ts +7 -0
  97. package/dist/store/commands/prune.spec.d.ts.map +1 -0
  98. package/dist/store/commands/reindexs.d.ts +54 -0
  99. package/dist/store/commands/reindexs.d.ts.map +1 -0
  100. package/dist/store/commands/reindexs.spec.d.ts +7 -0
  101. package/dist/store/commands/reindexs.spec.d.ts.map +1 -0
  102. package/dist/store/commands/remove.d.ts +63 -0
  103. package/dist/store/commands/remove.d.ts.map +1 -0
  104. package/dist/store/commands/remove.spec.d.ts +17 -0
  105. package/dist/store/commands/remove.spec.d.ts.map +1 -0
  106. package/dist/store/index.d.ts +32 -0
  107. package/dist/store/index.d.ts.map +1 -0
  108. package/dist/store/index.spec.d.ts +9 -0
  109. package/dist/store/index.spec.d.ts.map +1 -0
  110. package/dist/store/utils/resolve-store-name.d.ts +30 -0
  111. package/dist/store/utils/resolve-store-name.d.ts.map +1 -0
  112. package/dist/store/utils/resolve-store-name.spec.d.ts +2 -0
  113. package/dist/store/utils/resolve-store-name.spec.d.ts.map +1 -0
  114. package/dist/test-helpers.spec.d.ts +224 -0
  115. package/dist/test-helpers.spec.d.ts.map +1 -0
  116. package/dist/tests/cli.integration.spec.d.ts +11 -0
  117. package/dist/tests/cli.integration.spec.d.ts.map +1 -0
  118. package/dist/toon.d.ts +197 -0
  119. package/dist/toon.d.ts.map +1 -0
  120. package/dist/toon.spec.d.ts +9 -0
  121. package/dist/toon.spec.d.ts.map +1 -0
  122. package/dist/utils/git.d.ts +20 -0
  123. package/dist/utils/git.d.ts.map +1 -0
  124. package/dist/utils/git.spec.d.ts +7 -0
  125. package/dist/utils/git.spec.d.ts.map +1 -0
  126. package/package.json +45 -0
  127. package/src/category/commands/create.spec.ts +139 -0
  128. package/src/category/commands/create.ts +115 -0
  129. package/src/category/index.ts +24 -0
  130. package/src/commands/init.spec.ts +203 -0
  131. package/src/commands/init.ts +301 -0
  132. package/src/context.spec.ts +60 -0
  133. package/src/context.ts +175 -0
  134. package/src/errors.spec.ts +264 -0
  135. package/src/errors.ts +105 -0
  136. package/src/memory/commands/add.spec.ts +169 -0
  137. package/src/memory/commands/add.ts +157 -0
  138. package/src/memory/commands/definitions.spec.ts +80 -0
  139. package/src/memory/commands/list.spec.ts +123 -0
  140. package/src/memory/commands/list.ts +268 -0
  141. package/src/memory/commands/move.spec.ts +85 -0
  142. package/src/memory/commands/move.ts +115 -0
  143. package/src/memory/commands/remove.spec.ts +79 -0
  144. package/src/memory/commands/remove.ts +104 -0
  145. package/src/memory/commands/show.spec.ts +71 -0
  146. package/src/memory/commands/show.ts +164 -0
  147. package/src/memory/commands/test-helpers.spec.ts +127 -0
  148. package/src/memory/commands/update.spec.ts +86 -0
  149. package/src/memory/commands/update.ts +229 -0
  150. package/src/memory/index.spec.ts +59 -0
  151. package/src/memory/index.ts +44 -0
  152. package/src/memory/parsing.spec.ts +105 -0
  153. package/src/memory/parsing.ts +22 -0
  154. package/src/observability.spec.ts +139 -0
  155. package/src/observability.ts +63 -0
  156. package/src/output.spec.ts +835 -0
  157. package/src/output.ts +119 -0
  158. package/src/program.spec.ts +46 -0
  159. package/src/program.ts +75 -0
  160. package/src/run.spec.ts +31 -0
  161. package/src/run.ts +9 -0
  162. package/src/store/commands/add.spec.ts +131 -0
  163. package/src/store/commands/add.ts +231 -0
  164. package/src/store/commands/init.spec.ts +236 -0
  165. package/src/store/commands/init.ts +256 -0
  166. package/src/store/commands/list.spec.ts +175 -0
  167. package/src/store/commands/list.ts +102 -0
  168. package/src/store/commands/prune.spec.ts +120 -0
  169. package/src/store/commands/prune.ts +152 -0
  170. package/src/store/commands/reindexs.spec.ts +94 -0
  171. package/src/store/commands/reindexs.ts +96 -0
  172. package/src/store/commands/remove.spec.ts +97 -0
  173. package/src/store/commands/remove.ts +189 -0
  174. package/src/store/index.spec.ts +60 -0
  175. package/src/store/index.ts +49 -0
  176. package/src/store/utils/resolve-store-name.spec.ts +62 -0
  177. package/src/store/utils/resolve-store-name.ts +79 -0
  178. package/src/test-helpers.spec.ts +430 -0
  179. package/src/tests/cli.integration.spec.ts +1170 -0
  180. package/src/toon.spec.ts +183 -0
  181. package/src/toon.ts +462 -0
  182. package/src/utils/git.spec.ts +95 -0
  183. package/src/utils/git.ts +51 -0
  184. package/src/utils/input.spec.ts +326 -0
  185. package/src/utils/input.ts +145 -0
  186. package/src/utils/paths.spec.ts +235 -0
  187. package/src/utils/paths.ts +75 -0
  188. package/src/utils/prompts.spec.ts +23 -0
  189. package/src/utils/prompts.ts +88 -0
@@ -0,0 +1,264 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { CommanderError, InvalidArgumentError } from '@commander-js/extra-typings';
3
+
4
+ import { throwCliError, type CoreError } from './errors.ts';
5
+
6
+ describe('throwCliError', () => {
7
+ describe('argument error codes', () => {
8
+ const argumentErrorCodes = [
9
+ 'INVALID_PATH',
10
+ 'INVALID_FILE_PATH',
11
+ 'INVALID_SOURCE_PATH',
12
+ 'INVALID_DESTINATION_PATH',
13
+ 'INVALID_ARGUMENTS',
14
+ 'INVALID_STORE_NAME',
15
+ 'INVALID_STORE_PATH',
16
+ 'MISSING_CONTENT',
17
+ 'MULTIPLE_CONTENT_SOURCES',
18
+ 'CONTENT_INPUT_FAILED',
19
+ 'INVALID_COMMAND',
20
+ 'GIT_REPO_REQUIRED',
21
+ ];
22
+
23
+ it.each(argumentErrorCodes)('should throw InvalidArgumentError for %s', (code) => {
24
+ const error: CoreError = {
25
+ code,
26
+ message: `Test message for ${code}`,
27
+ };
28
+
29
+ expect(() => throwCliError(error)).toThrow(InvalidArgumentError);
30
+ });
31
+
32
+ it('should preserve error message in InvalidArgumentError', () => {
33
+ const error: CoreError = {
34
+ code: 'INVALID_PATH',
35
+ message: 'Path must not be empty',
36
+ };
37
+
38
+ try {
39
+ throwCliError(error);
40
+ expect.unreachable('mapCoreError should have thrown');
41
+ }
42
+ catch (e) {
43
+ expect(e).toBeInstanceOf(InvalidArgumentError);
44
+ expect((e as InvalidArgumentError).message).toBe('Path must not be empty');
45
+ }
46
+ });
47
+
48
+ it('should handle empty message for argument errors', () => {
49
+ const error: CoreError = {
50
+ code: 'MISSING_CONTENT',
51
+ message: '',
52
+ };
53
+
54
+ try {
55
+ throwCliError(error);
56
+ expect.unreachable('mapCoreError should have thrown');
57
+ }
58
+ catch (e) {
59
+ expect(e).toBeInstanceOf(InvalidArgumentError);
60
+ expect((e as InvalidArgumentError).message).toBe('');
61
+ }
62
+ });
63
+ });
64
+
65
+ describe('non-argument error codes', () => {
66
+ const nonArgumentErrorCodes = [
67
+ 'MEMORY_NOT_FOUND',
68
+ 'STORE_NOT_FOUND',
69
+ 'FILE_READ_FAILED',
70
+ 'CATEGORY_NOT_FOUND',
71
+ 'INTERNAL_ERROR',
72
+ 'UNKNOWN_ERROR',
73
+ 'SERIALIZE_FAILED',
74
+ 'STORE_INIT_FAILED',
75
+ 'STORE_REGISTRY_FAILED',
76
+ 'STORE_ALREADY_EXISTS',
77
+ 'STORAGE_ERROR',
78
+ 'IO_READ_ERROR',
79
+ 'IO_WRITE_ERROR',
80
+ 'PARSE_FAILED',
81
+ 'MOVE_FAILED',
82
+ 'REMOVE_FAILED',
83
+ 'REINDEX_FAILED',
84
+ ];
85
+
86
+ it.each(nonArgumentErrorCodes)('should throw CommanderError for %s', (code) => {
87
+ const error: CoreError = {
88
+ code,
89
+ message: `Test message for ${code}`,
90
+ };
91
+
92
+ expect(() => throwCliError(error)).toThrow(CommanderError);
93
+ });
94
+
95
+ it('should set exitCode to 1 for CommanderError', () => {
96
+ const error: CoreError = {
97
+ code: 'MEMORY_NOT_FOUND',
98
+ message: 'Memory not found at path',
99
+ };
100
+
101
+ try {
102
+ throwCliError(error);
103
+ expect.unreachable('mapCoreError should have thrown');
104
+ }
105
+ catch (e) {
106
+ expect(e).toBeInstanceOf(CommanderError);
107
+ expect((e as CommanderError).exitCode).toBe(1);
108
+ }
109
+ });
110
+
111
+ it('should preserve error code in CommanderError', () => {
112
+ const error: CoreError = {
113
+ code: 'STORE_NOT_FOUND',
114
+ message: 'Store not found',
115
+ };
116
+
117
+ try {
118
+ throwCliError(error);
119
+ expect.unreachable('mapCoreError should have thrown');
120
+ }
121
+ catch (e) {
122
+ expect(e).toBeInstanceOf(CommanderError);
123
+ expect((e as CommanderError).code).toBe('STORE_NOT_FOUND');
124
+ }
125
+ });
126
+
127
+ it('should preserve error message in CommanderError', () => {
128
+ const error: CoreError = {
129
+ code: 'FILE_READ_FAILED',
130
+ message: 'Could not read file: /path/to/file.txt',
131
+ };
132
+
133
+ try {
134
+ throwCliError(error);
135
+ expect.unreachable('mapCoreError should have thrown');
136
+ }
137
+ catch (e) {
138
+ expect(e).toBeInstanceOf(CommanderError);
139
+ expect((e as CommanderError).message).toBe(
140
+ 'Could not read file: /path/to/file.txt',
141
+ );
142
+ }
143
+ });
144
+
145
+ it('should handle empty message for non-argument errors', () => {
146
+ const error: CoreError = {
147
+ code: 'INTERNAL_ERROR',
148
+ message: '',
149
+ };
150
+
151
+ try {
152
+ throwCliError(error);
153
+ expect.unreachable('mapCoreError should have thrown');
154
+ }
155
+ catch (e) {
156
+ expect(e).toBeInstanceOf(CommanderError);
157
+ expect((e as CommanderError).message).toBe('');
158
+ }
159
+ });
160
+ });
161
+
162
+ describe('edge cases', () => {
163
+ it('should handle unknown error codes as non-argument errors', () => {
164
+ const error: CoreError = {
165
+ code: 'COMPLETELY_UNKNOWN_CODE',
166
+ message: 'An unexpected error',
167
+ };
168
+
169
+ try {
170
+ throwCliError(error);
171
+ expect.unreachable('mapCoreError should have thrown');
172
+ }
173
+ catch (e) {
174
+ expect(e).toBeInstanceOf(CommanderError);
175
+ expect((e as CommanderError).code).toBe('COMPLETELY_UNKNOWN_CODE');
176
+ }
177
+ });
178
+
179
+ it('should handle error codes that are similar to argument codes', () => {
180
+ const error: CoreError = {
181
+ code: 'INVALID_PATH_EXTRA', // Similar but not in the set
182
+ message: 'This should be a CommanderError',
183
+ };
184
+
185
+ try {
186
+ throwCliError(error);
187
+ expect.unreachable('mapCoreError should have thrown');
188
+ }
189
+ catch (e) {
190
+ expect(e).toBeInstanceOf(CommanderError);
191
+ }
192
+ });
193
+
194
+ it('should be case-sensitive for error codes', () => {
195
+ const error: CoreError = {
196
+ code: 'invalid_path', // lowercase version
197
+ message: 'This should be a CommanderError due to case sensitivity',
198
+ };
199
+
200
+ try {
201
+ throwCliError(error);
202
+ expect.unreachable('mapCoreError should have thrown');
203
+ }
204
+ catch (e) {
205
+ expect(e).toBeInstanceOf(CommanderError);
206
+ }
207
+ });
208
+
209
+ it('should handle message with special characters', () => {
210
+ const error: CoreError = {
211
+ code: 'INVALID_PATH',
212
+ message: 'Path contains invalid chars: <>&"\'\n\t',
213
+ };
214
+
215
+ try {
216
+ throwCliError(error);
217
+ expect.unreachable('mapCoreError should have thrown');
218
+ }
219
+ catch (e) {
220
+ expect(e).toBeInstanceOf(InvalidArgumentError);
221
+ expect((e as InvalidArgumentError).message).toBe(
222
+ 'Path contains invalid chars: <>&"\'\n\t',
223
+ );
224
+ }
225
+ });
226
+
227
+ it('should handle very long error messages', () => {
228
+ const longMessage = 'A'.repeat(10000);
229
+ const error: CoreError = {
230
+ code: 'MEMORY_NOT_FOUND',
231
+ message: longMessage,
232
+ };
233
+
234
+ try {
235
+ throwCliError(error);
236
+ expect.unreachable('mapCoreError should have thrown');
237
+ }
238
+ catch (e) {
239
+ expect(e).toBeInstanceOf(CommanderError);
240
+ expect((e as CommanderError).message).toBe(longMessage);
241
+ }
242
+ });
243
+ });
244
+
245
+ describe('return type verification', () => {
246
+ it('should never return (always throws)', () => {
247
+ const error: CoreError = {
248
+ code: 'INVALID_PATH',
249
+ message: 'Test',
250
+ };
251
+
252
+ let didReturn = false;
253
+ try {
254
+ throwCliError(error);
255
+ didReturn = true;
256
+ }
257
+ catch {
258
+ // Expected
259
+ }
260
+
261
+ expect(didReturn).toBe(false);
262
+ });
263
+ });
264
+ });
package/src/errors.ts ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Error mapping utilities for CLI commands.
3
+ *
4
+ * This module provides utilities to map core Result errors to Commander.js
5
+ * exceptions. Commander.js uses specific exception types to control error
6
+ * output and exit codes:
7
+ *
8
+ * - `InvalidArgumentError` - For user input validation errors (shows usage help)
9
+ * - `CommanderError` - For other errors (shows error message only)
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const result = await runAddCommand(options);
14
+ * if (!result.ok()) {
15
+ * mapCoreError(result.error);
16
+ * }
17
+ * ```
18
+ */
19
+
20
+ import { InvalidArgumentError, CommanderError } from '@commander-js/extra-typings';
21
+
22
+ /**
23
+ * Represents a core error with a code and message.
24
+ *
25
+ * This interface matches the error shape used throughout the CLI commands,
26
+ * allowing consistent error handling across all command implementations.
27
+ */
28
+ export interface CoreError {
29
+ code: string;
30
+ message: string;
31
+ cause?: unknown;
32
+ }
33
+
34
+ /**
35
+ * Error codes that indicate invalid user input or arguments.
36
+ *
37
+ * These errors result in `InvalidArgumentError` which causes Commander.js
38
+ * to display usage help alongside the error message.
39
+ *
40
+ * Includes:
41
+ * - Path validation errors (`INVALID_PATH`)
42
+ * - Argument parsing errors (`INVALID_ARGUMENTS`)
43
+ * - Store configuration errors (`INVALID_STORE_NAME`, `INVALID_STORE_PATH`)
44
+ * - Content input errors (`MISSING_CONTENT`, `MULTIPLE_CONTENT_SOURCES`, `CONTENT_INPUT_FAILED`)
45
+ * - Command syntax errors (`INVALID_COMMAND`)
46
+ */
47
+ const ARGUMENT_ERROR_CODES = new Set([
48
+ // Path validation
49
+ 'INVALID_PATH',
50
+ 'INVALID_FILE_PATH',
51
+ 'INVALID_SOURCE_PATH',
52
+ 'INVALID_DESTINATION_PATH',
53
+
54
+ // Argument parsing
55
+ 'INVALID_ARGUMENTS',
56
+
57
+ // Store configuration
58
+ 'INVALID_STORE_NAME',
59
+ 'INVALID_STORE_PATH',
60
+
61
+ // Content input
62
+ 'MISSING_CONTENT',
63
+ 'MULTIPLE_CONTENT_SOURCES',
64
+ 'CONTENT_INPUT_FAILED',
65
+
66
+ // Command syntax
67
+ 'INVALID_COMMAND',
68
+
69
+ // Git detection (user needs to provide --name)
70
+ 'GIT_REPO_REQUIRED',
71
+ ]);
72
+
73
+ /**
74
+ * Maps a core error to a Commander.js exception and throws it.
75
+ *
76
+ * This function examines the error code to determine the appropriate
77
+ * Commander.js exception type:
78
+ *
79
+ * - **Argument errors** (`InvalidArgumentError`): For errors caused by invalid
80
+ * user input. Commander.js displays usage help for these errors.
81
+ *
82
+ * - **Other errors** (`CommanderError`): For system errors, missing resources,
83
+ * or internal failures. Commander.js displays only the error message.
84
+ *
85
+ * @param error - The core error to map
86
+ * @throws {InvalidArgumentError} When the error code indicates invalid input
87
+ * @throws {CommanderError} For all other error codes
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * // In a command action handler
92
+ * const result = await runShowCommand(options);
93
+ * if (!result.ok()) {
94
+ * mapCoreError(result.error);
95
+ * // Never reaches here - mapCoreError always throws
96
+ * }
97
+ * ```
98
+ */
99
+ export function throwCliError(error: CoreError): never {
100
+ if (ARGUMENT_ERROR_CODES.has(error.code)) {
101
+ throw new InvalidArgumentError(error.message);
102
+ }
103
+
104
+ throw new CommanderError(1, error.code, error.message);
105
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Unit tests for the handleAdd command handler.
3
+ *
4
+ * @module cli/memory/commands/add.spec
5
+ */
6
+
7
+ import { describe, it, expect, afterEach, beforeEach } from 'bun:test';
8
+ import { CommanderError, InvalidArgumentError } from '@commander-js/extra-typings';
9
+ import { mkdtemp, rm } from 'node:fs/promises';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { PassThrough } from 'node:stream';
13
+ import {
14
+ ok,
15
+ type AdapterFactory,
16
+ } from '@yeseh/cortex-core';
17
+ import { handleAdd } from './add.ts';
18
+ import {
19
+ createCaptureStream,
20
+ createMemoryCommandContext,
21
+ createMockMemoryCommandAdapter,
22
+ } from './test-helpers.spec.ts';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Local helpers
26
+ // ---------------------------------------------------------------------------
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Tests
30
+ // ---------------------------------------------------------------------------
31
+
32
+ describe('handleAdd', () => {
33
+ let tempDir: string;
34
+
35
+ beforeEach(async () => {
36
+ tempDir = await mkdtemp(join(tmpdir(), 'cortex-add-'));
37
+ });
38
+
39
+ afterEach(async () => {
40
+ await rm(tempDir, { recursive: true, force: true });
41
+ });
42
+
43
+ it('should create a memory and write success message to stdout', async () => {
44
+ const capture = createCaptureStream();
45
+ const ctx = createMemoryCommandContext({
46
+ adapter: createMockMemoryCommandAdapter(),
47
+ storePath: tempDir,
48
+ stdout: capture.stream,
49
+ });
50
+
51
+ await handleAdd(ctx, undefined, 'project/notes', { content: 'Hello world' });
52
+
53
+ const out = capture.getOutput();
54
+ expect(out).toContain('Added memory');
55
+ expect(out).toContain('project/notes');
56
+ });
57
+
58
+ it('should pass tags from options', async () => {
59
+ const capture = createCaptureStream();
60
+ const ctx = createMemoryCommandContext({
61
+ adapter: createMockMemoryCommandAdapter(),
62
+ storePath: tempDir,
63
+ stdout: capture.stream,
64
+ });
65
+
66
+ await handleAdd(ctx, undefined, 'project/tagged', {
67
+ content: 'Tagged memory',
68
+ tags: ['foo', 'bar'],
69
+ });
70
+
71
+ expect(capture.getOutput()).toContain('Added memory');
72
+ expect(capture.getOutput()).toContain('project/tagged');
73
+ });
74
+
75
+ it('should pass expiresAt from options', async () => {
76
+ const capture = createCaptureStream();
77
+ const ctx = createMemoryCommandContext({
78
+ adapter: createMockMemoryCommandAdapter(),
79
+ storePath: tempDir,
80
+ stdout: capture.stream,
81
+ });
82
+
83
+ await handleAdd(ctx, undefined, 'project/expiring', {
84
+ content: 'Expires soon',
85
+ expiresAt: '2030-12-31T00:00:00Z',
86
+ });
87
+
88
+ expect(capture.getOutput()).toContain('Added memory');
89
+ });
90
+
91
+ it('should pass citations from options', async () => {
92
+ const capture = createCaptureStream();
93
+ const ctx = createMemoryCommandContext({
94
+ adapter: createMockMemoryCommandAdapter(),
95
+ storePath: tempDir,
96
+ stdout: capture.stream,
97
+ });
98
+
99
+ await handleAdd(ctx, undefined, 'project/cited', {
100
+ content: 'Cited memory',
101
+ citations: ['https://example.com/source'],
102
+ });
103
+
104
+ expect(capture.getOutput()).toContain('Added memory');
105
+ });
106
+
107
+ it('should throw CommanderError when store not found', async () => {
108
+ const failingFactory = (() => undefined) as unknown as AdapterFactory;
109
+
110
+ const stdin = new PassThrough();
111
+ const ctx = createMemoryCommandContext({
112
+ adapter: createMockMemoryCommandAdapter(),
113
+ storePath: tempDir,
114
+ stdin,
115
+ adapterFactory: failingFactory,
116
+ });
117
+
118
+ await expect(
119
+ handleAdd(ctx, 'nonexistent', 'project/notes', { content: 'test' })
120
+ ).rejects.toThrow(CommanderError);
121
+ });
122
+
123
+ it('should throw InvalidArgumentError for MISSING_CONTENT when no content provided', async () => {
124
+ const stdin = new PassThrough();
125
+ stdin.end(); // EOF with no data → empty content → MISSING_CONTENT
126
+ const ctx = createMemoryCommandContext({
127
+ adapter: createMockMemoryCommandAdapter(),
128
+ storePath: tempDir,
129
+ stdin,
130
+ });
131
+
132
+ await expect(handleAdd(ctx, undefined, 'project/notes', {})).rejects.toThrow(
133
+ InvalidArgumentError
134
+ );
135
+ });
136
+
137
+ it('should throw CommanderError when memory create fails due to missing category', async () => {
138
+ const ctx = createMemoryCommandContext({
139
+ adapter: createMockMemoryCommandAdapter({
140
+ categories: {
141
+ exists: async () => ok(false), // category absent → CATEGORY_NOT_FOUND
142
+ },
143
+ }),
144
+ storePath: tempDir,
145
+ });
146
+
147
+ await expect(
148
+ handleAdd(ctx, undefined, 'project/notes', { content: 'test' })
149
+ ).rejects.toThrow(CommanderError);
150
+ });
151
+
152
+ it('should use stdin when no content option is provided', async () => {
153
+ const stdin = new PassThrough();
154
+ stdin.end('Content from stdin');
155
+ const capture = createCaptureStream();
156
+ const ctx = createMemoryCommandContext({
157
+ adapter: createMockMemoryCommandAdapter(),
158
+ storePath: tempDir,
159
+ stdin,
160
+ stdout: capture.stream,
161
+ });
162
+
163
+ await handleAdd(ctx, undefined, 'project/stdin', {});
164
+
165
+ const out = capture.getOutput();
166
+ expect(out).toContain('Added memory');
167
+ expect(out).toContain('stdin');
168
+ });
169
+ });
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Memory add command implementation using Commander.js.
3
+ *
4
+ * Creates a new memory at the specified path with content from inline text,
5
+ * a file, or stdin.
6
+ *
7
+ * @example
8
+ * ```bash
9
+ * # Add memory with inline content
10
+ * cortex memory add project/tech-stack --content "Using TypeScript and Node.js"
11
+ *
12
+ * # Add memory from a file
13
+ * cortex memory add project/notes --file ./notes.md
14
+ *
15
+ * # Add memory from stdin
16
+ * echo "My notes" | cortex memory add project/notes
17
+ *
18
+ * # Add memory with tags and expiration
19
+ * cortex memory add project/temp --content "Temporary note" \
20
+ * --tags "temp,cleanup" --expires-at "2025-12-31T00:00:00Z"
21
+ * ```
22
+ */
23
+
24
+ import { Command } from '@commander-js/extra-typings';
25
+ import { throwCliError } from '../../errors.ts';
26
+ import { type CortexContext } from '@yeseh/cortex-core';
27
+ import { resolveInput as resolveCliContent } from '../../utils/input.ts';
28
+ import { parseExpiresAt, parseTags } from '../parsing.ts';
29
+ import { createCliCommandContext } from '../../context.ts';
30
+ import { serializeOutput, type OutputFormat } from '../../output.ts';
31
+
32
+ /** Options parsed by Commander for the add command */
33
+ export interface AddCommandOptions {
34
+ content?: string;
35
+ file?: string;
36
+ tags?: string[];
37
+ expiresAt?: string;
38
+ citations?: string[];
39
+ format?: string;
40
+ }
41
+ /**
42
+ * Handler for the memory add command.
43
+ * Exported for direct testing without Commander parsing.
44
+ *
45
+ * @param path - Memory path (e.g., "project/tech-stack")
46
+ * @param options - Command options from Commander
47
+ * @param storeName - Optional store name from parent command
48
+ * @param deps - Injectable dependencies for testing
49
+ */
50
+ export async function handleAdd(
51
+ ctx: CortexContext,
52
+ storeName: string | undefined,
53
+ path: string,
54
+ options: AddCommandOptions
55
+ ): Promise<void> {
56
+ const content = await resolveCliContent({
57
+ content: options.content,
58
+ filePath: options.file,
59
+ stream: ctx.stdin,
60
+ // `memory add` accepts stdin by default (when piped).
61
+ stdinRequested: options.content === undefined && options.file === undefined,
62
+ });
63
+
64
+ if (!content.ok()) {
65
+ throwCliError(content.error);
66
+ }
67
+
68
+ if (!content.value.content) {
69
+ throwCliError({
70
+ code: 'MISSING_CONTENT',
71
+ message: 'Memory content is required via --content, --file, or stdin.',
72
+ });
73
+ }
74
+
75
+ const { content: memoryContent, source } = content.value;
76
+ const tags = parseTags(options.tags);
77
+ const expiresAt = parseExpiresAt(options.expiresAt);
78
+ const citations = options.citations ?? [];
79
+
80
+ const storeResult = ctx.cortex.getStore(storeName ?? 'global');
81
+ if (!storeResult.ok()) {
82
+ throwCliError(storeResult.error);
83
+ }
84
+
85
+ const store = storeResult.value;
86
+ const timestamp = ctx.now() ?? new Date();
87
+ const memoryClient = store.getMemory(path);
88
+ const memoryResult = await memoryClient.create({
89
+ content: memoryContent!,
90
+ metadata: {
91
+ tags,
92
+ source,
93
+ createdAt: timestamp,
94
+ updatedAt: timestamp,
95
+ expiresAt,
96
+ citations,
97
+ },
98
+ });
99
+
100
+ if (!memoryResult.ok()) {
101
+ throwCliError(memoryResult.error);
102
+ }
103
+
104
+ const memory = memoryResult.value;
105
+ const out = ctx.stdout ?? process.stdout;
106
+
107
+ const rawFormat = options.format;
108
+ if (!rawFormat) {
109
+ out.write(`Added memory ${memory.path} (${source}).\n`);
110
+ } else {
111
+ const format = rawFormat as OutputFormat;
112
+ const serialized = serializeOutput(
113
+ {
114
+ kind: 'memory',
115
+ value: {
116
+ path: memory.path.toString(),
117
+ metadata: memory.metadata,
118
+ content: memory.content,
119
+ },
120
+ },
121
+ format
122
+ );
123
+ if (!serialized.ok()) {
124
+ throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
125
+ }
126
+ out.write(serialized.value + '\n');
127
+ }
128
+ }
129
+
130
+ /**
131
+ * The `memory add` subcommand.
132
+ *
133
+ * Creates a new memory at the specified path. Content can be provided via:
134
+ * - `--content` flag for inline text
135
+ * - `--file` flag to read from a file
136
+ * - stdin when piped
137
+ *
138
+ * The `--store` option is inherited from the parent `memory` command.
139
+ */
140
+ export const addCommand = new Command('add')
141
+ .description('Create a new memory')
142
+ .argument('<path>', 'Memory path (e.g., project/tech-stack)')
143
+ .option('-c, --content <text>', 'Memory content as inline text')
144
+ .option('-f, --file <filepath>', 'Read content from a file')
145
+ .option('-t, --tags <value...>', 'Tags (can be repeated or comma-separated)')
146
+ .option('-e, --expires-at <date>', 'Expiration date (ISO 8601)')
147
+ .option('--citation <value...>', 'Citation references (file paths or URLs)')
148
+ .option('-o, --format <format>', 'Output format (yaml, json, toon)')
149
+ .action(async (path, options, command) => {
150
+ const parentOpts = command.parent?.opts() as { store?: string } | undefined;
151
+ const context = await createCliCommandContext();
152
+ if (!context.ok()) {
153
+ throwCliError(context.error);
154
+ }
155
+
156
+ await handleAdd(context.value, parentOpts?.store, path, options);
157
+ });