@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.
Files changed (145) hide show
  1. package/package.json +3 -3
  2. package/src/category/commands/create.ts +7 -3
  3. package/src/context.ts +15 -20
  4. package/src/memory/commands/add.ts +2 -1
  5. package/src/memory/commands/list.ts +2 -1
  6. package/src/memory/commands/move.ts +6 -2
  7. package/src/memory/commands/remove.ts +6 -2
  8. package/src/memory/commands/show.ts +3 -2
  9. package/src/memory/commands/update.ts +2 -1
  10. package/src/observability.spec.ts +8 -21
  11. package/src/observability.ts +26 -7
  12. package/src/program.ts +1 -1
  13. package/src/run.ts +0 -0
  14. package/src/store/commands/init.spec.ts +62 -78
  15. package/src/store/commands/init.ts +31 -15
  16. package/src/store/commands/prune.ts +4 -4
  17. package/src/store/commands/reindexs.ts +4 -3
  18. package/src/tests/cli.integration.spec.ts +136 -0
  19. package/src/utils/input.ts +9 -4
  20. package/src/utils/resolve-default-store.spec.ts +135 -0
  21. package/src/utils/resolve-default-store.ts +74 -0
  22. package/dist/category/commands/create.d.ts +0 -44
  23. package/dist/category/commands/create.d.ts.map +0 -1
  24. package/dist/category/commands/create.spec.d.ts +0 -7
  25. package/dist/category/commands/create.spec.d.ts.map +0 -1
  26. package/dist/category/index.d.ts +0 -19
  27. package/dist/category/index.d.ts.map +0 -1
  28. package/dist/commands/init.d.ts +0 -58
  29. package/dist/commands/init.d.ts.map +0 -1
  30. package/dist/commands/init.spec.d.ts +0 -2
  31. package/dist/commands/init.spec.d.ts.map +0 -1
  32. package/dist/context.d.ts +0 -18
  33. package/dist/context.d.ts.map +0 -1
  34. package/dist/context.spec.d.ts +0 -2
  35. package/dist/context.spec.d.ts.map +0 -1
  36. package/dist/create-cli-command.d.ts +0 -23
  37. package/dist/create-cli-command.d.ts.map +0 -1
  38. package/dist/create-cli-command.spec.d.ts +0 -10
  39. package/dist/create-cli-command.spec.d.ts.map +0 -1
  40. package/dist/errors.d.ts +0 -57
  41. package/dist/errors.d.ts.map +0 -1
  42. package/dist/errors.spec.d.ts +0 -2
  43. package/dist/errors.spec.d.ts.map +0 -1
  44. package/dist/input.d.ts +0 -42
  45. package/dist/input.d.ts.map +0 -1
  46. package/dist/input.spec.d.ts +0 -2
  47. package/dist/input.spec.d.ts.map +0 -1
  48. package/dist/memory/commands/add.d.ts +0 -62
  49. package/dist/memory/commands/add.d.ts.map +0 -1
  50. package/dist/memory/commands/add.spec.d.ts +0 -7
  51. package/dist/memory/commands/add.spec.d.ts.map +0 -1
  52. package/dist/memory/commands/definitions.spec.d.ts +0 -10
  53. package/dist/memory/commands/definitions.spec.d.ts.map +0 -1
  54. package/dist/memory/commands/handlers.spec.d.ts +0 -2
  55. package/dist/memory/commands/handlers.spec.d.ts.map +0 -1
  56. package/dist/memory/commands/list.d.ts +0 -119
  57. package/dist/memory/commands/list.d.ts.map +0 -1
  58. package/dist/memory/commands/list.spec.d.ts +0 -2
  59. package/dist/memory/commands/list.spec.d.ts.map +0 -1
  60. package/dist/memory/commands/move.d.ts +0 -42
  61. package/dist/memory/commands/move.d.ts.map +0 -1
  62. package/dist/memory/commands/move.spec.d.ts +0 -2
  63. package/dist/memory/commands/move.spec.d.ts.map +0 -1
  64. package/dist/memory/commands/remove.d.ts +0 -41
  65. package/dist/memory/commands/remove.d.ts.map +0 -1
  66. package/dist/memory/commands/remove.spec.d.ts +0 -2
  67. package/dist/memory/commands/remove.spec.d.ts.map +0 -1
  68. package/dist/memory/commands/show.d.ts +0 -81
  69. package/dist/memory/commands/show.d.ts.map +0 -1
  70. package/dist/memory/commands/show.spec.d.ts +0 -2
  71. package/dist/memory/commands/show.spec.d.ts.map +0 -1
  72. package/dist/memory/commands/test-helpers.spec.d.ts +0 -19
  73. package/dist/memory/commands/test-helpers.spec.d.ts.map +0 -1
  74. package/dist/memory/commands/update.d.ts +0 -73
  75. package/dist/memory/commands/update.d.ts.map +0 -1
  76. package/dist/memory/commands/update.spec.d.ts +0 -2
  77. package/dist/memory/commands/update.spec.d.ts.map +0 -1
  78. package/dist/memory/index.d.ts +0 -29
  79. package/dist/memory/index.d.ts.map +0 -1
  80. package/dist/memory/index.spec.d.ts +0 -10
  81. package/dist/memory/index.spec.d.ts.map +0 -1
  82. package/dist/memory/parsing.d.ts +0 -3
  83. package/dist/memory/parsing.d.ts.map +0 -1
  84. package/dist/memory/parsing.spec.d.ts +0 -7
  85. package/dist/memory/parsing.spec.d.ts.map +0 -1
  86. package/dist/output.d.ts +0 -87
  87. package/dist/output.d.ts.map +0 -1
  88. package/dist/output.spec.d.ts +0 -2
  89. package/dist/output.spec.d.ts.map +0 -1
  90. package/dist/paths.d.ts +0 -27
  91. package/dist/paths.d.ts.map +0 -1
  92. package/dist/paths.spec.d.ts +0 -7
  93. package/dist/paths.spec.d.ts.map +0 -1
  94. package/dist/program.d.ts +0 -41
  95. package/dist/program.d.ts.map +0 -1
  96. package/dist/program.spec.d.ts +0 -11
  97. package/dist/program.spec.d.ts.map +0 -1
  98. package/dist/run.d.ts +0 -7
  99. package/dist/run.d.ts.map +0 -1
  100. package/dist/run.spec.d.ts +0 -12
  101. package/dist/run.spec.d.ts.map +0 -1
  102. package/dist/store/commands/add.d.ts +0 -73
  103. package/dist/store/commands/add.d.ts.map +0 -1
  104. package/dist/store/commands/add.spec.d.ts +0 -17
  105. package/dist/store/commands/add.spec.d.ts.map +0 -1
  106. package/dist/store/commands/init.d.ts +0 -75
  107. package/dist/store/commands/init.d.ts.map +0 -1
  108. package/dist/store/commands/init.spec.d.ts +0 -7
  109. package/dist/store/commands/init.spec.d.ts.map +0 -1
  110. package/dist/store/commands/list.d.ts +0 -62
  111. package/dist/store/commands/list.d.ts.map +0 -1
  112. package/dist/store/commands/list.spec.d.ts +0 -7
  113. package/dist/store/commands/list.spec.d.ts.map +0 -1
  114. package/dist/store/commands/prune.d.ts +0 -92
  115. package/dist/store/commands/prune.d.ts.map +0 -1
  116. package/dist/store/commands/prune.spec.d.ts +0 -7
  117. package/dist/store/commands/prune.spec.d.ts.map +0 -1
  118. package/dist/store/commands/reindexs.d.ts +0 -54
  119. package/dist/store/commands/reindexs.d.ts.map +0 -1
  120. package/dist/store/commands/reindexs.spec.d.ts +0 -7
  121. package/dist/store/commands/reindexs.spec.d.ts.map +0 -1
  122. package/dist/store/commands/remove.d.ts +0 -63
  123. package/dist/store/commands/remove.d.ts.map +0 -1
  124. package/dist/store/commands/remove.spec.d.ts +0 -17
  125. package/dist/store/commands/remove.spec.d.ts.map +0 -1
  126. package/dist/store/index.d.ts +0 -32
  127. package/dist/store/index.d.ts.map +0 -1
  128. package/dist/store/index.spec.d.ts +0 -9
  129. package/dist/store/index.spec.d.ts.map +0 -1
  130. package/dist/store/utils/resolve-store-name.d.ts +0 -30
  131. package/dist/store/utils/resolve-store-name.d.ts.map +0 -1
  132. package/dist/store/utils/resolve-store-name.spec.d.ts +0 -2
  133. package/dist/store/utils/resolve-store-name.spec.d.ts.map +0 -1
  134. package/dist/test-helpers.spec.d.ts +0 -224
  135. package/dist/test-helpers.spec.d.ts.map +0 -1
  136. package/dist/tests/cli.integration.spec.d.ts +0 -11
  137. package/dist/tests/cli.integration.spec.d.ts.map +0 -1
  138. package/dist/toon.d.ts +0 -197
  139. package/dist/toon.d.ts.map +0 -1
  140. package/dist/toon.spec.d.ts +0 -9
  141. package/dist/toon.spec.d.ts.map +0 -1
  142. package/dist/utils/git.d.ts +0 -20
  143. package/dist/utils/git.d.ts.map +0 -1
  144. package/dist/utils/git.spec.d.ts +0 -7
  145. 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
