@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.
- package/dist/program.js +1538 -5
- package/dist/program.js.map +32 -3
- package/dist/run.d.ts +0 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +3 -4
- package/dist/run.js.map +3 -3
- package/package.json +4 -6
- package/dist/chunk-dsfj4baj.js +0 -1543
- package/dist/chunk-dsfj4baj.js.map +0 -38
- package/src/category/commands/create.spec.ts +0 -139
- package/src/category/commands/create.ts +0 -119
- package/src/category/index.ts +0 -24
- package/src/commands/init.spec.ts +0 -203
- package/src/commands/init.ts +0 -301
- package/src/context.spec.ts +0 -60
- package/src/context.ts +0 -170
- package/src/errors.spec.ts +0 -264
- package/src/errors.ts +0 -105
- package/src/memory/commands/add.spec.ts +0 -169
- package/src/memory/commands/add.ts +0 -158
- package/src/memory/commands/definitions.spec.ts +0 -80
- package/src/memory/commands/list.spec.ts +0 -123
- package/src/memory/commands/list.ts +0 -269
- package/src/memory/commands/move.spec.ts +0 -85
- package/src/memory/commands/move.ts +0 -119
- package/src/memory/commands/remove.spec.ts +0 -79
- package/src/memory/commands/remove.ts +0 -108
- package/src/memory/commands/show.spec.ts +0 -71
- package/src/memory/commands/show.ts +0 -165
- package/src/memory/commands/test-helpers.spec.ts +0 -127
- package/src/memory/commands/update.spec.ts +0 -86
- package/src/memory/commands/update.ts +0 -230
- package/src/memory/index.spec.ts +0 -59
- package/src/memory/index.ts +0 -44
- package/src/memory/parsing.spec.ts +0 -105
- package/src/memory/parsing.ts +0 -22
- package/src/observability.spec.ts +0 -126
- package/src/observability.ts +0 -82
- package/src/output.spec.ts +0 -835
- package/src/output.ts +0 -119
- package/src/program.spec.ts +0 -46
- package/src/program.ts +0 -75
- package/src/run.spec.ts +0 -31
- package/src/run.ts +0 -9
- package/src/store/commands/add.spec.ts +0 -131
- package/src/store/commands/add.ts +0 -231
- package/src/store/commands/init.spec.ts +0 -220
- package/src/store/commands/init.ts +0 -272
- package/src/store/commands/list.spec.ts +0 -175
- package/src/store/commands/list.ts +0 -102
- package/src/store/commands/prune.spec.ts +0 -120
- package/src/store/commands/prune.ts +0 -152
- package/src/store/commands/reindexs.spec.ts +0 -94
- package/src/store/commands/reindexs.ts +0 -97
- package/src/store/commands/remove.spec.ts +0 -97
- package/src/store/commands/remove.ts +0 -189
- package/src/store/index.spec.ts +0 -60
- package/src/store/index.ts +0 -49
- package/src/store/utils/resolve-store-name.spec.ts +0 -62
- package/src/store/utils/resolve-store-name.ts +0 -79
- package/src/test-helpers.spec.ts +0 -430
- package/src/tests/cli.integration.spec.ts +0 -1306
- package/src/toon.spec.ts +0 -183
- package/src/toon.ts +0 -462
- package/src/utils/git.spec.ts +0 -95
- package/src/utils/git.ts +0 -51
- package/src/utils/input.spec.ts +0 -326
- package/src/utils/input.ts +0 -150
- package/src/utils/paths.spec.ts +0 -235
- package/src/utils/paths.ts +0 -75
- package/src/utils/prompts.spec.ts +0 -23
- package/src/utils/prompts.ts +0 -88
- package/src/utils/resolve-default-store.spec.ts +0 -135
- 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
|
-
});
|
package/src/category/index.ts
DELETED
|
@@ -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
|
-
});
|
package/src/commands/init.ts
DELETED
|
@@ -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
|
-
|