@yeseh/cortex-cli 0.6.8 → 0.6.10

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 (74) hide show
  1. package/dist/program.js +1538 -5
  2. package/dist/program.js.map +32 -3
  3. package/dist/run.d.ts +0 -1
  4. package/dist/run.d.ts.map +1 -1
  5. package/dist/run.js +3 -4
  6. package/dist/run.js.map +3 -3
  7. package/package.json +4 -6
  8. package/dist/chunk-dsfj4baj.js +0 -1543
  9. package/dist/chunk-dsfj4baj.js.map +0 -38
  10. package/src/category/commands/create.spec.ts +0 -139
  11. package/src/category/commands/create.ts +0 -119
  12. package/src/category/index.ts +0 -24
  13. package/src/commands/init.spec.ts +0 -203
  14. package/src/commands/init.ts +0 -301
  15. package/src/context.spec.ts +0 -60
  16. package/src/context.ts +0 -170
  17. package/src/errors.spec.ts +0 -264
  18. package/src/errors.ts +0 -105
  19. package/src/memory/commands/add.spec.ts +0 -169
  20. package/src/memory/commands/add.ts +0 -158
  21. package/src/memory/commands/definitions.spec.ts +0 -80
  22. package/src/memory/commands/list.spec.ts +0 -123
  23. package/src/memory/commands/list.ts +0 -269
  24. package/src/memory/commands/move.spec.ts +0 -85
  25. package/src/memory/commands/move.ts +0 -119
  26. package/src/memory/commands/remove.spec.ts +0 -79
  27. package/src/memory/commands/remove.ts +0 -108
  28. package/src/memory/commands/show.spec.ts +0 -71
  29. package/src/memory/commands/show.ts +0 -165
  30. package/src/memory/commands/test-helpers.spec.ts +0 -127
  31. package/src/memory/commands/update.spec.ts +0 -86
  32. package/src/memory/commands/update.ts +0 -230
  33. package/src/memory/index.spec.ts +0 -59
  34. package/src/memory/index.ts +0 -44
  35. package/src/memory/parsing.spec.ts +0 -105
  36. package/src/memory/parsing.ts +0 -22
  37. package/src/observability.spec.ts +0 -126
  38. package/src/observability.ts +0 -82
  39. package/src/output.spec.ts +0 -835
  40. package/src/output.ts +0 -119
  41. package/src/program.spec.ts +0 -46
  42. package/src/program.ts +0 -75
  43. package/src/run.spec.ts +0 -31
  44. package/src/run.ts +0 -9
  45. package/src/store/commands/add.spec.ts +0 -131
  46. package/src/store/commands/add.ts +0 -231
  47. package/src/store/commands/init.spec.ts +0 -220
  48. package/src/store/commands/init.ts +0 -272
  49. package/src/store/commands/list.spec.ts +0 -175
  50. package/src/store/commands/list.ts +0 -102
  51. package/src/store/commands/prune.spec.ts +0 -120
  52. package/src/store/commands/prune.ts +0 -152
  53. package/src/store/commands/reindexs.spec.ts +0 -94
  54. package/src/store/commands/reindexs.ts +0 -97
  55. package/src/store/commands/remove.spec.ts +0 -97
  56. package/src/store/commands/remove.ts +0 -189
  57. package/src/store/index.spec.ts +0 -60
  58. package/src/store/index.ts +0 -49
  59. package/src/store/utils/resolve-store-name.spec.ts +0 -62
  60. package/src/store/utils/resolve-store-name.ts +0 -79
  61. package/src/test-helpers.spec.ts +0 -430
  62. package/src/tests/cli.integration.spec.ts +0 -1306
  63. package/src/toon.spec.ts +0 -183
  64. package/src/toon.ts +0 -462
  65. package/src/utils/git.spec.ts +0 -95
  66. package/src/utils/git.ts +0 -51
  67. package/src/utils/input.spec.ts +0 -326
  68. package/src/utils/input.ts +0 -150
  69. package/src/utils/paths.spec.ts +0 -235
  70. package/src/utils/paths.ts +0 -75
  71. package/src/utils/prompts.spec.ts +0 -23
  72. package/src/utils/prompts.ts +0 -88
  73. package/src/utils/resolve-default-store.spec.ts +0 -135
  74. package/src/utils/resolve-default-store.ts +0 -74
