@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,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
|
+
};
|