@yeseh/cortex-cli 0.6.0 → 0.6.4
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/package.json +3 -3
- package/src/category/commands/create.ts +7 -3
- package/src/context.ts +15 -20
- package/src/memory/commands/add.ts +2 -1
- package/src/memory/commands/list.ts +2 -1
- package/src/memory/commands/move.ts +6 -2
- package/src/memory/commands/remove.ts +6 -2
- package/src/memory/commands/show.ts +3 -2
- package/src/memory/commands/update.ts +2 -1
- package/src/observability.spec.ts +8 -21
- package/src/observability.ts +26 -7
- package/src/program.ts +1 -1
- package/src/run.ts +0 -0
- package/src/store/commands/init.spec.ts +62 -78
- package/src/store/commands/init.ts +31 -15
- package/src/store/commands/prune.ts +4 -4
- package/src/store/commands/reindexs.ts +4 -3
- package/src/tests/cli.integration.spec.ts +136 -0
- package/src/utils/input.ts +9 -4
- package/src/utils/resolve-default-store.spec.ts +135 -0
- package/src/utils/resolve-default-store.ts +74 -0
- package/dist/category/commands/create.d.ts +0 -44
- package/dist/category/commands/create.d.ts.map +0 -1
- package/dist/category/commands/create.spec.d.ts +0 -7
- package/dist/category/commands/create.spec.d.ts.map +0 -1
- package/dist/category/index.d.ts +0 -19
- package/dist/category/index.d.ts.map +0 -1
- package/dist/commands/init.d.ts +0 -58
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.spec.d.ts +0 -2
- package/dist/commands/init.spec.d.ts.map +0 -1
- package/dist/context.d.ts +0 -18
- package/dist/context.d.ts.map +0 -1
- package/dist/context.spec.d.ts +0 -2
- package/dist/context.spec.d.ts.map +0 -1
- package/dist/create-cli-command.d.ts +0 -23
- package/dist/create-cli-command.d.ts.map +0 -1
- package/dist/create-cli-command.spec.d.ts +0 -10
- package/dist/create-cli-command.spec.d.ts.map +0 -1
- package/dist/errors.d.ts +0 -57
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.spec.d.ts +0 -2
- package/dist/errors.spec.d.ts.map +0 -1
- package/dist/input.d.ts +0 -42
- package/dist/input.d.ts.map +0 -1
- package/dist/input.spec.d.ts +0 -2
- package/dist/input.spec.d.ts.map +0 -1
- package/dist/memory/commands/add.d.ts +0 -62
- package/dist/memory/commands/add.d.ts.map +0 -1
- package/dist/memory/commands/add.spec.d.ts +0 -7
- package/dist/memory/commands/add.spec.d.ts.map +0 -1
- package/dist/memory/commands/definitions.spec.d.ts +0 -10
- package/dist/memory/commands/definitions.spec.d.ts.map +0 -1
- package/dist/memory/commands/handlers.spec.d.ts +0 -2
- package/dist/memory/commands/handlers.spec.d.ts.map +0 -1
- package/dist/memory/commands/list.d.ts +0 -119
- package/dist/memory/commands/list.d.ts.map +0 -1
- package/dist/memory/commands/list.spec.d.ts +0 -2
- package/dist/memory/commands/list.spec.d.ts.map +0 -1
- package/dist/memory/commands/move.d.ts +0 -42
- package/dist/memory/commands/move.d.ts.map +0 -1
- package/dist/memory/commands/move.spec.d.ts +0 -2
- package/dist/memory/commands/move.spec.d.ts.map +0 -1
- package/dist/memory/commands/remove.d.ts +0 -41
- package/dist/memory/commands/remove.d.ts.map +0 -1
- package/dist/memory/commands/remove.spec.d.ts +0 -2
- package/dist/memory/commands/remove.spec.d.ts.map +0 -1
- package/dist/memory/commands/show.d.ts +0 -81
- package/dist/memory/commands/show.d.ts.map +0 -1
- package/dist/memory/commands/show.spec.d.ts +0 -2
- package/dist/memory/commands/show.spec.d.ts.map +0 -1
- package/dist/memory/commands/test-helpers.spec.d.ts +0 -19
- package/dist/memory/commands/test-helpers.spec.d.ts.map +0 -1
- package/dist/memory/commands/update.d.ts +0 -73
- package/dist/memory/commands/update.d.ts.map +0 -1
- package/dist/memory/commands/update.spec.d.ts +0 -2
- package/dist/memory/commands/update.spec.d.ts.map +0 -1
- package/dist/memory/index.d.ts +0 -29
- package/dist/memory/index.d.ts.map +0 -1
- package/dist/memory/index.spec.d.ts +0 -10
- package/dist/memory/index.spec.d.ts.map +0 -1
- package/dist/memory/parsing.d.ts +0 -3
- package/dist/memory/parsing.d.ts.map +0 -1
- package/dist/memory/parsing.spec.d.ts +0 -7
- package/dist/memory/parsing.spec.d.ts.map +0 -1
- package/dist/output.d.ts +0 -87
- package/dist/output.d.ts.map +0 -1
- package/dist/output.spec.d.ts +0 -2
- package/dist/output.spec.d.ts.map +0 -1
- package/dist/paths.d.ts +0 -27
- package/dist/paths.d.ts.map +0 -1
- package/dist/paths.spec.d.ts +0 -7
- package/dist/paths.spec.d.ts.map +0 -1
- package/dist/program.d.ts +0 -41
- package/dist/program.d.ts.map +0 -1
- package/dist/program.spec.d.ts +0 -11
- package/dist/program.spec.d.ts.map +0 -1
- package/dist/run.d.ts +0 -7
- package/dist/run.d.ts.map +0 -1
- package/dist/run.spec.d.ts +0 -12
- package/dist/run.spec.d.ts.map +0 -1
- package/dist/store/commands/add.d.ts +0 -73
- package/dist/store/commands/add.d.ts.map +0 -1
- package/dist/store/commands/add.spec.d.ts +0 -17
- package/dist/store/commands/add.spec.d.ts.map +0 -1
- package/dist/store/commands/init.d.ts +0 -75
- package/dist/store/commands/init.d.ts.map +0 -1
- package/dist/store/commands/init.spec.d.ts +0 -7
- package/dist/store/commands/init.spec.d.ts.map +0 -1
- package/dist/store/commands/list.d.ts +0 -62
- package/dist/store/commands/list.d.ts.map +0 -1
- package/dist/store/commands/list.spec.d.ts +0 -7
- package/dist/store/commands/list.spec.d.ts.map +0 -1
- package/dist/store/commands/prune.d.ts +0 -92
- package/dist/store/commands/prune.d.ts.map +0 -1
- package/dist/store/commands/prune.spec.d.ts +0 -7
- package/dist/store/commands/prune.spec.d.ts.map +0 -1
- package/dist/store/commands/reindexs.d.ts +0 -54
- package/dist/store/commands/reindexs.d.ts.map +0 -1
- package/dist/store/commands/reindexs.spec.d.ts +0 -7
- package/dist/store/commands/reindexs.spec.d.ts.map +0 -1
- package/dist/store/commands/remove.d.ts +0 -63
- package/dist/store/commands/remove.d.ts.map +0 -1
- package/dist/store/commands/remove.spec.d.ts +0 -17
- package/dist/store/commands/remove.spec.d.ts.map +0 -1
- package/dist/store/index.d.ts +0 -32
- package/dist/store/index.d.ts.map +0 -1
- package/dist/store/index.spec.d.ts +0 -9
- package/dist/store/index.spec.d.ts.map +0 -1
- package/dist/store/utils/resolve-store-name.d.ts +0 -30
- package/dist/store/utils/resolve-store-name.d.ts.map +0 -1
- package/dist/store/utils/resolve-store-name.spec.d.ts +0 -2
- package/dist/store/utils/resolve-store-name.spec.d.ts.map +0 -1
- package/dist/test-helpers.spec.d.ts +0 -224
- package/dist/test-helpers.spec.d.ts.map +0 -1
- package/dist/tests/cli.integration.spec.d.ts +0 -11
- package/dist/tests/cli.integration.spec.d.ts.map +0 -1
- package/dist/toon.d.ts +0 -197
- package/dist/toon.d.ts.map +0 -1
- package/dist/toon.spec.d.ts +0 -9
- package/dist/toon.spec.d.ts.map +0 -1
- package/dist/utils/git.d.ts +0 -20
- package/dist/utils/git.d.ts.map +0 -1
- package/dist/utils/git.spec.d.ts +0 -7
- package/dist/utils/git.spec.d.ts.map +0 -1
|
@@ -12,28 +12,25 @@ import { join } from 'node:path';
|
|
|
12
12
|
import type { PromptDeps } from '../../utils/prompts.ts';
|
|
13
13
|
|
|
14
14
|
import { handleInit } from './init.ts';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
saveStore: async () => ({ ok: () => true as const, value: undefined }),
|
|
35
|
-
} as any,
|
|
36
|
-
});
|
|
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 };
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
describe('handleInit', () => {
|
|
@@ -48,10 +45,7 @@ describe('handleInit', () => {
|
|
|
48
45
|
});
|
|
49
46
|
|
|
50
47
|
it('should initialize a store and write success message to stdout', async () => {
|
|
51
|
-
const { ctx, stdout } =
|
|
52
|
-
adapter: createInitAdapter(),
|
|
53
|
-
cwd: tempDir,
|
|
54
|
-
});
|
|
48
|
+
const { ctx, stdout } = await createInitContext(tempDir);
|
|
55
49
|
|
|
56
50
|
await handleInit(ctx, undefined, { name: 'my-project', format: 'yaml' });
|
|
57
51
|
|
|
@@ -61,10 +55,7 @@ describe('handleInit', () => {
|
|
|
61
55
|
|
|
62
56
|
it('should use the provided target path as the store path', async () => {
|
|
63
57
|
const customPath = join(tempDir, 'custom-store');
|
|
64
|
-
const { ctx, stdout } =
|
|
65
|
-
adapter: createInitAdapter(),
|
|
66
|
-
cwd: tempDir,
|
|
67
|
-
});
|
|
58
|
+
const { ctx, stdout } = await createInitContext(tempDir);
|
|
68
59
|
|
|
69
60
|
await handleInit(ctx, customPath, { name: 'my-project', format: 'yaml' });
|
|
70
61
|
|
|
@@ -73,10 +64,7 @@ describe('handleInit', () => {
|
|
|
73
64
|
});
|
|
74
65
|
|
|
75
66
|
it('should default to .cortex under cwd when no target path is given', async () => {
|
|
76
|
-
const { ctx, stdout } =
|
|
77
|
-
adapter: createInitAdapter(),
|
|
78
|
-
cwd: tempDir,
|
|
79
|
-
});
|
|
67
|
+
const { ctx, stdout } = await createInitContext(tempDir);
|
|
80
68
|
|
|
81
69
|
await handleInit(ctx, undefined, { name: 'my-project', format: 'yaml' });
|
|
82
70
|
|
|
@@ -86,10 +74,7 @@ describe('handleInit', () => {
|
|
|
86
74
|
});
|
|
87
75
|
|
|
88
76
|
it('should throw InvalidArgumentError for an invalid store name', async () => {
|
|
89
|
-
const { ctx } =
|
|
90
|
-
adapter: createInitAdapter(),
|
|
91
|
-
cwd: tempDir,
|
|
92
|
-
});
|
|
77
|
+
const { ctx } = await createInitContext(tempDir);
|
|
93
78
|
|
|
94
79
|
await expect(handleInit(ctx, undefined, { name: ' ', format: 'yaml' })).rejects.toThrow(
|
|
95
80
|
InvalidArgumentError,
|
|
@@ -97,10 +82,7 @@ describe('handleInit', () => {
|
|
|
97
82
|
});
|
|
98
83
|
|
|
99
84
|
it('should output in JSON format when format option is json', async () => {
|
|
100
|
-
const { ctx, stdout } =
|
|
101
|
-
adapter: createInitAdapter(),
|
|
102
|
-
cwd: tempDir,
|
|
103
|
-
});
|
|
85
|
+
const { ctx, stdout } = await createInitContext(tempDir);
|
|
104
86
|
|
|
105
87
|
await handleInit(ctx, undefined, { name: 'my-project', format: 'json' });
|
|
106
88
|
|
|
@@ -111,10 +93,7 @@ describe('handleInit', () => {
|
|
|
111
93
|
});
|
|
112
94
|
|
|
113
95
|
it('should include the store name in the success output', async () => {
|
|
114
|
-
const { ctx, stdout } =
|
|
115
|
-
adapter: createInitAdapter(),
|
|
116
|
-
cwd: tempDir,
|
|
117
|
-
});
|
|
96
|
+
const { ctx, stdout } = await createInitContext(tempDir);
|
|
118
97
|
|
|
119
98
|
await handleInit(ctx, undefined, { name: 'hello-world', format: 'json' });
|
|
120
99
|
|
|
@@ -124,10 +103,7 @@ describe('handleInit', () => {
|
|
|
124
103
|
});
|
|
125
104
|
|
|
126
105
|
it('should expand tilde in target path', async () => {
|
|
127
|
-
const { ctx, stdout } =
|
|
128
|
-
adapter: createInitAdapter(),
|
|
129
|
-
cwd: tempDir,
|
|
130
|
-
});
|
|
106
|
+
const { ctx, stdout } = await createInitContext(tempDir);
|
|
131
107
|
|
|
132
108
|
// resolveUserPath expands ~ — we just verify it doesn't throw and
|
|
133
109
|
// produces output with a home-like absolute path
|
|
@@ -136,6 +112,19 @@ describe('handleInit', () => {
|
|
|
136
112
|
const out = captureOutput(stdout);
|
|
137
113
|
expect(out).toContain('my-store');
|
|
138
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
|
+
});
|
|
139
128
|
});
|
|
140
129
|
|
|
141
130
|
describe('handleInit - interactive mode', () => {
|
|
@@ -150,13 +139,12 @@ describe('handleInit - interactive mode', () => {
|
|
|
150
139
|
});
|
|
151
140
|
|
|
152
141
|
it('should call promptDeps.input once (path only) when stdin is a TTY and --name is explicitly given', async () => {
|
|
153
|
-
const inputMock = mock(
|
|
142
|
+
const inputMock = mock(
|
|
143
|
+
async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value',
|
|
144
|
+
);
|
|
154
145
|
const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
|
|
155
146
|
|
|
156
|
-
const { ctx, stdin } =
|
|
157
|
-
adapter: createInitAdapter(),
|
|
158
|
-
cwd: tempDir,
|
|
159
|
-
});
|
|
147
|
+
const { ctx, stdin } = await createInitContext(tempDir);
|
|
160
148
|
(stdin as unknown as { isTTY: boolean }).isTTY = true;
|
|
161
149
|
|
|
162
150
|
await handleInit(ctx, undefined, { name: 'my-project', format: 'yaml' }, promptDeps);
|
|
@@ -167,13 +155,12 @@ describe('handleInit - interactive mode', () => {
|
|
|
167
155
|
});
|
|
168
156
|
|
|
169
157
|
it('should call promptDeps.input twice (name + path) when stdin is a TTY and no --name given', async () => {
|
|
170
|
-
const inputMock = mock(
|
|
158
|
+
const inputMock = mock(
|
|
159
|
+
async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value',
|
|
160
|
+
);
|
|
171
161
|
const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
|
|
172
162
|
|
|
173
|
-
const { ctx, stdin } =
|
|
174
|
-
adapter: createInitAdapter(),
|
|
175
|
-
cwd: tempDir,
|
|
176
|
-
});
|
|
163
|
+
const { ctx, stdin } = await createInitContext(tempDir);
|
|
177
164
|
(stdin as unknown as { isTTY: boolean }).isTTY = true;
|
|
178
165
|
|
|
179
166
|
// No --name provided, should prompt for both name and path
|
|
@@ -183,13 +170,12 @@ describe('handleInit - interactive mode', () => {
|
|
|
183
170
|
});
|
|
184
171
|
|
|
185
172
|
it('should NOT call promptDeps.input when stdin is NOT a TTY', async () => {
|
|
186
|
-
const inputMock = mock(
|
|
173
|
+
const inputMock = mock(
|
|
174
|
+
async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value',
|
|
175
|
+
);
|
|
187
176
|
const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
|
|
188
177
|
|
|
189
|
-
const { ctx } =
|
|
190
|
-
adapter: createInitAdapter(),
|
|
191
|
-
cwd: tempDir,
|
|
192
|
-
});
|
|
178
|
+
const { ctx } = await createInitContext(tempDir);
|
|
193
179
|
// ctx stdin has isTTY = undefined (non-TTY)
|
|
194
180
|
|
|
195
181
|
await handleInit(ctx, undefined, { name: 'my-project', format: 'yaml' }, promptDeps);
|
|
@@ -198,13 +184,12 @@ describe('handleInit - interactive mode', () => {
|
|
|
198
184
|
});
|
|
199
185
|
|
|
200
186
|
it('should skip both prompts when --name and target path are both given explicitly and TTY', async () => {
|
|
201
|
-
const inputMock = mock(
|
|
187
|
+
const inputMock = mock(
|
|
188
|
+
async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value',
|
|
189
|
+
);
|
|
202
190
|
const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
|
|
203
191
|
|
|
204
|
-
const { ctx, stdin } =
|
|
205
|
-
adapter: createInitAdapter(),
|
|
206
|
-
cwd: tempDir,
|
|
207
|
-
});
|
|
192
|
+
const { ctx, stdin } = await createInitContext(tempDir);
|
|
208
193
|
(stdin as unknown as { isTTY: boolean }).isTTY = true;
|
|
209
194
|
const customPath = join(tempDir, 'custom-store');
|
|
210
195
|
|
|
@@ -215,16 +200,15 @@ describe('handleInit - interactive mode', () => {
|
|
|
215
200
|
|
|
216
201
|
it('should use prompted store name in the output', async () => {
|
|
217
202
|
const promptedName = 'prompted-store-name';
|
|
218
|
-
const inputMock = mock(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
+
);
|
|
222
209
|
const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
|
|
223
210
|
|
|
224
|
-
const { ctx, stdin, stdout } =
|
|
225
|
-
adapter: createInitAdapter(),
|
|
226
|
-
cwd: tempDir,
|
|
227
|
-
});
|
|
211
|
+
const { ctx, stdin, stdout } = await createInitContext(tempDir);
|
|
228
212
|
(stdin as unknown as { isTTY: boolean }).isTTY = true;
|
|
229
213
|
|
|
230
214
|
await handleInit(ctx, undefined, { format: 'json' }, promptDeps);
|
|
@@ -24,14 +24,17 @@
|
|
|
24
24
|
|
|
25
25
|
import { Command } from '@commander-js/extra-typings';
|
|
26
26
|
import { resolve } from 'node:path';
|
|
27
|
+
import { stdin, stdout as processStdout } from 'node:process';
|
|
27
28
|
import { resolveStoreName } from '../utils/resolve-store-name.ts';
|
|
28
29
|
import { throwCliError } from '../../errors.ts';
|
|
29
|
-
import { type StoreData } from '@yeseh/cortex-core/store';
|
|
30
|
+
import { type StoreData, initializeStore } from '@yeseh/cortex-core/store';
|
|
30
31
|
import { type CategoryMode, type CortexContext } from '@yeseh/cortex-core';
|
|
31
32
|
import { serializeOutput, type OutputStoreInit, type OutputFormat } from '../../output.ts';
|
|
32
33
|
import { resolveUserPath } from '../../utils/paths.ts';
|
|
33
|
-
import {
|
|
34
|
+
import { createCliConfigContext } from '../../context.ts';
|
|
34
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';
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
40
|
* Options for the init command.
|
|
@@ -95,7 +98,8 @@ async function promptStoreInitOptions(
|
|
|
95
98
|
let storePath: string;
|
|
96
99
|
if (explicit.path) {
|
|
97
100
|
storePath = resolved.storePath;
|
|
98
|
-
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
99
103
|
const promptedPath = await promptDeps.input({
|
|
100
104
|
message: 'Store path:',
|
|
101
105
|
default: resolved.storePath,
|
|
@@ -123,7 +127,8 @@ async function resolveStoreNameOrEmpty(
|
|
|
123
127
|
): Promise<string> {
|
|
124
128
|
try {
|
|
125
129
|
return await resolveStoreName(cwd, explicitName);
|
|
126
|
-
}
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
127
132
|
// When running in a TTY, only swallow errors (and fall back to prompting)
|
|
128
133
|
// if no explicit name was provided. If the user passed an explicit --name,
|
|
129
134
|
// re-throw so they see the actual invalid-name error.
|
|
@@ -194,11 +199,6 @@ export async function handleInit(
|
|
|
194
199
|
});
|
|
195
200
|
}
|
|
196
201
|
|
|
197
|
-
const clientResult = ctx.cortex.getStore(finalStoreName);
|
|
198
|
-
if (!clientResult.ok()) {
|
|
199
|
-
throwCliError(clientResult.error);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
202
|
const storeData: StoreData = {
|
|
203
203
|
kind: 'filesystem',
|
|
204
204
|
categoryMode: (options.categoryMode as CategoryMode) ?? 'free',
|
|
@@ -209,8 +209,10 @@ export async function handleInit(
|
|
|
209
209
|
description: options.description,
|
|
210
210
|
};
|
|
211
211
|
|
|
212
|
-
const
|
|
213
|
-
|
|
212
|
+
const adapter = new FilesystemStorageAdapter(ctx.config as FilesystemConfigAdapter, {
|
|
213
|
+
rootDirectory: finalStorePath,
|
|
214
|
+
});
|
|
215
|
+
const createResult = await initializeStore(adapter, finalStoreName, storeData);
|
|
214
216
|
if (!createResult.ok()) {
|
|
215
217
|
throwCliError(createResult.error);
|
|
216
218
|
}
|
|
@@ -242,12 +244,26 @@ export const initCommand = new Command('init')
|
|
|
242
244
|
.option('-c, --category-mode <mode>', 'Category mode (free, strict, flat)', 'free')
|
|
243
245
|
.option('-o, --format <format>', 'Output format (yaml, json, toon)', 'yaml')
|
|
244
246
|
.action(async (path, options) => {
|
|
245
|
-
const
|
|
246
|
-
if (!
|
|
247
|
-
throwCliError(
|
|
247
|
+
const configCtx = await createCliConfigContext();
|
|
248
|
+
if (!configCtx.ok()) {
|
|
249
|
+
throwCliError(configCtx.error);
|
|
248
250
|
}
|
|
249
251
|
|
|
250
|
-
|
|
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, {
|
|
251
267
|
name: options.name,
|
|
252
268
|
description: options.description,
|
|
253
269
|
categoryMode: options.categoryMode as CategoryMode,
|
|
@@ -23,6 +23,7 @@ import { Command } from '@commander-js/extra-typings';
|
|
|
23
23
|
import { throwCliError } from '../../errors.ts';
|
|
24
24
|
import { type CortexContext } from '@yeseh/cortex-core';
|
|
25
25
|
import { createCliCommandContext } from '../../context.ts';
|
|
26
|
+
import { resolveDefaultStore } from '../../utils/resolve-default-store.ts';
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Options for the prune command.
|
|
@@ -83,13 +84,13 @@ export async function handlePrune(
|
|
|
83
84
|
ctx: CortexContext,
|
|
84
85
|
storeName: string | undefined,
|
|
85
86
|
options: PruneCommandOptions,
|
|
86
|
-
deps: PruneHandlerDeps = {}
|
|
87
|
+
deps: PruneHandlerDeps = {}
|
|
87
88
|
): Promise<void> {
|
|
88
89
|
const now = deps.now ?? ctx.now();
|
|
89
90
|
const stdout = deps.stdout ?? ctx.stdout ?? process.stdout;
|
|
90
91
|
|
|
91
92
|
// Get store through Cortex client
|
|
92
|
-
const storeResult = ctx.cortex.getStore(storeName
|
|
93
|
+
const storeResult = ctx.cortex.getStore(resolveDefaultStore(ctx, storeName));
|
|
93
94
|
if (!storeResult.ok()) {
|
|
94
95
|
throwCliError(storeResult.error);
|
|
95
96
|
}
|
|
@@ -121,8 +122,7 @@ export async function handlePrune(
|
|
|
121
122
|
const paths = pruned.map((entry) => entry.path).join('\n ');
|
|
122
123
|
if (options.dryRun) {
|
|
123
124
|
stdout.write(`Would prune ${pruned.length} expired memories:\n ${paths}\n`);
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
125
|
+
} else {
|
|
126
126
|
stdout.write(`Pruned ${pruned.length} expired memories:\n ${paths}\n`);
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -18,6 +18,7 @@ import { Command } from '@commander-js/extra-typings';
|
|
|
18
18
|
import { throwCliError } from '../../errors.ts';
|
|
19
19
|
import { type CortexContext } from '@yeseh/cortex-core';
|
|
20
20
|
import { createCliCommandContext } from '../../context.ts';
|
|
21
|
+
import { resolveDefaultStore } from '../../utils/resolve-default-store.ts';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Dependencies for the reindex command handler.
|
|
@@ -45,12 +46,12 @@ export interface ReindexHandlerDeps {
|
|
|
45
46
|
export async function handleReindex(
|
|
46
47
|
ctx: CortexContext,
|
|
47
48
|
storeName: string | undefined,
|
|
48
|
-
deps: ReindexHandlerDeps = {}
|
|
49
|
+
deps: ReindexHandlerDeps = {}
|
|
49
50
|
): Promise<void> {
|
|
50
51
|
const stdout = deps.stdout ?? ctx.stdout ?? process.stdout;
|
|
51
|
-
|
|
52
|
+
|
|
52
53
|
// Get store through Cortex client
|
|
53
|
-
const effectiveStoreName = storeName
|
|
54
|
+
const effectiveStoreName = resolveDefaultStore(ctx, storeName);
|
|
54
55
|
const storeResult = ctx.cortex.getStore(effectiveStoreName);
|
|
55
56
|
if (!storeResult.ok()) {
|
|
56
57
|
throwCliError(storeResult.error);
|
|
@@ -1167,4 +1167,140 @@ describe('Cortex CLI Integration Tests', () => {
|
|
|
1167
1167
|
expect(result.stdout).toContain('reindex');
|
|
1168
1168
|
});
|
|
1169
1169
|
});
|
|
1170
|
+
|
|
1171
|
+
describe('store init command', () => {
|
|
1172
|
+
it('should initialize a new store when no store is configured', async () => {
|
|
1173
|
+
// Create an isolated directory with no pre-configured stores — the
|
|
1174
|
+
// config.yaml exists but has no store entries, simulating a fresh install.
|
|
1175
|
+
const freshDir = await fs.mkdtemp(join(tmpdir(), 'cortex-store-init-'));
|
|
1176
|
+
const configDir = join(freshDir, '.config', 'cortex');
|
|
1177
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
1178
|
+
await fs.writeFile(join(configDir, 'config.yaml'), 'stores: {}\n', 'utf8');
|
|
1179
|
+
|
|
1180
|
+
const storePath = join(freshDir, '.cortex', 'memory');
|
|
1181
|
+
|
|
1182
|
+
try {
|
|
1183
|
+
const result = await runCortexCli(
|
|
1184
|
+
[
|
|
1185
|
+
'store',
|
|
1186
|
+
'init',
|
|
1187
|
+
storePath,
|
|
1188
|
+
'--name',
|
|
1189
|
+
'my-project',
|
|
1190
|
+
'--format',
|
|
1191
|
+
'json',
|
|
1192
|
+
],
|
|
1193
|
+
{
|
|
1194
|
+
cwd: freshDir,
|
|
1195
|
+
env: { CORTEX_CONFIG_DIR: configDir },
|
|
1196
|
+
},
|
|
1197
|
+
);
|
|
1198
|
+
|
|
1199
|
+
expectCliOk([
|
|
1200
|
+
'store',
|
|
1201
|
+
'init',
|
|
1202
|
+
storePath,
|
|
1203
|
+
'--name',
|
|
1204
|
+
'my-project',
|
|
1205
|
+
], result);
|
|
1206
|
+
expect(result.exitCode).toBe(0);
|
|
1207
|
+
|
|
1208
|
+
const parsed = JSON.parse(result.stdout) as {
|
|
1209
|
+
value: { name: string; path: string };
|
|
1210
|
+
};
|
|
1211
|
+
expect(parsed.value.name).toBe('my-project');
|
|
1212
|
+
expect(parsed.value.path).toBe(storePath);
|
|
1213
|
+
|
|
1214
|
+
// Verify the store was registered in config.yaml
|
|
1215
|
+
const configContent = await fs.readFile(join(configDir, 'config.yaml'), 'utf8');
|
|
1216
|
+
expect(configContent).toContain('my-project');
|
|
1217
|
+
expect(configContent).toContain(storePath);
|
|
1218
|
+
}
|
|
1219
|
+
finally {
|
|
1220
|
+
await fs.rm(freshDir, { recursive: true, force: true });
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it('should create the store directory when it does not yet exist', async () => {
|
|
1225
|
+
const freshDir = await fs.mkdtemp(join(tmpdir(), 'cortex-store-init-newdir-'));
|
|
1226
|
+
const configDir = join(freshDir, '.config', 'cortex');
|
|
1227
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
1228
|
+
await fs.writeFile(join(configDir, 'config.yaml'), 'stores: {}\n', 'utf8');
|
|
1229
|
+
|
|
1230
|
+
// Point at a path that does NOT yet exist on disk
|
|
1231
|
+
const storePath = join(freshDir, 'brand-new-store');
|
|
1232
|
+
|
|
1233
|
+
try {
|
|
1234
|
+
const result = await runCortexCli(
|
|
1235
|
+
[
|
|
1236
|
+
'store',
|
|
1237
|
+
'init',
|
|
1238
|
+
storePath,
|
|
1239
|
+
'--name',
|
|
1240
|
+
'new-dir-store',
|
|
1241
|
+
'--format',
|
|
1242
|
+
'json',
|
|
1243
|
+
],
|
|
1244
|
+
{
|
|
1245
|
+
cwd: freshDir,
|
|
1246
|
+
env: { CORTEX_CONFIG_DIR: configDir },
|
|
1247
|
+
},
|
|
1248
|
+
);
|
|
1249
|
+
|
|
1250
|
+
expectCliOk([
|
|
1251
|
+
'store',
|
|
1252
|
+
'init',
|
|
1253
|
+
storePath,
|
|
1254
|
+
'--name',
|
|
1255
|
+
'new-dir-store',
|
|
1256
|
+
], result);
|
|
1257
|
+
|
|
1258
|
+
// The directory must have been created by the init command
|
|
1259
|
+
const dirStat = await fs.stat(storePath);
|
|
1260
|
+
expect(dirStat.isDirectory()).toBe(true);
|
|
1261
|
+
}
|
|
1262
|
+
finally {
|
|
1263
|
+
await fs.rm(freshDir, { recursive: true, force: true });
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
it('should fail when the store name already exists', async () => {
|
|
1268
|
+
// Register a store first using the existing testProject setup
|
|
1269
|
+
const storePath = join(testProject, '.cortex', 'memory2');
|
|
1270
|
+
const firstInit = await runCortexCli(
|
|
1271
|
+
[
|
|
1272
|
+
'store',
|
|
1273
|
+
'init',
|
|
1274
|
+
storePath,
|
|
1275
|
+
'--name',
|
|
1276
|
+
'existing-store',
|
|
1277
|
+
'--format',
|
|
1278
|
+
'json',
|
|
1279
|
+
],
|
|
1280
|
+
{ cwd: testProject },
|
|
1281
|
+
);
|
|
1282
|
+
expectCliOk([
|
|
1283
|
+
'store',
|
|
1284
|
+
'init',
|
|
1285
|
+
storePath,
|
|
1286
|
+
'--name',
|
|
1287
|
+
'existing-store',
|
|
1288
|
+
], firstInit);
|
|
1289
|
+
|
|
1290
|
+
// Attempt to init the same name again — should fail
|
|
1291
|
+
const secondInit = await runCortexCli(
|
|
1292
|
+
[
|
|
1293
|
+
'store',
|
|
1294
|
+
'init',
|
|
1295
|
+
join(testProject, '.cortex', 'memory3'),
|
|
1296
|
+
'--name',
|
|
1297
|
+
'existing-store',
|
|
1298
|
+
],
|
|
1299
|
+
{ cwd: testProject },
|
|
1300
|
+
);
|
|
1301
|
+
|
|
1302
|
+
expect(secondInit.exitCode).toBe(1);
|
|
1303
|
+
expect(secondInit.stderr.toLowerCase()).toMatch(/already exists/);
|
|
1304
|
+
});
|
|
1305
|
+
});
|
|
1170
1306
|
});
|
package/src/utils/input.ts
CHANGED
|
@@ -49,7 +49,7 @@ type InputResult = Result<InputContent, InputError>;
|
|
|
49
49
|
type OptionalContentResult = Result<InputContent | null, InputError>;
|
|
50
50
|
|
|
51
51
|
export const readContentFromFile = async (
|
|
52
|
-
filePath: string | undefined
|
|
52
|
+
filePath: string | undefined,
|
|
53
53
|
): Promise<OptionalContentResult> => {
|
|
54
54
|
if (filePath === undefined) {
|
|
55
55
|
return ok(null);
|
|
@@ -64,7 +64,8 @@ export const readContentFromFile = async (
|
|
|
64
64
|
try {
|
|
65
65
|
const content = await readFile(trimmed, 'utf8');
|
|
66
66
|
return ok({ content, source: 'file' });
|
|
67
|
-
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
68
69
|
return err({
|
|
69
70
|
code: 'FILE_READ_FAILED',
|
|
70
71
|
message: `Failed to read content file: ${trimmed}.`,
|
|
@@ -75,7 +76,7 @@ export const readContentFromFile = async (
|
|
|
75
76
|
};
|
|
76
77
|
|
|
77
78
|
export const readContentFromStream = async (
|
|
78
|
-
stream: NodeJS.ReadableStream
|
|
79
|
+
stream: NodeJS.ReadableStream,
|
|
79
80
|
): Promise<OptionalContentResult> => {
|
|
80
81
|
const isTty = 'isTTY' in stream ? Boolean(stream.isTTY) : false;
|
|
81
82
|
if (isTty) {
|
|
@@ -110,7 +111,11 @@ export const resolveInput = async (source: InputSource): Promise<InputResult> =>
|
|
|
110
111
|
source.stream !== undefined &&
|
|
111
112
|
!('isTTY' in source.stream && Boolean((source.stream as { isTTY?: boolean }).isTTY));
|
|
112
113
|
|
|
113
|
-
const requestedSources = [
|
|
114
|
+
const requestedSources = [
|
|
115
|
+
contentProvided,
|
|
116
|
+
fileProvided,
|
|
117
|
+
streamRequested,
|
|
118
|
+
].filter(Boolean);
|
|
114
119
|
|
|
115
120
|
if (requestedSources.length > 1) {
|
|
116
121
|
return err({
|