@@ -1,119 +0,0 @@
1
- /**
2
- * Category create command.
3
- *
4
- * Creates a category at the specified path, including any missing ancestors.
5
- *
6
- * @example
7
- * ```bash
8
- * # Create a category in default store
9
- * cortex category create standards/typescript
10
- *
11
- * # Create a category in a specific store
12
- * cortex category --store my-store create standards/typescript
13
- *
14
- * # Serialize output as JSON
15
- * cortex category create standards --format json
16
- * ```
17
- */
18
-
19
- import { Command } from '@commander-js/extra-typings';
20
- import { type CortexContext, type Result } from '@yeseh/cortex-core';
21
-
22
- import { createCliCommandContext } from '../../context.ts';
23
- import { throwCliError } from '../../errors.ts';
24
- import { serializeOutput, type OutputFormat } from '../../output.ts';
25
- import { resolveDefaultStore } from '../../utils/resolve-default-store.ts';
26
-
27
- /** Options parsed by Commander for the create command */
28
- export interface CreateCommandOptions {
29
- description?: string;
30
- format?: string;
31
- }
32
-
33
- /**
34
- * Writes command output to stdout.
35
- *
36
- * @param payload - Category creation result payload
37
- * @param options - Command options
38
- * @param stdout - Output stream
39
- */
40
- function writeCreateOutput(
41
- payload: { path: string; created: boolean },
42
- options: CreateCommandOptions,
43
- stdout: NodeJS.WritableStream
44
- ): void {
45
- const rawFormat = options.format;
46
-
47
- if (!rawFormat) {
48
- const verb = payload.created ? 'Created' : 'Category already exists:';
49
- stdout.write(`${verb} ${payload.path}\n`);
50
- return;
51
- }
52
-
53
- const serialized = serializeOutput(
54
- { kind: 'created-category', value: payload },
55
- rawFormat as OutputFormat
56
- );
57
- if (!serialized.ok()) {
58
- throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
59
- }
60
- stdout.write(serialized.value + '\n');
61
- }
62
-
63
- /**
64
- * Unwrap a Result and throw a mapped CLI error on failure.
65
- *
66
- * @param result - Result value
67
- * @returns Unwrapped value
68
- */
69
- function unwrapOrThrow<T, E extends { code: string; message: string }>(result: Result<T, E>): T {
70
- if (!result.ok()) {
71
- throwCliError(result.error);
72
- }
73
-
74
- return result.value;
75
- }
76
-
77
- /**
78
- * Handler for the category create command.
79
- * Exported for direct testing without Commander parsing.
80
- *
81
- * @param ctx - CLI execution context
82
- * @param storeName - Optional store name from parent command
83
- * @param path - Category path (e.g., "standards/typescript")
84
- * @param options - Command options from Commander
85
- */
86
- export async function handleCreate(
87
- ctx: CortexContext,
88
- storeName: string | undefined,
89
- path: string,
90
- options: CreateCommandOptions = {}
91
- ): Promise<void> {
92
- const store = unwrapOrThrow(ctx.cortex.getStore(resolveDefaultStore(ctx, storeName)));
93
- const root = unwrapOrThrow(store.root());
94
- const category = unwrapOrThrow(root.getCategory(path));
95
- const result = unwrapOrThrow(await category.create());
96
-
97
- const out = ctx.stdout ?? process.stdout;
98
- writeCreateOutput(result, options, out);
99
- }
100
-
101
- /**
102
- * The `category create` subcommand.
103
- *
104
- * Creates a category and any missing ancestors.
105
- */
106
- export const createCommand = new Command('create')
107
- .description('Create a category (and any missing ancestors)')
108
- .argument('<path>', 'Category path (e.g., standards/typescript)')
109
- .option('-d, --description <text>', 'Optional description for the category')
110
- .option('-o, --format <format>', 'Output format (yaml, json, toon)')
111
- .action(async (path, options, command) => {
112
- const parentOpts = command.parent?.opts() as { store?: string } | undefined;
113
- const context = await createCliCommandContext();
114
- if (!context.ok()) {
115
- throwCliError(context.error);
116
- }
117
-
118
- await handleCreate(context.value, parentOpts?.store, path, options);
119
- });
@@ -1,24 +0,0 @@
1
- /**
2
- * Category command group for the CLI.
3
- *
4
- * This module defines the `category` command group, which provides operations
5
- * for managing categories in the Cortex memory system. The `--store` option
6
- * is defined at the group level and inherited by all subcommands.
7
- */
8
-
9
- import { Command } from '@commander-js/extra-typings';
10
-
11
- import { createCommand } from './commands/create';
12
-
13
- /**
14
- * The `category` command group.
15
- *
16
- * Provides category management operations. The `--store` option allows
17
- * targeting a specific named store instead of the default store.
18
- * This option is inherited by all subcommands.
19
- */
20
- export const categoryCommand = new Command('category')
21
- .description('Category operations')
22
- .option('-s, --store <name>', 'Use a specific named store');
23
-
24
- categoryCommand.addCommand(createCommand);
@@ -1,203 +0,0 @@
1
- import { describe, expect, it, mock } from 'bun:test';
2
- import { CommanderError } from '@commander-js/extra-typings';
3
- import { PassThrough } from 'node:stream';
4
- import { err, ok, type ConfigStore, type CortexContext } from '@yeseh/cortex-core';
5
-
6
- import { handleInit } from './init.ts';
7
- import type { PromptDeps } from '../utils/prompts.ts';
8
-
9
- const existingGlobalStore: ConfigStore = {
10
- kind: 'filesystem',
11
- categoryMode: 'free',
12
- categories: {},
13
- properties: { path: '/tmp/existing-global-store' },
14
- };
15
-
16
- const createContext = (config: CortexContext['config']): { ctx: CortexContext; stdout: PassThrough } => {
17
- const stdout = new PassThrough();
18
- let output = '';
19
-
20
- stdout.on('data', (chunk: Buffer | string) => {
21
- output += chunk.toString();
22
- });
23
-
24
- const ctx = {
25
- cortex: {} as CortexContext['cortex'],
26
- config,
27
- settings: {},
28
- stores: {},
29
- now: () => new Date('2025-01-01T00:00:00.000Z'),
30
- stdin: new PassThrough() as unknown as NodeJS.ReadStream,
31
- stdout: stdout as unknown as NodeJS.WriteStream,
32
- } as CortexContext;
33
-
34
- return {
35
- ctx,
36
- stdout: Object.assign(stdout, {
37
- getOutput: () => output,
38
- }) as PassThrough,
39
- };
40
- };
41
-
42
- describe('handleInit', () => {
43
- it('should initialize config and create global store when missing', async () => {
44
- const initializeConfig = mock(async () => ok(undefined));
45
- const getStore = mock(async () => ok(null));
46
- const saveStore = mock(async () => ok(undefined));
47
-
48
- const { ctx, stdout } = createContext({
49
- path: '/tmp/test-config.yaml',
50
- data: null,
51
- stores: null,
52
- settings: null,
53
- initializeConfig,
54
- getSettings: async () => ok({}),
55
- getStores: async () => ok({}),
56
- getStore,
57
- saveStore,
58
- });
59
-
60
- await handleInit(ctx, { format: 'json' });
61
-
62
- expect(initializeConfig).toHaveBeenCalledTimes(1);
63
- expect(saveStore).toHaveBeenCalledTimes(1);
64
-
65
- const output = (stdout as PassThrough & { getOutput: () => string }).getOutput();
66
- const parsed = JSON.parse(output) as { value: { path: string } };
67
- expect(parsed.value.path).toContain('/.config/cortex/memory');
68
- });
69
-
70
- it('should throw when global store already exists without force', async () => {
71
- const saveStore = mock(async () => ok(undefined));
72
-
73
- const { ctx } = createContext({
74
- path: '/tmp/test-config.yaml',
75
- data: null,
76
- stores: null,
77
- settings: null,
78
- initializeConfig: async () => ok(undefined),
79
- getSettings: async () => ok({}),
80
- getStores: async () => ok({}),
81
- getStore: async () => ok(existingGlobalStore),
82
- saveStore,
83
- });
84
-
85
- await expect(handleInit(ctx, { format: 'yaml' })).rejects.toThrow(CommanderError);
86
- expect(saveStore).not.toHaveBeenCalled();
87
- });
88
-
89
- it('should allow force and skip saveStore when global store already exists', async () => {
90
- const saveStore = mock(async () => ok(undefined));
91
-
92
- const { ctx } = createContext({
93
- path: '/tmp/test-config.yaml',
94
- data: null,
95
- stores: null,
96
- settings: null,
97
- initializeConfig: async () => ok(undefined),
98
- getSettings: async () => ok({}),
99
- getStores: async () => ok({}),
100
- getStore: async () => ok(existingGlobalStore),
101
- saveStore,
102
- });
103
-
104
- await expect(handleInit(ctx, { force: true, format: 'yaml' })).resolves.toBeUndefined();
105
- expect(saveStore).not.toHaveBeenCalled();
106
- });
107
-
108
- it('should surface config initialization failures', async () => {
109
- const { ctx } = createContext({
110
- path: '/tmp/test-config.yaml',
111
- data: null,
112
- stores: null,
113
- settings: null,
114
- initializeConfig: async () =>
115
- err({
116
- code: 'CONFIG_WRITE_FAILED',
117
- message: 'Failed to write config file',
118
- }),
119
- getSettings: async () => ok({}),
120
- getStores: async () => ok({}),
121
- getStore: async () => ok(null),
122
- saveStore: async () => ok(undefined),
123
- });
124
-
125
- await expect(handleInit(ctx, { format: 'yaml' })).rejects.toThrow(CommanderError);
126
- });
127
- });
128
-
129
- describe('handleInit - interactive mode', () => {
130
- it('should call promptDeps.input twice when stdin is a TTY and no flags are given', async () => {
131
- const inputMock = mock(async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value');
132
- const confirmMock = mock(async () => true);
133
- const promptDeps: PromptDeps = { input: inputMock, confirm: confirmMock };
134
-
135
- const { ctx } = createContext({
136
- path: '/tmp/test-config.yaml',
137
- data: null,
138
- stores: null,
139
- settings: null,
140
- initializeConfig: async () => ok(undefined),
141
- getSettings: async () => ok({}),
142
- getStores: async () => ok({}),
143
- getStore: async () => ok(null),
144
- saveStore: async () => ok(undefined),
145
- });
146
- (ctx.stdin as unknown as { isTTY: boolean }).isTTY = true;
147
-
148
- await handleInit(ctx, { format: 'json' }, promptDeps);
149
-
150
- expect(inputMock).toHaveBeenCalledTimes(2);
151
- });
152
-
153
- it('should NOT call promptDeps.input when stdin is NOT a TTY', async () => {
154
- const inputMock = mock(async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value');
155
- const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
156
-
157
- const { ctx } = createContext({
158
- path: '/tmp/test-config.yaml',
159
- data: null,
160
- stores: null,
161
- settings: null,
162
- initializeConfig: async () => ok(undefined),
163
- getSettings: async () => ok({}),
164
- getStores: async () => ok({}),
165
- getStore: async () => ok(null),
166
- saveStore: async () => ok(undefined),
167
- });
168
- // ctx.stdin is a PassThrough with isTTY = undefined (non-TTY)
169
-
170
- await handleInit(ctx, { format: 'json' }, promptDeps);
171
-
172
- expect(inputMock).not.toHaveBeenCalled();
173
- });
174
-
175
- it('should use prompted store name in saveStore call when TTY', async () => {
176
- const promptedName = 'my-custom-name';
177
- const inputMock = mock(async ({ message, default: d }: { message: string; default?: string }) => {
178
- // Second call is for store name
179
- if (message.includes('name')) return promptedName;
180
- return d ?? 'default-path';
181
- });
182
- const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
183
-
184
- const saveStore = mock(async () => ok(undefined));
185
- const { ctx } = createContext({
186
- path: '/tmp/test-config.yaml',
187
- data: null,
188
- stores: null,
189
- settings: null,
190
- initializeConfig: async () => ok(undefined),
191
- getSettings: async () => ok({}),
192
- getStores: async () => ok({}),
193
- getStore: async () => ok(null),
194
- saveStore,
195
- });
196
- (ctx.stdin as unknown as { isTTY: boolean }).isTTY = true;
197
-
198
- await handleInit(ctx, { format: 'json' }, promptDeps);
199
-
200
- // saveStore should be called with the prompted name
201
- expect(saveStore).toHaveBeenCalledWith(promptedName, expect.any(Object));
202
- });
203
- });
@@ -1,301 +0,0 @@
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
-