@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.
- package/README.md +144 -0
- package/dist/category/commands/create.d.ts +44 -0
- package/dist/category/commands/create.d.ts.map +1 -0
- package/dist/category/commands/create.spec.d.ts +7 -0
- package/dist/category/commands/create.spec.d.ts.map +1 -0
- package/dist/category/index.d.ts +19 -0
- package/dist/category/index.d.ts.map +1 -0
- package/dist/commands/init.d.ts +58 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.spec.d.ts +2 -0
- package/dist/commands/init.spec.d.ts.map +1 -0
- package/dist/context.d.ts +18 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.spec.d.ts +2 -0
- package/dist/context.spec.d.ts.map +1 -0
- package/dist/create-cli-command.d.ts +23 -0
- package/dist/create-cli-command.d.ts.map +1 -0
- package/dist/create-cli-command.spec.d.ts +10 -0
- package/dist/create-cli-command.spec.d.ts.map +1 -0
- package/dist/errors.d.ts +57 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.spec.d.ts +2 -0
- package/dist/errors.spec.d.ts.map +1 -0
- package/dist/input.d.ts +42 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.spec.d.ts +2 -0
- package/dist/input.spec.d.ts.map +1 -0
- package/dist/memory/commands/add.d.ts +62 -0
- package/dist/memory/commands/add.d.ts.map +1 -0
- package/dist/memory/commands/add.spec.d.ts +7 -0
- package/dist/memory/commands/add.spec.d.ts.map +1 -0
- package/dist/memory/commands/definitions.spec.d.ts +10 -0
- package/dist/memory/commands/definitions.spec.d.ts.map +1 -0
- package/dist/memory/commands/handlers.spec.d.ts +2 -0
- package/dist/memory/commands/handlers.spec.d.ts.map +1 -0
- package/dist/memory/commands/list.d.ts +119 -0
- package/dist/memory/commands/list.d.ts.map +1 -0
- package/dist/memory/commands/list.spec.d.ts +2 -0
- package/dist/memory/commands/list.spec.d.ts.map +1 -0
- package/dist/memory/commands/move.d.ts +42 -0
- package/dist/memory/commands/move.d.ts.map +1 -0
- package/dist/memory/commands/move.spec.d.ts +2 -0
- package/dist/memory/commands/move.spec.d.ts.map +1 -0
- package/dist/memory/commands/remove.d.ts +41 -0
- package/dist/memory/commands/remove.d.ts.map +1 -0
- package/dist/memory/commands/remove.spec.d.ts +2 -0
- package/dist/memory/commands/remove.spec.d.ts.map +1 -0
- package/dist/memory/commands/show.d.ts +81 -0
- package/dist/memory/commands/show.d.ts.map +1 -0
- package/dist/memory/commands/show.spec.d.ts +2 -0
- package/dist/memory/commands/show.spec.d.ts.map +1 -0
- package/dist/memory/commands/test-helpers.spec.d.ts +19 -0
- package/dist/memory/commands/test-helpers.spec.d.ts.map +1 -0
- package/dist/memory/commands/update.d.ts +73 -0
- package/dist/memory/commands/update.d.ts.map +1 -0
- package/dist/memory/commands/update.spec.d.ts +2 -0
- package/dist/memory/commands/update.spec.d.ts.map +1 -0
- package/dist/memory/index.d.ts +29 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.spec.d.ts +10 -0
- package/dist/memory/index.spec.d.ts.map +1 -0
- package/dist/memory/parsing.d.ts +3 -0
- package/dist/memory/parsing.d.ts.map +1 -0
- package/dist/memory/parsing.spec.d.ts +7 -0
- package/dist/memory/parsing.spec.d.ts.map +1 -0
- package/dist/output.d.ts +87 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.spec.d.ts +2 -0
- package/dist/output.spec.d.ts.map +1 -0
- package/dist/paths.d.ts +27 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.spec.d.ts +7 -0
- package/dist/paths.spec.d.ts.map +1 -0
- package/dist/program.d.ts +41 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.spec.d.ts +11 -0
- package/dist/program.spec.d.ts.map +1 -0
- package/dist/run.d.ts +7 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.spec.d.ts +12 -0
- package/dist/run.spec.d.ts.map +1 -0
- package/dist/store/commands/add.d.ts +73 -0
- package/dist/store/commands/add.d.ts.map +1 -0
- package/dist/store/commands/add.spec.d.ts +17 -0
- package/dist/store/commands/add.spec.d.ts.map +1 -0
- package/dist/store/commands/init.d.ts +75 -0
- package/dist/store/commands/init.d.ts.map +1 -0
- package/dist/store/commands/init.spec.d.ts +7 -0
- package/dist/store/commands/init.spec.d.ts.map +1 -0
- package/dist/store/commands/list.d.ts +62 -0
- package/dist/store/commands/list.d.ts.map +1 -0
- package/dist/store/commands/list.spec.d.ts +7 -0
- package/dist/store/commands/list.spec.d.ts.map +1 -0
- package/dist/store/commands/prune.d.ts +92 -0
- package/dist/store/commands/prune.d.ts.map +1 -0
- package/dist/store/commands/prune.spec.d.ts +7 -0
- package/dist/store/commands/prune.spec.d.ts.map +1 -0
- package/dist/store/commands/reindexs.d.ts +54 -0
- package/dist/store/commands/reindexs.d.ts.map +1 -0
- package/dist/store/commands/reindexs.spec.d.ts +7 -0
- package/dist/store/commands/reindexs.spec.d.ts.map +1 -0
- package/dist/store/commands/remove.d.ts +63 -0
- package/dist/store/commands/remove.d.ts.map +1 -0
- package/dist/store/commands/remove.spec.d.ts +17 -0
- package/dist/store/commands/remove.spec.d.ts.map +1 -0
- package/dist/store/index.d.ts +32 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.spec.d.ts +9 -0
- package/dist/store/index.spec.d.ts.map +1 -0
- package/dist/store/utils/resolve-store-name.d.ts +30 -0
- package/dist/store/utils/resolve-store-name.d.ts.map +1 -0
- package/dist/store/utils/resolve-store-name.spec.d.ts +2 -0
- package/dist/store/utils/resolve-store-name.spec.d.ts.map +1 -0
- package/dist/test-helpers.spec.d.ts +224 -0
- package/dist/test-helpers.spec.d.ts.map +1 -0
- package/dist/tests/cli.integration.spec.d.ts +11 -0
- package/dist/tests/cli.integration.spec.d.ts.map +1 -0
- package/dist/toon.d.ts +197 -0
- package/dist/toon.d.ts.map +1 -0
- package/dist/toon.spec.d.ts +9 -0
- package/dist/toon.spec.d.ts.map +1 -0
- package/dist/utils/git.d.ts +20 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.spec.d.ts +7 -0
- package/dist/utils/git.spec.d.ts.map +1 -0
- package/package.json +45 -0
- package/src/category/commands/create.spec.ts +139 -0
- package/src/category/commands/create.ts +115 -0
- package/src/category/index.ts +24 -0
- package/src/commands/init.spec.ts +203 -0
- package/src/commands/init.ts +301 -0
- package/src/context.spec.ts +60 -0
- package/src/context.ts +175 -0
- package/src/errors.spec.ts +264 -0
- package/src/errors.ts +105 -0
- package/src/memory/commands/add.spec.ts +169 -0
- package/src/memory/commands/add.ts +157 -0
- package/src/memory/commands/definitions.spec.ts +80 -0
- package/src/memory/commands/list.spec.ts +123 -0
- package/src/memory/commands/list.ts +268 -0
- package/src/memory/commands/move.spec.ts +85 -0
- package/src/memory/commands/move.ts +115 -0
- package/src/memory/commands/remove.spec.ts +79 -0
- package/src/memory/commands/remove.ts +104 -0
- package/src/memory/commands/show.spec.ts +71 -0
- package/src/memory/commands/show.ts +164 -0
- package/src/memory/commands/test-helpers.spec.ts +127 -0
- package/src/memory/commands/update.spec.ts +86 -0
- package/src/memory/commands/update.ts +229 -0
- package/src/memory/index.spec.ts +59 -0
- package/src/memory/index.ts +44 -0
- package/src/memory/parsing.spec.ts +105 -0
- package/src/memory/parsing.ts +22 -0
- package/src/observability.spec.ts +139 -0
- package/src/observability.ts +63 -0
- package/src/output.spec.ts +835 -0
- package/src/output.ts +119 -0
- package/src/program.spec.ts +46 -0
- package/src/program.ts +75 -0
- package/src/run.spec.ts +31 -0
- package/src/run.ts +9 -0
- package/src/store/commands/add.spec.ts +131 -0
- package/src/store/commands/add.ts +231 -0
- package/src/store/commands/init.spec.ts +236 -0
- package/src/store/commands/init.ts +256 -0
- package/src/store/commands/list.spec.ts +175 -0
- package/src/store/commands/list.ts +102 -0
- package/src/store/commands/prune.spec.ts +120 -0
- package/src/store/commands/prune.ts +152 -0
- package/src/store/commands/reindexs.spec.ts +94 -0
- package/src/store/commands/reindexs.ts +96 -0
- package/src/store/commands/remove.spec.ts +97 -0
- package/src/store/commands/remove.ts +189 -0
- package/src/store/index.spec.ts +60 -0
- package/src/store/index.ts +49 -0
- package/src/store/utils/resolve-store-name.spec.ts +62 -0
- package/src/store/utils/resolve-store-name.ts +79 -0
- package/src/test-helpers.spec.ts +430 -0
- package/src/tests/cli.integration.spec.ts +1170 -0
- package/src/toon.spec.ts +183 -0
- package/src/toon.ts +462 -0
- package/src/utils/git.spec.ts +95 -0
- package/src/utils/git.ts +51 -0
- package/src/utils/input.spec.ts +326 -0
- package/src/utils/input.ts +145 -0
- package/src/utils/paths.spec.ts +235 -0
- package/src/utils/paths.ts +75 -0
- package/src/utils/prompts.spec.ts +23 -0
- 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
|
+
});
|