@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,301 @@
1
+ /**
2
+ * Init command for initializing the global cortex configuration store.
3
+ *
4
+ * Creates the global config store at ~/.config/cortex/ with:
5
+ * - config.yaml: Global configuration with default settings
6
+ * - stores.yaml: Store registry with a 'global' store pointing to the memory directory
7
+ * - memory/: Default store with 'global' and 'projects' categories
8
+ *
9
+ * @example
10
+ * ```bash
11
+ * # Initialize global cortex configuration
12
+ * cortex init
13
+ *
14
+ * # Reinitialize even if already initialized
15
+ * cortex init --force
16
+ * ```
17
+ */
18
+
19
+ import { homedir } from 'node:os';
20
+ import { resolve } from 'node:path';
21
+ import { Command } from '@commander-js/extra-typings';
22
+ import { throwCliError } from '../errors.ts';
23
+ import {
24
+ serializeOutput,
25
+ type OutputFormat,
26
+ type OutputInit,
27
+ type OutputPayload,
28
+ } from '../output.ts';
29
+ import { defaultGlobalStoreCategories } from '@yeseh/cortex-core/category';
30
+ import {
31
+ configCategoriesToStoreCategories,
32
+ getDefaultSettings,
33
+ type CortexConfig,
34
+ type CortexContext,
35
+ type StoreData,
36
+ } from '@yeseh/cortex-core';
37
+ import { createCliCommandContext } from '../context.ts';
38
+ import { isTTY, defaultPromptDeps, type PromptDeps } from '../utils/prompts.ts';
39
+
40
+ /**
41
+ * Options for the init command.
42
+ */
43
+ export interface InitCommandOptions {
44
+ /** Reinitialize even if already initialized */
45
+ force?: boolean;
46
+ /** Output format (yaml, json, toon) */
47
+ format?: string;
48
+ }
49
+
50
+ /**
51
+ * The `init` command for initializing the global cortex configuration.
52
+ *
53
+ * Creates the global config store at ~/.config/cortex/ with default settings
54
+ * and store registry.
55
+ *
56
+ * @example
57
+ * ```bash
58
+ * cortex init # Initialize global config
59
+ * cortex init --force # Reinitialize even if exists
60
+ * ```
61
+ */
62
+ export const initCommand = new Command('init')
63
+ .description('Initialize global cortex configuration')
64
+ .option('-F, --force', 'Reinitialize even if already initialized')
65
+ .option('-o, --format <format>', 'Output format (yaml, json, toon)', 'yaml')
66
+ .action(async (options) => {
67
+ const context = await createCliCommandContext();
68
+ if (!context.ok()) {
69
+ throwCliError({
70
+ code: 'CONTEXT_CREATION_FAILED',
71
+ message: `Failed to create command context: ${context.error.message}`,
72
+ });
73
+ }
74
+ await handleInit(context.value, options);
75
+ });
76
+
77
+ /**
78
+ * Prompts the user to confirm or change the resolved global store path and name.
79
+ *
80
+ * Returns `resolved` unchanged when stdin is not a TTY.
81
+ *
82
+ * @param ctx - Cortex context used for TTY detection via `ctx.stdin`
83
+ * @param resolved - Default store name and path to present as suggestions
84
+ * @param promptDeps - Injectable prompt functions for testability
85
+ * @returns Finalized store name and path (either from prompts or from `resolved`)
86
+ */
87
+
88
+ /**
89
+ * Resolve a user-supplied path:
90
+ * - expands a leading '~' to the user's home directory
91
+ * - resolves relative paths to an absolute path
92
+ */
93
+ function resolveUserPath(userPath: string): string {
94
+ if (!userPath) return userPath;
95
+
96
+ let expanded = userPath;
97
+ if (userPath.startsWith('~')) {
98
+ expanded = resolve(homedir(), userPath.slice(1));
99
+ }
100
+
101
+ return resolve(expanded);
102
+ }
103
+
104
+ function normalizeStoreName(input: string, fallback: string): string {
105
+ const trimmed = input.trim();
106
+ if (!trimmed) return fallback;
107
+
108
+ const slug = trimmed
109
+ .toLowerCase()
110
+ .replace(/[^a-z0-9_-]+/gi, '-')
111
+ .replace(/^-+|-+$/g, '');
112
+
113
+ return slug || fallback;
114
+ }
115
+
116
+ function normalizeStorePath(input: string, fallback: string): string {
117
+ const trimmed = input.trim();
118
+ const base = trimmed || fallback;
119
+ return resolveUserPath(base);
120
+ }
121
+
122
+ async function promptInitOptions(
123
+ ctx: CortexContext,
124
+ resolved: { storeName: string; storePath: string },
125
+ promptDeps: PromptDeps,
126
+ ): Promise<{ storeName: string; storePath: string }> {
127
+ if (!isTTY(ctx.stdin)) {
128
+ return {
129
+ storeName: normalizeStoreName(resolved.storeName, resolved.storeName),
130
+ storePath: normalizeStorePath(resolved.storePath, resolved.storePath),
131
+ };
132
+ }
133
+
134
+ const storePathInput = await promptDeps.input({
135
+ message: 'Global store path:',
136
+ default: resolved.storePath,
137
+ });
138
+ const storeNameInput = await promptDeps.input({
139
+ message: 'Global store name:',
140
+ default: resolved.storeName,
141
+ });
142
+
143
+ const storeName = normalizeStoreName(storeNameInput, resolved.storeName);
144
+ const storePath = normalizeStorePath(storePathInput, resolved.storePath);
145
+ return { storePath, storeName };
146
+ }
147
+
148
+ // TODO: We should move this logic into the core package as a helper function, and just call it from the CLI command handler.
149
+ // Use the ConfigAdapter to initialize the config store and write the default config, instead of manually writing files here. This way we can reuse the same initialization logic in other contexts (e.g. programmatic setup, tests).
150
+
151
+ /**
152
+ * Handles the init command execution.
153
+ *
154
+ * This function:
155
+ * 1. When stdin is a TTY, prompts for global store path and name confirmation
156
+ * 2. Initializes the global cortex config store
157
+ * 3. Creates default categories
158
+ * 4. Outputs the result
159
+ *
160
+ * Interactive mode activates automatically when `ctx.stdin.isTTY === true`.
161
+ * In non-TTY environments (CI, pipes) the defaults are used without prompting.
162
+ *
163
+ * @param ctx - The Cortex context (stdin TTY state used for interactive detection)
164
+ * @param options - Command options (force, format)
165
+ * @param promptDeps - Injectable prompt functions; defaults to real `@inquirer/prompts` functions
166
+ * @throws {InvalidArgumentError} When arguments are invalid
167
+ * @throws {CommanderError} When initialization fails
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * // Non-interactive (CI / scripts):
172
+ * await handleInit(ctx, { format: 'yaml' });
173
+ *
174
+ * // Force interactive with test stubs:
175
+ * const stubs: PromptDeps = {
176
+ * input: async ({ default: d }) => d ?? 'test',
177
+ * confirm: async () => true,
178
+ * };
179
+ * (ctx.stdin as any).isTTY = true;
180
+ * await handleInit(ctx, {}, stubs);
181
+ * ```
182
+ */
183
+ export async function handleInit(
184
+ ctx: CortexContext,
185
+ options: InitCommandOptions = {},
186
+ promptDeps: PromptDeps = defaultPromptDeps,
187
+ ): Promise<void> {
188
+ const cortexConfigDir = resolve(homedir(), '.config', 'cortex');
189
+ const globalStorePath = resolve(cortexConfigDir, 'memory');
190
+
191
+ const resolved = await promptInitOptions(
192
+ ctx,
193
+ { storeName: 'global', storePath: globalStorePath },
194
+ promptDeps,
195
+ );
196
+ const finalStorePath = resolved.storePath;
197
+ const finalStoreName = resolved.storeName;
198
+
199
+ await initializeConfigAdapter(ctx);
200
+ await ensureNotInitialized(ctx, finalStoreName, finalStorePath, options.force);
201
+ await createGlobalStore(ctx, finalStoreName, finalStorePath);
202
+
203
+ // Build output
204
+ const output: OutputPayload = {
205
+ kind: 'init',
206
+ value: formatInit(finalStorePath, Object.keys(defaultGlobalStoreCategories)),
207
+ };
208
+
209
+ // Output result
210
+ const format: OutputFormat = (options.format as OutputFormat) ?? 'yaml';
211
+ const outputSerialized = serializeOrThrow(output, format);
212
+
213
+ const out = ctx.stdout ?? process.stdout;
214
+ out.write(outputSerialized.value + '\n');
215
+ }
216
+
217
+ const ensureNotInitialized = async (
218
+ ctx: CortexContext,
219
+ storeName: string,
220
+ globalStorePath: string,
221
+ force = false,
222
+ ): Promise<void> => {
223
+ if (force) {
224
+ return;
225
+ }
226
+
227
+ const existingStoreResult = await ctx.config.getStore(storeName);
228
+ if (!existingStoreResult.ok()) {
229
+ throwCliError(existingStoreResult.error);
230
+ }
231
+
232
+ if (!existingStoreResult.value) {
233
+ return;
234
+ }
235
+
236
+ throwCliError({
237
+ code: 'ALREADY_INITIALIZED',
238
+ message: `Global config store already exists at ${globalStorePath}. Use --force to reinitialize.`,
239
+ });
240
+ };
241
+
242
+ const initializeConfigAdapter = async (ctx: CortexContext): Promise<void> => {
243
+ const config: CortexConfig = {
244
+ settings: getDefaultSettings(),
245
+ stores: {},
246
+ };
247
+
248
+ const initConfigResult = await ctx.config.initializeConfig(config);
249
+ if (!initConfigResult.ok()) {
250
+ throwCliError(initConfigResult.error);
251
+ }
252
+ };
253
+
254
+ const serializeOrThrow = <T extends OutputPayload>(value: T, format: OutputFormat) => {
255
+ const serialized = serializeOutput(value, format);
256
+ if (!serialized.ok()) {
257
+ throwCliError(serialized.error);
258
+ }
259
+ return serialized;
260
+ };
261
+
262
+ const createGlobalStore = async (
263
+ ctx: CortexContext,
264
+ storeName: string,
265
+ globalStorePath: string,
266
+ ): Promise<void> => {
267
+ const existingStoreResult = await ctx.config.getStore(storeName);
268
+ if (!existingStoreResult.ok()) {
269
+ throwCliError(existingStoreResult.error);
270
+ }
271
+
272
+ if (existingStoreResult.value) {
273
+ return;
274
+ }
275
+
276
+ const templateCategories = configCategoriesToStoreCategories(
277
+ defaultGlobalStoreCategories,
278
+ ).unwrap(); // defaultGlobalStoreCategories is valid, unwrap is safe here
279
+
280
+ const globalStoreData: StoreData = {
281
+ kind: 'filesystem',
282
+ categoryMode: 'free',
283
+ description:
284
+ 'Global memory store for Cortex. Use for cross-project memories and configurations.',
285
+ categories: templateCategories,
286
+ properties: {
287
+ path: globalStorePath,
288
+ },
289
+ };
290
+
291
+ const saveStoreResult = await ctx.config.saveStore(storeName, globalStoreData);
292
+ if (!saveStoreResult.ok()) {
293
+ throwCliError(saveStoreResult.error);
294
+ }
295
+ };
296
+
297
+ const formatInit = (path: string, categories: readonly string[]): OutputInit => ({
298
+ path,
299
+ categories: [...categories],
300
+ });
301
+
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Unit tests for create-cli-command.ts — validateStorePath.
3
+ *
4
+ * Verifies that absolute paths pass validation and relative paths
5
+ * produce a typed INVALID_STORE_PATH error result.
6
+ *
7
+ * @module cli/create-cli-command.spec
8
+ */
9
+
10
+ import { describe, it, expect } from 'bun:test';
11
+
12
+ import { validateStorePath } from './context.ts';
13
+
14
+ describe('validateStorePath', () => {
15
+ it('should return ok for a Unix absolute path', () => {
16
+ const result = validateStorePath('/home/user/.cortex/memory', 'my-store');
17
+ expect(result.ok()).toBe(true);
18
+ });
19
+
20
+ it('should return ok for a Windows-style absolute path', () => {
21
+ // isAbsolute on Linux treats "C:\\..." as relative, but a UNC-style
22
+ // or drive-letter path on Windows would be absolute. We test with a
23
+ // typical Unix absolute path variant to keep cross-platform coverage.
24
+ const result = validateStorePath('/var/cortex/stores/global', 'global');
25
+ expect(result.ok()).toBe(true);
26
+ });
27
+
28
+ it('should return INVALID_STORE_PATH for a relative path', () => {
29
+ const result = validateStorePath('relative/path/to/store', 'my-store');
30
+ expect(result.ok()).toBe(false);
31
+ if (!result.ok()) {
32
+ expect(result.error.code).toBe('INVALID_STORE_PATH');
33
+ }
34
+ });
35
+
36
+ it('should return INVALID_STORE_PATH for a path starting with ./', () => {
37
+ const result = validateStorePath('./local/store', 'my-store');
38
+ expect(result.ok()).toBe(false);
39
+ if (!result.ok()) {
40
+ expect(result.error.code).toBe('INVALID_STORE_PATH');
41
+ }
42
+ });
43
+
44
+ it('should include the store name in the error message', () => {
45
+ const storeName = 'my-named-store';
46
+ const result = validateStorePath('not/absolute', storeName);
47
+ expect(result.ok()).toBe(false);
48
+ if (!result.ok()) {
49
+ expect(result.error.message).toContain(storeName);
50
+ }
51
+ });
52
+
53
+ it('should return INVALID_STORE_PATH for empty string', () => {
54
+ const result = validateStorePath('', 'some-store');
55
+ expect(result.ok()).toBe(false);
56
+ if (!result.ok()) {
57
+ expect(result.error.code).toBe('INVALID_STORE_PATH');
58
+ }
59
+ });
60
+ });
package/src/context.ts ADDED
@@ -0,0 +1,175 @@
1
+ import {
2
+ Cortex,
3
+ err,
4
+ getDefaultSettings,
5
+ ok,
6
+ type ConfigValidationError,
7
+ type CortexContext,
8
+ type Result,
9
+ } from '@yeseh/cortex-core';
10
+ import { homedir } from 'os';
11
+ import { isAbsolute, resolve } from 'path';
12
+ import { FilesystemStorageAdapter, FilesystemConfigAdapter } from '@yeseh/cortex-storage-fs';
13
+ import { stdin, stdout } from 'process';
14
+ import { createCliLogger } from './observability.ts';
15
+
16
+ // TODO: Much of this module should move to the FS adapter, since it's all about loading config from the filesystem. The CLI command handlers should just call into the core module to load config and create a context, rather than having all the logic here.
17
+
18
+ const makeAbsolute = (pathStr: string): string => {
19
+ if (pathStr.startsWith('~')) {
20
+ return resolve(homedir(), pathStr.slice(1).replace(/^[/\\]/, ''));
21
+ }
22
+ return isAbsolute(pathStr) ? pathStr : resolve(pathStr);
23
+ };
24
+
25
+ export const validateStorePath = (
26
+ storePath: string,
27
+ storeName: string,
28
+ ): Result<void, ConfigValidationError> => {
29
+ if (!isAbsolute(storePath)) {
30
+ return err({
31
+ code: 'INVALID_STORE_PATH',
32
+ message:
33
+ `Store '${storeName}' path must be absolute. Got: ${storePath}. ` +
34
+ "Use an absolute path like '/home/user/.cortex/memory'.",
35
+ store: storeName,
36
+ });
37
+ }
38
+ return ok(undefined);
39
+ };
40
+
41
+ export interface ConfigLoadOptions {
42
+ cwd?: string;
43
+ globalConfigPath?: string;
44
+ localConfigPath?: string;
45
+ }
46
+
47
+ export interface CliContextOptions {
48
+ configDir?: string;
49
+ configCwd?: string;
50
+ }
51
+
52
+ export interface CliConfigContext {
53
+ configAdapter: FilesystemConfigAdapter;
54
+ stores: Record<string, any>;
55
+ settings: ReturnType<typeof getDefaultSettings>;
56
+ effectiveCwd: string;
57
+ }
58
+
59
+ export const createCliConfigAdapter = (configPath: string): FilesystemConfigAdapter => {
60
+ return new FilesystemConfigAdapter(configPath);
61
+ };
62
+
63
+ export const createCliAdapterFactory = (configAdapter: FilesystemConfigAdapter) => {
64
+ return (storeName: string) => {
65
+ const stores = configAdapter.stores!;
66
+ const storeEntry = stores[storeName];
67
+ if (!storeEntry) {
68
+ throw new Error(
69
+ `Store '${storeName}' not found. Available stores: ${Object.keys(stores).join(', ')}`,
70
+ );
71
+ }
72
+
73
+ const storePath = storeEntry.properties?.path as string | undefined;
74
+ if (!storePath) {
75
+ throw new Error(`Store '${storeName}' has no path configured in properties.`);
76
+ }
77
+
78
+ return new FilesystemStorageAdapter(configAdapter, {
79
+ rootDirectory: storePath,
80
+ });
81
+ };
82
+ };
83
+
84
+ export const createCliConfigContext = async (
85
+ options: CliContextOptions = {},
86
+ ): Promise<Result<CliConfigContext, any>> => {
87
+ const envConfigPath = process.env.CORTEX_CONFIG;
88
+ const envConfigDir = process.env.CORTEX_CONFIG_DIR;
89
+
90
+ const explicitConfigPath =
91
+ typeof envConfigPath === 'string' && envConfigPath.length > 0
92
+ ? makeAbsolute(envConfigPath)
93
+ : undefined;
94
+
95
+ const dir = options.configDir ?? envConfigDir ?? resolve(homedir(), '.config', 'cortex');
96
+ const absoluteDir = makeAbsolute(dir);
97
+ const configPath = explicitConfigPath ?? resolve(absoluteDir, 'config.yaml');
98
+
99
+ const envConfigCwd = process.env.CORTEX_CONFIG_CWD;
100
+ const effectiveCwd =
101
+ options.configCwd ??
102
+ (typeof envConfigCwd === 'string' && envConfigCwd.length > 0
103
+ ? envConfigCwd
104
+ : process.cwd());
105
+
106
+ const configAdapter = createCliConfigAdapter(configPath);
107
+ const initResult = await configAdapter.initializeConfig();
108
+ if (!initResult.ok()) {
109
+ return initResult;
110
+ }
111
+
112
+ const settingsResult = await configAdapter.getSettings();
113
+ if (!settingsResult.ok()) {
114
+ return settingsResult;
115
+ }
116
+
117
+ const storesResult = await configAdapter.getStores();
118
+ if (!storesResult.ok()) {
119
+ return storesResult;
120
+ }
121
+
122
+ return ok({
123
+ configAdapter,
124
+ settings: settingsResult.value,
125
+ stores: storesResult.value,
126
+ effectiveCwd,
127
+ });
128
+ };
129
+
130
+ /* Creates a CortexContext from the CLI environment, including loading configuration and setting up dependencies.
131
+ * This function is used to create a context object that can be injected into command handlers for consistent access to the Cortex client and other utilities.
132
+ */
133
+ export const createCliCommandContext = async (
134
+ configDir?: string,
135
+ ): Promise<Result<CortexContext, any>> => {
136
+ try {
137
+ const configContextResult = await createCliConfigContext({
138
+ configDir,
139
+ });
140
+ if (!configContextResult.ok()) {
141
+ return configContextResult;
142
+ }
143
+
144
+ const { configAdapter, settings, stores } = configContextResult.value;
145
+ const adapterFactory = createCliAdapterFactory(configAdapter);
146
+
147
+ const now = () => new Date();
148
+ const cortex = Cortex.init({
149
+ settings,
150
+ stores,
151
+ adapterFactory,
152
+ });
153
+
154
+ const logger = createCliLogger();
155
+
156
+ const context: CortexContext = {
157
+ config: configAdapter,
158
+ settings: settings ?? getDefaultSettings(),
159
+ stores: stores ?? {},
160
+ cortex,
161
+ now,
162
+ stdin,
163
+ stdout,
164
+ logger,
165
+ };
166
+
167
+ return ok(context);
168
+ }
169
+ catch (error) {
170
+ return err({
171
+ code: 'CONTEXT_CREATION_FAILED',
172
+ message: `Unexpected error creating CLI command context: ${error instanceof Error ? error.message : String(error)}`,
173
+ });
174
+ }
175
+ };