- createMockContext,
17
- createMockStorageAdapter,
18
- captureOutput,
19
- } from '../../test-helpers.spec.ts';
20
-
21
- // Produce a store adapter whose `stores.load` reports NOT_FOUND (so the new
22
- // store can be created) and `stores.save` succeeds.
23
- function createInitAdapter() {
24
- return createMockStorageAdapter({
25
- config: {
26
- path: '/tmp/cortex-test-config.yaml',
27
- data: null,
28
- stores: null,
29
- settings: null,
30
- initializeConfig: async () => ({ ok: () => true as const, value: undefined }),
31
- getSettings: async () => ({ ok: () => true as const, value: {} }),
32
- getStores: async () => ({ ok: () => true as const, value: {} }),
33
- getStore: async () => ({ ok: () => true as const, value: null }),
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 } = createMockContext({
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 } = createMockContext({
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 } = createMockContext({
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 } = createMockContext({
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 } = createMockContext({
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 } = createMockContext({
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 } = createMockContext({
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(async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value');
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 } = createMockContext({
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(async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value');
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 } = createMockContext({
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(async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value');
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 } = createMockContext({
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(async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value');
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 } = createMockContext({
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(async ({ message, default: d }: { message: string; default?: string }) => {
219
- if (message.toLowerCase().includes('name')) return promptedName;
220
- return d ?? 'default-path';
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 } = createMockContext({
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 { createCliCommandContext } from '../../context.ts';
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
- } else {
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
- } catch (e) {
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 store = clientResult.value;
213
- const createResult = await store.initialize(storeData);
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 context = await createCliCommandContext();
246
- if (!context.ok()) {
247
- throwCliError(context.error);
247
+ const configCtx = await createCliConfigContext();
248
+ if (!configCtx.ok()) {
249
+ throwCliError(configCtx.error);
248
250
  }
249
251
 
250
- await handleInit(context.value, path, {
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 ?? 'global');
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 ?? 'global';
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
  });
@@ -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
- } catch (error) {
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 = [contentProvided, fileProvided, streamRequested].filter(Boolean);
114
+ const requestedSources = [
115
+ contentProvided,
116
+ fileProvided,
117
+ streamRequested,
118
+ ].filter(Boolean);
114
119
 
115
120
  if (requestedSources.length > 1) {
116
121
  return err({