@yeseh/cortex-cli 0.6.8 → 0.6.9
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,220 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the store init command handler.
|
|
3
|
-
*
|
|
4
|
-
* @module cli/store/commands/init.spec
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, afterEach, beforeEach, mock } from 'bun:test';
|
|
8
|
-
import { InvalidArgumentError } from '@commander-js/extra-typings';
|
|
9
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
10
|
-
import { tmpdir } from 'node:os';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
import type { PromptDeps } from '../../utils/prompts.ts';
|
|
13
|
-
|
|
14
|
-
import { handleInit } from './init.ts';
|
|
15
|
-
import { createMockContext, captureOutput } from '../../test-helpers.spec.ts';
|
|
16
|
-
import { FilesystemConfigAdapter } from '@yeseh/cortex-storage-fs';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Creates a real FilesystemConfigAdapter backed by a temp config file and
|
|
20
|
-
* injects it into a mock context so handleInit can construct a real
|
|
21
|
-
* FilesystemStorageAdapter without the adapter factory throwing STORE_NOT_FOUND.
|
|
22
|
-
*/
|
|
23
|
-
async function createInitContext(tempDir: string) {
|
|
24
|
-
const configPath = join(tempDir, 'config.yaml');
|
|
25
|
-
const configAdapter = new FilesystemConfigAdapter(configPath);
|
|
26
|
-
await configAdapter.initializeConfig();
|
|
27
|
-
|
|
28
|
-
const { ctx, stdout, stdin } = createMockContext({ cwd: tempDir });
|
|
29
|
-
// Replace the mock ConfigAdapter with a real one so FilesystemStorageAdapter
|
|
30
|
-
// construction inside handleInit works correctly.
|
|
31
|
-
(ctx as unknown as Record<string, unknown>).config = configAdapter;
|
|
32
|
-
|
|
33
|
-
return { ctx, stdout, stdin };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe('handleInit', () => {
|
|
37
|
-
let tempDir: string;
|
|
38
|
-
|
|
39
|
-
beforeEach(async () => {
|
|
40
|
-
tempDir = await mkdtemp(join(tmpdir(), 'cortex-init-'));
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
afterEach(async () => {
|
|
44
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should initialize a store and write success message to stdout', async () => {
|
|
48
|
-
const { ctx, stdout } = await createInitContext(tempDir);
|
|
49
|
-
|
|
50
|
-
await handleInit(ctx, undefined, { name: 'my-project', format: 'yaml' });
|
|
51
|
-
|
|
52
|
-
const out = captureOutput(stdout);
|
|
53
|
-
expect(out).toContain('my-project');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should use the provided target path as the store path', async () => {
|
|
57
|
-
const customPath = join(tempDir, 'custom-store');
|
|
58
|
-
const { ctx, stdout } = await createInitContext(tempDir);
|
|
59
|
-
|
|
60
|
-
await handleInit(ctx, customPath, { name: 'my-project', format: 'yaml' });
|
|
61
|
-
|
|
62
|
-
const out = captureOutput(stdout);
|
|
63
|
-
expect(out).toContain('custom-store');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should default to .cortex under cwd when no target path is given', async () => {
|
|
67
|
-
const { ctx, stdout } = await createInitContext(tempDir);
|
|
68
|
-
|
|
69
|
-
await handleInit(ctx, undefined, { name: 'my-project', format: 'yaml' });
|
|
70
|
-
|
|
71
|
-
const out = captureOutput(stdout);
|
|
72
|
-
// Default path is <cwd>/.cortex
|
|
73
|
-
expect(out).toContain('.cortex');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should throw InvalidArgumentError for an invalid store name', async () => {
|
|
77
|
-
const { ctx } = await createInitContext(tempDir);
|
|
78
|
-
|
|
79
|
-
await expect(handleInit(ctx, undefined, { name: ' ', format: 'yaml' })).rejects.toThrow(
|
|
80
|
-
InvalidArgumentError,
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should output in JSON format when format option is json', async () => {
|
|
85
|
-
const { ctx, stdout } = await createInitContext(tempDir);
|
|
86
|
-
|
|
87
|
-
await handleInit(ctx, undefined, { name: 'my-project', format: 'json' });
|
|
88
|
-
|
|
89
|
-
const out = captureOutput(stdout);
|
|
90
|
-
const parsed = JSON.parse(out) as { value: Record<string, unknown> };
|
|
91
|
-
expect(typeof parsed.value.name).toBe('string');
|
|
92
|
-
expect(typeof parsed.value.path).toBe('string');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should include the store name in the success output', async () => {
|
|
96
|
-
const { ctx, stdout } = await createInitContext(tempDir);
|
|
97
|
-
|
|
98
|
-
await handleInit(ctx, undefined, { name: 'hello-world', format: 'json' });
|
|
99
|
-
|
|
100
|
-
const out = captureOutput(stdout);
|
|
101
|
-
const parsed = JSON.parse(out) as { value: { name: string } };
|
|
102
|
-
expect(parsed.value.name).toBe('hello-world');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should expand tilde in target path', async () => {
|
|
106
|
-
const { ctx, stdout } = await createInitContext(tempDir);
|
|
107
|
-
|
|
108
|
-
// resolveUserPath expands ~ — we just verify it doesn't throw and
|
|
109
|
-
// produces output with a home-like absolute path
|
|
110
|
-
await handleInit(ctx, '~/my-store', { name: 'my-project', format: 'yaml' });
|
|
111
|
-
|
|
112
|
-
const out = captureOutput(stdout);
|
|
113
|
-
expect(out).toContain('my-store');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should fail when the store name already exists', async () => {
|
|
117
|
-
const { ctx } = await createInitContext(tempDir);
|
|
118
|
-
const storePath = join(tempDir, '.cortex');
|
|
119
|
-
|
|
120
|
-
await handleInit(ctx, storePath, { name: 'duplicate-store', format: 'yaml' });
|
|
121
|
-
|
|
122
|
-
// Second init with the same name should throw
|
|
123
|
-
const { ctx: ctx2 } = await createInitContext(tempDir);
|
|
124
|
-
await expect(
|
|
125
|
-
handleInit(ctx2, join(tempDir, '.cortex2'), { name: 'duplicate-store', format: 'yaml' }),
|
|
126
|
-
).rejects.toThrow();
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe('handleInit - interactive mode', () => {
|
|
131
|
-
let tempDir: string;
|
|
132
|
-
|
|
133
|
-
beforeEach(async () => {
|
|
134
|
-
tempDir = await mkdtemp(join(tmpdir(), 'cortex-init-interactive-'));
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
afterEach(async () => {
|
|
138
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should call promptDeps.input once (path only) when stdin is a TTY and --name is explicitly given', async () => {
|
|
142
|
-
const inputMock = mock(
|
|
143
|
-
async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value',
|
|
144
|
-
);
|
|
145
|
-
const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
|
|
146
|
-
|
|
147
|
-
const { ctx, stdin } = await createInitContext(tempDir);
|
|
148
|
-
(stdin as unknown as { isTTY: boolean }).isTTY = true;
|
|
149
|
-
|
|
150
|
-
await handleInit(ctx, undefined, { name: 'my-project', format: 'yaml' }, promptDeps);
|
|
151
|
-
|
|
152
|
-
// Should NOT call input when --name is explicitly given (skips name prompt but still prompts path)
|
|
153
|
-
// With explicit name, only path prompt is called
|
|
154
|
-
expect(inputMock).toHaveBeenCalledTimes(1);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should call promptDeps.input twice (name + path) when stdin is a TTY and no --name given', async () => {
|
|
158
|
-
const inputMock = mock(
|
|
159
|
-
async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value',
|
|
160
|
-
);
|
|
161
|
-
const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
|
|
162
|
-
|
|
163
|
-
const { ctx, stdin } = await createInitContext(tempDir);
|
|
164
|
-
(stdin as unknown as { isTTY: boolean }).isTTY = true;
|
|
165
|
-
|
|
166
|
-
// No --name provided, should prompt for both name and path
|
|
167
|
-
await handleInit(ctx, undefined, { format: 'yaml' }, promptDeps);
|
|
168
|
-
|
|
169
|
-
expect(inputMock).toHaveBeenCalledTimes(2);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('should NOT call promptDeps.input when stdin is NOT a TTY', async () => {
|
|
173
|
-
const inputMock = mock(
|
|
174
|
-
async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value',
|
|
175
|
-
);
|
|
176
|
-
const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
|
|
177
|
-
|
|
178
|
-
const { ctx } = await createInitContext(tempDir);
|
|
179
|
-
// ctx stdin has isTTY = undefined (non-TTY)
|
|
180
|
-
|
|
181
|
-
await handleInit(ctx, undefined, { name: 'my-project', format: 'yaml' }, promptDeps);
|
|
182
|
-
|
|
183
|
-
expect(inputMock).not.toHaveBeenCalled();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('should skip both prompts when --name and target path are both given explicitly and TTY', async () => {
|
|
187
|
-
const inputMock = mock(
|
|
188
|
-
async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value',
|
|
189
|
-
);
|
|
190
|
-
const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
|
|
191
|
-
|
|
192
|
-
const { ctx, stdin } = await createInitContext(tempDir);
|
|
193
|
-
(stdin as unknown as { isTTY: boolean }).isTTY = true;
|
|
194
|
-
const customPath = join(tempDir, 'custom-store');
|
|
195
|
-
|
|
196
|
-
await handleInit(ctx, customPath, { name: 'my-project', format: 'yaml' }, promptDeps);
|
|
197
|
-
|
|
198
|
-
expect(inputMock).not.toHaveBeenCalled();
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('should use prompted store name in the output', async () => {
|
|
202
|
-
const promptedName = 'prompted-store-name';
|
|
203
|
-
const inputMock = mock(
|
|
204
|
-
async ({ message, default: d }: { message: string; default?: string }) => {
|
|
205
|
-
if (message.toLowerCase().includes('name')) return promptedName;
|
|
206
|
-
return d ?? 'default-path';
|
|
207
|
-
},
|
|
208
|
-
);
|
|
209
|
-
const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
|
|
210
|
-
|
|
211
|
-
const { ctx, stdin, stdout } = await createInitContext(tempDir);
|
|
212
|
-
(stdin as unknown as { isTTY: boolean }).isTTY = true;
|
|
213
|
-
|
|
214
|
-
await handleInit(ctx, undefined, { format: 'json' }, promptDeps);
|
|
215
|
-
|
|
216
|
-
const out = captureOutput(stdout);
|
|
217
|
-
const parsed = JSON.parse(out) as { value: { name: string } };
|
|
218
|
-
expect(parsed.value.name).toBe(promptedName);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Store init command for initializing a new memory store.
|
|
3
|
-
*
|
|
4
|
-
* This command creates a new memory store at the specified path (or current
|
|
5
|
-
* directory) and registers it in the global registry. The store name is
|
|
6
|
-
* either explicitly provided via --name or auto-detected from the git
|
|
7
|
-
* repository name.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```bash
|
|
11
|
-
* # Initialize store with auto-detected name from git repo
|
|
12
|
-
* cortex store init
|
|
13
|
-
*
|
|
14
|
-
* # Initialize store with explicit name
|
|
15
|
-
* cortex store init --name my-project
|
|
16
|
-
*
|
|
17
|
-
* # Initialize store at a specific path
|
|
18
|
-
* cortex store init ./my-store --name my-project
|
|
19
|
-
*
|
|
20
|
-
* # Initialize store with tilde expansion
|
|
21
|
-
* cortex store init ~/memories --name personal
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { Command } from '@commander-js/extra-typings';
|
|
26
|
-
import { resolve } from 'node:path';
|
|
27
|
-
import { stdin, stdout as processStdout } from 'node:process';
|
|
28
|
-
import { resolveStoreName } from '../utils/resolve-store-name.ts';
|
|
29
|
-
import { throwCliError } from '../../errors.ts';
|
|
30
|
-
import { type StoreData, initializeStore } from '@yeseh/cortex-core/store';
|
|
31
|
-
import { type CategoryMode, type CortexContext } from '@yeseh/cortex-core';
|
|
32
|
-
import { serializeOutput, type OutputStoreInit, type OutputFormat } from '../../output.ts';
|
|
33
|
-
import { resolveUserPath } from '../../utils/paths.ts';
|
|
34
|
-
import { createCliConfigContext } from '../../context.ts';
|
|
35
|
-
import { isTTY, defaultPromptDeps, type PromptDeps } from '../../utils/prompts.ts';
|
|
36
|
-
import type { FilesystemConfigAdapter } from '@yeseh/cortex-storage-fs';
|
|
37
|
-
import { FilesystemStorageAdapter } from '@yeseh/cortex-storage-fs';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Options for the init command.
|
|
41
|
-
*/
|
|
42
|
-
export interface InitCommandOptions {
|
|
43
|
-
/** Explicit store name (otherwise auto-detected from git) */
|
|
44
|
-
name?: string;
|
|
45
|
-
/** Category mode for the store */
|
|
46
|
-
categoryMode?: CategoryMode;
|
|
47
|
-
/** Output format (yaml, json, toon) */
|
|
48
|
-
format?: string;
|
|
49
|
-
/** Optional description for the store */
|
|
50
|
-
description?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Writes the serialized output to the output stream.
|
|
55
|
-
*
|
|
56
|
-
* @param output - The store init output payload
|
|
57
|
-
* @param format - The output format
|
|
58
|
-
* @param stdout - The output stream
|
|
59
|
-
*/
|
|
60
|
-
function writeOutput(
|
|
61
|
-
output: OutputStoreInit,
|
|
62
|
-
format: OutputFormat,
|
|
63
|
-
stdout: NodeJS.WritableStream,
|
|
64
|
-
): void {
|
|
65
|
-
const serialized = serializeOutput({ kind: 'store-init', value: output }, format);
|
|
66
|
-
if (!serialized.ok()) {
|
|
67
|
-
throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
|
|
68
|
-
}
|
|
69
|
-
stdout.write(serialized.value + '\n');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Prompts the user to confirm or change the resolved store name and path.
|
|
74
|
-
*
|
|
75
|
-
* Prompts are selectively skipped:
|
|
76
|
-
* - Name prompt skipped when `explicit.name` is provided
|
|
77
|
-
* - Path prompt skipped when `explicit.path` is provided
|
|
78
|
-
* - All prompts skipped when stdin is not a TTY
|
|
79
|
-
*
|
|
80
|
-
* @param ctx - Cortex context used for TTY detection via `ctx.stdin`
|
|
81
|
-
* @param resolved - Default store name and path to present as suggestions
|
|
82
|
-
* @param explicit - Which values were provided explicitly by the user
|
|
83
|
-
* @param promptDeps - Injectable prompt functions for testability
|
|
84
|
-
* @returns Finalized store name and path
|
|
85
|
-
*/
|
|
86
|
-
async function promptStoreInitOptions(
|
|
87
|
-
ctx: CortexContext,
|
|
88
|
-
resolved: { storeName: string; storePath: string },
|
|
89
|
-
explicit: { name?: string; path?: string },
|
|
90
|
-
promptDeps: PromptDeps,
|
|
91
|
-
): Promise<{ storeName: string; storePath: string }> {
|
|
92
|
-
if (!isTTY(ctx.stdin)) return resolved;
|
|
93
|
-
|
|
94
|
-
const storeName = explicit.name
|
|
95
|
-
? resolved.storeName
|
|
96
|
-
: await promptDeps.input({ message: 'Store name:', default: resolved.storeName });
|
|
97
|
-
|
|
98
|
-
let storePath: string;
|
|
99
|
-
if (explicit.path) {
|
|
100
|
-
storePath = resolved.storePath;
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
const promptedPath = await promptDeps.input({
|
|
104
|
-
message: 'Store path:',
|
|
105
|
-
default: resolved.storePath,
|
|
106
|
-
});
|
|
107
|
-
const trimmedPath = promptedPath.trim();
|
|
108
|
-
storePath = resolveUserPath(trimmedPath, process.cwd());
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return { storeName, storePath };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Tries to resolve the store name. Returns an empty string when resolution
|
|
116
|
-
* fails and stdin is a TTY (allowing the interactive prompt to take over).
|
|
117
|
-
* Re-throws when stdin is not a TTY.
|
|
118
|
-
*
|
|
119
|
-
* @param cwd - Current working directory for git detection
|
|
120
|
-
* @param explicitName - Optional explicit name from `--name` flag
|
|
121
|
-
* @param tty - Whether stdin is a TTY
|
|
122
|
-
*/
|
|
123
|
-
async function resolveStoreNameOrEmpty(
|
|
124
|
-
cwd: string,
|
|
125
|
-
explicitName: string | undefined,
|
|
126
|
-
tty: boolean,
|
|
127
|
-
): Promise<string> {
|
|
128
|
-
try {
|
|
129
|
-
return await resolveStoreName(cwd, explicitName);
|
|
130
|
-
}
|
|
131
|
-
catch (e) {
|
|
132
|
-
// When running in a TTY, only swallow errors (and fall back to prompting)
|
|
133
|
-
// if no explicit name was provided. If the user passed an explicit --name,
|
|
134
|
-
// re-throw so they see the actual invalid-name error.
|
|
135
|
-
if (tty && !explicitName) return '';
|
|
136
|
-
throw e;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Handles the store init command execution.
|
|
142
|
-
*
|
|
143
|
-
* This function:
|
|
144
|
-
* 1. Resolves the store name (explicit or git detection; falls back to empty for TTY prompt)
|
|
145
|
-
* 2. Resolves target path (default to .cortex in cwd)
|
|
146
|
-
* 3. When stdin is a TTY, prompts for unresolved name and/or path
|
|
147
|
-
* 4. Uses `store.initialize` to create directory, index, and register
|
|
148
|
-
* 5. Outputs the result
|
|
149
|
-
*
|
|
150
|
-
* Interactive mode activates automatically when `ctx.stdin.isTTY === true`.
|
|
151
|
-
* In non-TTY environments (CI, pipes) the command behaves exactly as before —
|
|
152
|
-
* no behavioral regression.
|
|
153
|
-
*
|
|
154
|
-
* @param ctx - The Cortex context (stdin TTY state used for interactive detection)
|
|
155
|
-
* @param targetPath - Optional path for the store (defaults to .cortex in cwd)
|
|
156
|
-
* @param options - Command options (name, format, categoryMode, description)
|
|
157
|
-
* @param promptDeps - Injectable prompt functions; defaults to real `@inquirer/prompts` functions
|
|
158
|
-
* @throws {InvalidArgumentError} When the store name is invalid
|
|
159
|
-
* @throws {CommanderError} When the store already exists or init fails
|
|
160
|
-
*
|
|
161
|
-
* @example
|
|
162
|
-
* ```typescript
|
|
163
|
-
* // Explicit name + path (no prompts even in TTY):
|
|
164
|
-
* await handleInit(ctx, './my-store', { name: 'my-project' });
|
|
165
|
-
*
|
|
166
|
-
* // Interactive (TTY detected, no --name given):
|
|
167
|
-
* await handleInit(ctx, undefined, {}, defaultPromptDeps);
|
|
168
|
-
* ```
|
|
169
|
-
*/
|
|
170
|
-
export async function handleInit(
|
|
171
|
-
ctx: CortexContext,
|
|
172
|
-
targetPath: string | undefined,
|
|
173
|
-
options: InitCommandOptions = {},
|
|
174
|
-
promptDeps: PromptDeps = defaultPromptDeps,
|
|
175
|
-
): Promise<void> {
|
|
176
|
-
const cwd = ctx.cwd ?? process.cwd();
|
|
177
|
-
const stdout = ctx.stdout ?? process.stdout;
|
|
178
|
-
|
|
179
|
-
const storeName = await resolveStoreNameOrEmpty(cwd, options.name, isTTY(ctx.stdin));
|
|
180
|
-
|
|
181
|
-
const storePath = targetPath
|
|
182
|
-
? resolveUserPath(targetPath.trim(), cwd)
|
|
183
|
-
: resolve(cwd, '.cortex');
|
|
184
|
-
|
|
185
|
-
const resolved = await promptStoreInitOptions(
|
|
186
|
-
ctx,
|
|
187
|
-
{ storeName, storePath },
|
|
188
|
-
{ name: options.name, path: targetPath },
|
|
189
|
-
promptDeps,
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
const finalStoreName = resolved.storeName;
|
|
193
|
-
const finalStorePath = resolved.storePath;
|
|
194
|
-
|
|
195
|
-
if (!finalStoreName) {
|
|
196
|
-
throwCliError({
|
|
197
|
-
code: 'INVALID_STORE_NAME',
|
|
198
|
-
message: 'Store name is required. Use --name to specify one.',
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const storeData: StoreData = {
|
|
203
|
-
kind: 'filesystem',
|
|
204
|
-
categoryMode: (options.categoryMode as CategoryMode) ?? 'free',
|
|
205
|
-
categories: [],
|
|
206
|
-
properties: {
|
|
207
|
-
path: finalStorePath,
|
|
208
|
-
},
|
|
209
|
-
description: options.description,
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const adapter = new FilesystemStorageAdapter(ctx.config as FilesystemConfigAdapter, {
|
|
213
|
-
rootDirectory: finalStorePath,
|
|
214
|
-
});
|
|
215
|
-
const createResult = await initializeStore(adapter, finalStoreName, storeData);
|
|
216
|
-
if (!createResult.ok()) {
|
|
217
|
-
throwCliError(createResult.error);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const output: OutputStoreInit = { path: finalStorePath, name: finalStoreName };
|
|
221
|
-
const format: OutputFormat = (options.format as OutputFormat) ?? 'yaml';
|
|
222
|
-
writeOutput(output, format, stdout);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* The `init` subcommand for initializing a new memory store.
|
|
227
|
-
*
|
|
228
|
-
* Creates a new store at the specified path (or .cortex in current directory)
|
|
229
|
-
* and registers it in the global registry. The store name is auto-detected
|
|
230
|
-
* from the git repository name or can be explicitly provided.
|
|
231
|
-
*
|
|
232
|
-
* @example
|
|
233
|
-
* ```bash
|
|
234
|
-
* cortex store init # Auto-detect name from git
|
|
235
|
-
* cortex store init --name my-project # Explicit name
|
|
236
|
-
* cortex store init ./store --name custom # Custom path and name
|
|
237
|
-
* ```
|
|
238
|
-
*/
|
|
239
|
-
export const initCommand = new Command('init')
|
|
240
|
-
.description('Initialize a new memory store')
|
|
241
|
-
.argument('[path]', 'Path for the store (defaults to .cortex in current directory)')
|
|
242
|
-
.option('-n, --name <name>', 'Explicit store name (otherwise auto-detected from git)')
|
|
243
|
-
.option('-d, --description <description>', 'Optional description for the store')
|
|
244
|
-
.option('-c, --category-mode <mode>', 'Category mode (free, strict, flat)', 'free')
|
|
245
|
-
.option('-o, --format <format>', 'Output format (yaml, json, toon)', 'yaml')
|
|
246
|
-
.action(async (path, options) => {
|
|
247
|
-
const configCtx = await createCliConfigContext();
|
|
248
|
-
if (!configCtx.ok()) {
|
|
249
|
-
throwCliError(configCtx.error);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const { configAdapter, effectiveCwd } = configCtx.value;
|
|
253
|
-
|
|
254
|
-
// Build a minimal context for handleInit. The init command cannot go
|
|
255
|
-
// through the full CortexContext/adapterFactory because the store is
|
|
256
|
-
// not yet registered — the adapter factory would throw STORE_NOT_FOUND.
|
|
257
|
-
// handleInit only needs cwd, stdin, stdout, and config (to construct
|
|
258
|
-
// its own adapter directly via initializeStore).
|
|
259
|
-
const ctx = {
|
|
260
|
-
config: configAdapter,
|
|
261
|
-
cwd: effectiveCwd,
|
|
262
|
-
stdin,
|
|
263
|
-
stdout: processStdout,
|
|
264
|
-
} as unknown as CortexContext;
|
|
265
|
-
|
|
266
|
-
await handleInit(ctx, path, {
|
|
267
|
-
name: options.name,
|
|
268
|
-
description: options.description,
|
|
269
|
-
categoryMode: options.categoryMode as CategoryMode,
|
|
270
|
-
format: options.format,
|
|
271
|
-
});
|
|
272
|
-
});
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the store list command handler.
|
|
3
|
-
*
|
|
4
|
-
* @module cli/store/commands/list.spec
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect } from 'bun:test';
|
|
8
|
-
import { handleList } from './list.ts';
|
|
9
|
-
import {
|
|
10
|
-
createMockContext,
|
|
11
|
-
captureOutput,
|
|
12
|
-
} from '../../test-helpers.spec.ts';
|
|
13
|
-
|
|
14
|
-
describe('handleList', () => {
|
|
15
|
-
it('should output empty store list when no stores are configured', async () => {
|
|
16
|
-
const { ctx, stdout } = createMockContext({ stores: {} });
|
|
17
|
-
|
|
18
|
-
await handleList(ctx, {}, { stdout });
|
|
19
|
-
|
|
20
|
-
const output = captureOutput(stdout);
|
|
21
|
-
// YAML output for empty stores list
|
|
22
|
-
expect(output).toContain('stores');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('should output a single store in YAML format by default', async () => {
|
|
26
|
-
const { ctx, stdout } = createMockContext({
|
|
27
|
-
stores: {
|
|
28
|
-
global: {
|
|
29
|
-
kind: 'filesystem',
|
|
30
|
-
categoryMode: 'free',
|
|
31
|
-
categories: {},
|
|
32
|
-
properties: { path: '/mock/store' },
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
await handleList(ctx, {}, { stdout });
|
|
38
|
-
|
|
39
|
-
const output = captureOutput(stdout);
|
|
40
|
-
expect(output).toContain('global');
|
|
41
|
-
expect(output).toContain('/mock/store');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should output multiple stores sorted alphabetically by name', async () => {
|
|
45
|
-
const { ctx, stdout } = createMockContext({
|
|
46
|
-
stores: {
|
|
47
|
-
zebra: {
|
|
48
|
-
kind: 'filesystem',
|
|
49
|
-
categoryMode: 'free',
|
|
50
|
-
categories: {},
|
|
51
|
-
properties: { path: '/path/zebra' },
|
|
52
|
-
},
|
|
53
|
-
alpha: {
|
|
54
|
-
kind: 'filesystem',
|
|
55
|
-
categoryMode: 'free',
|
|
56
|
-
categories: {},
|
|
57
|
-
properties: { path: '/path/alpha' },
|
|
58
|
-
},
|
|
59
|
-
middle: {
|
|
60
|
-
kind: 'filesystem',
|
|
61
|
-
categoryMode: 'free',
|
|
62
|
-
categories: {},
|
|
63
|
-
properties: { path: '/path/middle' },
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
await handleList(ctx, {}, { stdout });
|
|
69
|
-
|
|
70
|
-
const output = captureOutput(stdout);
|
|
71
|
-
const alphaPos = output.indexOf('alpha');
|
|
72
|
-
const middlePos = output.indexOf('middle');
|
|
73
|
-
const zebraPos = output.indexOf('zebra');
|
|
74
|
-
|
|
75
|
-
expect(alphaPos).toBeLessThan(middlePos);
|
|
76
|
-
expect(middlePos).toBeLessThan(zebraPos);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should output stores in JSON format when format is json', async () => {
|
|
80
|
-
const { ctx, stdout } = createMockContext({
|
|
81
|
-
stores: {
|
|
82
|
-
global: {
|
|
83
|
-
kind: 'filesystem',
|
|
84
|
-
categoryMode: 'free',
|
|
85
|
-
categories: {},
|
|
86
|
-
properties: { path: '/mock/store' },
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
await handleList(ctx, { format: 'json' }, { stdout });
|
|
92
|
-
|
|
93
|
-
const output = captureOutput(stdout);
|
|
94
|
-
const parsed = JSON.parse(output);
|
|
95
|
-
const stores = parsed.stores ?? parsed.value?.stores;
|
|
96
|
-
expect(Array.isArray(stores)).toBe(true);
|
|
97
|
-
expect(stores).toEqual([
|
|
98
|
-
{
|
|
99
|
-
name: 'global',
|
|
100
|
-
path: '/mock/store',
|
|
101
|
-
},
|
|
102
|
-
]);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should include store paths in the output', async () => {
|
|
106
|
-
const { ctx, stdout } = createMockContext({
|
|
107
|
-
stores: {
|
|
108
|
-
work: {
|
|
109
|
-
kind: 'filesystem',
|
|
110
|
-
categoryMode: 'free',
|
|
111
|
-
categories: {},
|
|
112
|
-
properties: { path: '/home/user/work-memories' },
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
await handleList(ctx, {}, { stdout });
|
|
118
|
-
|
|
119
|
-
const output = captureOutput(stdout);
|
|
120
|
-
expect(output).toContain('work');
|
|
121
|
-
expect(output).toContain('/home/user/work-memories');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should write output to the injected stdout stream', async () => {
|
|
125
|
-
const { ctx } = createMockContext();
|
|
126
|
-
const { PassThrough } = await import('node:stream');
|
|
127
|
-
const customStdout = new PassThrough();
|
|
128
|
-
let captured = '';
|
|
129
|
-
customStdout.on('data', (chunk: Buffer | string) => {
|
|
130
|
-
captured += chunk.toString();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
await handleList(ctx, {}, { stdout: customStdout });
|
|
134
|
-
|
|
135
|
-
expect(captured.length).toBeGreaterThan(0);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('should use ctx.stdout when no deps.stdout is provided', async () => {
|
|
139
|
-
const { ctx, stdout } = createMockContext();
|
|
140
|
-
|
|
141
|
-
// Call without deps – handler falls back to ctx.stdout then process.stdout.
|
|
142
|
-
// We cannot easily capture process.stdout, so pass ctx.stdout via deps.
|
|
143
|
-
await handleList(ctx, {}, { stdout });
|
|
144
|
-
|
|
145
|
-
const output = captureOutput(stdout);
|
|
146
|
-
expect(output.length).toBeGreaterThan(0);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('should output two stores with correct names and paths in YAML', async () => {
|
|
150
|
-
const { ctx, stdout } = createMockContext({
|
|
151
|
-
stores: {
|
|
152
|
-
personal: {
|
|
153
|
-
kind: 'filesystem',
|
|
154
|
-
categoryMode: 'free',
|
|
155
|
-
categories: {},
|
|
156
|
-
properties: { path: '/home/user/personal' },
|
|
157
|
-
},
|
|
158
|
-
work: {
|
|
159
|
-
kind: 'filesystem',
|
|
160
|
-
categoryMode: 'free',
|
|
161
|
-
categories: {},
|
|
162
|
-
properties: { path: '/home/user/work' },
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
await handleList(ctx, { format: 'yaml' }, { stdout });
|
|
168
|
-
|
|
169
|
-
const output = captureOutput(stdout);
|
|
170
|
-
expect(output).toContain('personal');
|
|
171
|
-
expect(output).toContain('/home/user/personal');
|
|
172
|
-
expect(output).toContain('work');
|
|
173
|
-
expect(output).toContain('/home/user/work');
|
|
174
|
-
});
|
|
175
|
-
});
|