@yeseh/cortex-cli 0.6.0 → 0.6.3

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 (142) 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/run.ts +0 -0
  11. package/src/store/commands/init.spec.ts +62 -78
  12. package/src/store/commands/init.ts +31 -15
  13. package/src/store/commands/prune.ts +4 -4
  14. package/src/store/commands/reindexs.ts +4 -3
  15. package/src/tests/cli.integration.spec.ts +136 -0
  16. package/src/utils/input.ts +9 -4
  17. package/src/utils/resolve-default-store.spec.ts +135 -0
  18. package/src/utils/resolve-default-store.ts +74 -0
  19. package/dist/category/commands/create.d.ts +0 -44
  20. package/dist/category/commands/create.d.ts.map +0 -1
  21. package/dist/category/commands/create.spec.d.ts +0 -7
  22. package/dist/category/commands/create.spec.d.ts.map +0 -1
  23. package/dist/category/index.d.ts +0 -19
  24. package/dist/category/index.d.ts.map +0 -1
  25. package/dist/commands/init.d.ts +0 -58
  26. package/dist/commands/init.d.ts.map +0 -1
  27. package/dist/commands/init.spec.d.ts +0 -2
  28. package/dist/commands/init.spec.d.ts.map +0 -1
  29. package/dist/context.d.ts +0 -18
  30. package/dist/context.d.ts.map +0 -1
  31. package/dist/context.spec.d.ts +0 -2
  32. package/dist/context.spec.d.ts.map +0 -1
  33. package/dist/create-cli-command.d.ts +0 -23
  34. package/dist/create-cli-command.d.ts.map +0 -1
  35. package/dist/create-cli-command.spec.d.ts +0 -10
  36. package/dist/create-cli-command.spec.d.ts.map +0 -1
  37. package/dist/errors.d.ts +0 -57
  38. package/dist/errors.d.ts.map +0 -1
  39. package/dist/errors.spec.d.ts +0 -2
  40. package/dist/errors.spec.d.ts.map +0 -1
  41. package/dist/input.d.ts +0 -42
  42. package/dist/input.d.ts.map +0 -1
  43. package/dist/input.spec.d.ts +0 -2
  44. package/dist/input.spec.d.ts.map +0 -1
  45. package/dist/memory/commands/add.d.ts +0 -62
  46. package/dist/memory/commands/add.d.ts.map +0 -1
  47. package/dist/memory/commands/add.spec.d.ts +0 -7
  48. package/dist/memory/commands/add.spec.d.ts.map +0 -1
  49. package/dist/memory/commands/definitions.spec.d.ts +0 -10
  50. package/dist/memory/commands/definitions.spec.d.ts.map +0 -1
  51. package/dist/memory/commands/handlers.spec.d.ts +0 -2
  52. package/dist/memory/commands/handlers.spec.d.ts.map +0 -1
  53. package/dist/memory/commands/list.d.ts +0 -119
  54. package/dist/memory/commands/list.d.ts.map +0 -1
  55. package/dist/memory/commands/list.spec.d.ts +0 -2
  56. package/dist/memory/commands/list.spec.d.ts.map +0 -1
  57. package/dist/memory/commands/move.d.ts +0 -42
  58. package/dist/memory/commands/move.d.ts.map +0 -1
  59. package/dist/memory/commands/move.spec.d.ts +0 -2
  60. package/dist/memory/commands/move.spec.d.ts.map +0 -1
  61. package/dist/memory/commands/remove.d.ts +0 -41
  62. package/dist/memory/commands/remove.d.ts.map +0 -1
  63. package/dist/memory/commands/remove.spec.d.ts +0 -2
  64. package/dist/memory/commands/remove.spec.d.ts.map +0 -1
  65. package/dist/memory/commands/show.d.ts +0 -81
  66. package/dist/memory/commands/show.d.ts.map +0 -1
  67. package/dist/memory/commands/show.spec.d.ts +0 -2
  68. package/dist/memory/commands/show.spec.d.ts.map +0 -1
  69. package/dist/memory/commands/test-helpers.spec.d.ts +0 -19
  70. package/dist/memory/commands/test-helpers.spec.d.ts.map +0 -1
  71. package/dist/memory/commands/update.d.ts +0 -73
  72. package/dist/memory/commands/update.d.ts.map +0 -1
  73. package/dist/memory/commands/update.spec.d.ts +0 -2
  74. package/dist/memory/commands/update.spec.d.ts.map +0 -1
  75. package/dist/memory/index.d.ts +0 -29
  76. package/dist/memory/index.d.ts.map +0 -1
  77. package/dist/memory/index.spec.d.ts +0 -10
  78. package/dist/memory/index.spec.d.ts.map +0 -1
  79. package/dist/memory/parsing.d.ts +0 -3
  80. package/dist/memory/parsing.d.ts.map +0 -1
  81. package/dist/memory/parsing.spec.d.ts +0 -7
  82. package/dist/memory/parsing.spec.d.ts.map +0 -1
  83. package/dist/output.d.ts +0 -87
  84. package/dist/output.d.ts.map +0 -1
  85. package/dist/output.spec.d.ts +0 -2
  86. package/dist/output.spec.d.ts.map +0 -1
  87. package/dist/paths.d.ts +0 -27
  88. package/dist/paths.d.ts.map +0 -1
  89. package/dist/paths.spec.d.ts +0 -7
  90. package/dist/paths.spec.d.ts.map +0 -1
  91. package/dist/program.d.ts +0 -41
  92. package/dist/program.d.ts.map +0 -1
  93. package/dist/program.spec.d.ts +0 -11
  94. package/dist/program.spec.d.ts.map +0 -1
  95. package/dist/run.d.ts +0 -7
  96. package/dist/run.d.ts.map +0 -1
  97. package/dist/run.spec.d.ts +0 -12
  98. package/dist/run.spec.d.ts.map +0 -1
  99. package/dist/store/commands/add.d.ts +0 -73
  100. package/dist/store/commands/add.d.ts.map +0 -1
  101. package/dist/store/commands/add.spec.d.ts +0 -17
  102. package/dist/store/commands/add.spec.d.ts.map +0 -1
  103. package/dist/store/commands/init.d.ts +0 -75
  104. package/dist/store/commands/init.d.ts.map +0 -1
  105. package/dist/store/commands/init.spec.d.ts +0 -7
  106. package/dist/store/commands/init.spec.d.ts.map +0 -1
  107. package/dist/store/commands/list.d.ts +0 -62
  108. package/dist/store/commands/list.d.ts.map +0 -1
  109. package/dist/store/commands/list.spec.d.ts +0 -7
  110. package/dist/store/commands/list.spec.d.ts.map +0 -1
  111. package/dist/store/commands/prune.d.ts +0 -92
  112. package/dist/store/commands/prune.d.ts.map +0 -1
  113. package/dist/store/commands/prune.spec.d.ts +0 -7
  114. package/dist/store/commands/prune.spec.d.ts.map +0 -1
  115. package/dist/store/commands/reindexs.d.ts +0 -54
  116. package/dist/store/commands/reindexs.d.ts.map +0 -1
  117. package/dist/store/commands/reindexs.spec.d.ts +0 -7
  118. package/dist/store/commands/reindexs.spec.d.ts.map +0 -1
  119. package/dist/store/commands/remove.d.ts +0 -63
  120. package/dist/store/commands/remove.d.ts.map +0 -1
  121. package/dist/store/commands/remove.spec.d.ts +0 -17
  122. package/dist/store/commands/remove.spec.d.ts.map +0 -1
  123. package/dist/store/index.d.ts +0 -32
  124. package/dist/store/index.d.ts.map +0 -1
  125. package/dist/store/index.spec.d.ts +0 -9
  126. package/dist/store/index.spec.d.ts.map +0 -1
  127. package/dist/store/utils/resolve-store-name.d.ts +0 -30
  128. package/dist/store/utils/resolve-store-name.d.ts.map +0 -1
  129. package/dist/store/utils/resolve-store-name.spec.d.ts +0 -2
  130. package/dist/store/utils/resolve-store-name.spec.d.ts.map +0 -1
  131. package/dist/test-helpers.spec.d.ts +0 -224
  132. package/dist/test-helpers.spec.d.ts.map +0 -1
  133. package/dist/tests/cli.integration.spec.d.ts +0 -11
  134. package/dist/tests/cli.integration.spec.d.ts.map +0 -1
  135. package/dist/toon.d.ts +0 -197
  136. package/dist/toon.d.ts.map +0 -1
  137. package/dist/toon.spec.d.ts +0 -9
  138. package/dist/toon.spec.d.ts.map +0 -1
  139. package/dist/utils/git.d.ts +0 -20
  140. package/dist/utils/git.d.ts.map +0 -1
  141. package/dist/utils/git.spec.d.ts +0 -7
  142. package/dist/utils/git.spec.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeseh/cortex-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -34,8 +34,8 @@
34
34
  "typescript": "^5"
35
35
  },
36
36
  "dependencies": {
37
- "@yeseh/cortex-core": "0.6.0",
38
- "@yeseh/cortex-storage-fs": "0.6.0",
37
+ "@yeseh/cortex-core": "workspace:*",
38
+ "@yeseh/cortex-storage-fs": "workspace:*",
39
39
  "@commander-js/extra-typings": "^14.0.0",
40
40
  "@inquirer/prompts": "^8.0.0",
41
41
  "@toon-format/toon": "^1.0.0",
@@ -22,6 +22,7 @@ import { type CortexContext, type Result } from '@yeseh/cortex-core';
22
22
  import { createCliCommandContext } from '../../context.ts';
23
23
  import { throwCliError } from '../../errors.ts';
24
24
  import { serializeOutput, type OutputFormat } from '../../output.ts';
25
+ import { resolveDefaultStore } from '../../utils/resolve-default-store.ts';
25
26
 
26
27
  /** Options parsed by Commander for the create command */
27
28
  export interface CreateCommandOptions {
@@ -39,7 +40,7 @@ export interface CreateCommandOptions {
39
40
  function writeCreateOutput(
40
41
  payload: { path: string; created: boolean },
41
42
  options: CreateCommandOptions,
42
- stdout: NodeJS.WritableStream,
43
+ stdout: NodeJS.WritableStream
43
44
  ): void {
44
45
  const rawFormat = options.format;
45
46
 
@@ -49,7 +50,10 @@ function writeCreateOutput(
49
50
  return;
50
51
  }
51
52
 
52
- const serialized = serializeOutput({ kind: 'created-category', value: payload }, rawFormat as OutputFormat);
53
+ const serialized = serializeOutput(
54
+ { kind: 'created-category', value: payload },
55
+ rawFormat as OutputFormat
56
+ );
53
57
  if (!serialized.ok()) {
54
58
  throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
55
59
  }
@@ -85,7 +89,7 @@ export async function handleCreate(
85
89
  path: string,
86
90
  options: CreateCommandOptions = {}
87
91
  ): Promise<void> {
88
- const store = unwrapOrThrow(ctx.cortex.getStore(storeName ?? 'global'));
92
+ const store = unwrapOrThrow(ctx.cortex.getStore(resolveDefaultStore(ctx, storeName)));
89
93
  const root = unwrapOrThrow(store.root());
90
94
  const category = unwrapOrThrow(root.getCategory(path));
91
95
  const result = unwrapOrThrow(await category.create());
package/src/context.ts CHANGED
@@ -24,7 +24,7 @@ const makeAbsolute = (pathStr: string): string => {
24
24
 
25
25
  export const validateStorePath = (
26
26
  storePath: string,
27
- storeName: string,
27
+ storeName: string
28
28
  ): Result<void, ConfigValidationError> => {
29
29
  if (!isAbsolute(storePath)) {
30
30
  return err({
@@ -65,14 +65,19 @@ export const createCliAdapterFactory = (configAdapter: FilesystemConfigAdapter)
65
65
  const stores = configAdapter.stores!;
66
66
  const storeEntry = stores[storeName];
67
67
  if (!storeEntry) {
68
+ const available = Object.keys(stores).join(', ') || 'none';
68
69
  throw new Error(
69
- `Store '${storeName}' not found. Available stores: ${Object.keys(stores).join(', ')}`,
70
+ `Store '${storeName}' is not registered. Available stores: ${available}. ` +
71
+ 'Use --store to specify a registered store, or run `cortex store init` to create one.'
70
72
  );
71
73
  }
72
74
 
73
75
  const storePath = storeEntry.properties?.path as string | undefined;
74
76
  if (!storePath) {
75
- throw new Error(`Store '${storeName}' has no path configured in properties.`);
77
+ throw new Error(
78
+ `Store '${storeName}' has no path configured. ` +
79
+ 'Check your cortex config or re-run `cortex store init`.'
80
+ );
76
81
  }
77
82
 
78
83
  return new FilesystemStorageAdapter(configAdapter, {
@@ -82,10 +87,11 @@ export const createCliAdapterFactory = (configAdapter: FilesystemConfigAdapter)
82
87
  };
83
88
 
84
89
  export const createCliConfigContext = async (
85
- options: CliContextOptions = {},
90
+ options: CliContextOptions = {}
86
91
  ): Promise<Result<CliConfigContext, any>> => {
87
92
  const envConfigPath = process.env.CORTEX_CONFIG;
88
93
  const envConfigDir = process.env.CORTEX_CONFIG_DIR;
94
+ const envConfigCwd = process.env.CORTEX_CONFIG_CWD;
89
95
 
90
96
  const explicitConfigPath =
91
97
  typeof envConfigPath === 'string' && envConfigPath.length > 0
@@ -93,10 +99,10 @@ export const createCliConfigContext = async (
93
99
  : undefined;
94
100
 
95
101
  const dir = options.configDir ?? envConfigDir ?? resolve(homedir(), '.config', 'cortex');
102
+
96
103
  const absoluteDir = makeAbsolute(dir);
97
104
  const configPath = explicitConfigPath ?? resolve(absoluteDir, 'config.yaml');
98
105
 
99
- const envConfigCwd = process.env.CORTEX_CONFIG_CWD;
100
106
  const effectiveCwd =
101
107
  options.configCwd ??
102
108
  (typeof envConfigCwd === 'string' && envConfigCwd.length > 0
@@ -109,20 +115,10 @@ export const createCliConfigContext = async (
109
115
  return initResult;
110
116
  }
111
117
 
112
- const settingsResult = await configAdapter.getSettings();
113
- if (!settingsResult.ok()) {
114
- return settingsResult;
115
- }
116
-
117
- const storesResult = await configAdapter.getStores();
118
- if (!storesResult.ok()) {
119
- return storesResult;
120
- }
121
-
122
118
  return ok({
123
119
  configAdapter,
124
- settings: settingsResult.value,
125
- stores: storesResult.value,
120
+ settings: configAdapter.settings!,
121
+ stores: configAdapter.stores!,
126
122
  effectiveCwd,
127
123
  });
128
124
  };
@@ -131,7 +127,7 @@ export const createCliConfigContext = async (
131
127
  * This function is used to create a context object that can be injected into command handlers for consistent access to the Cortex client and other utilities.
132
128
  */
133
129
  export const createCliCommandContext = async (
134
- configDir?: string,
130
+ configDir?: string
135
131
  ): Promise<Result<CortexContext, any>> => {
136
132
  try {
137
133
  const configContextResult = await createCliConfigContext({
@@ -165,8 +161,7 @@ export const createCliCommandContext = async (
165
161
  };
166
162
 
167
163
  return ok(context);
168
- }
169
- catch (error) {
164
+ } catch (error) {
170
165
  return err({
171
166
  code: 'CONTEXT_CREATION_FAILED',
172
167
  message: `Unexpected error creating CLI command context: ${error instanceof Error ? error.message : String(error)}`,
@@ -28,6 +28,7 @@ import { resolveInput as resolveCliContent } from '../../utils/input.ts';
28
28
  import { parseExpiresAt, parseTags } from '../parsing.ts';
29
29
  import { createCliCommandContext } from '../../context.ts';
30
30
  import { serializeOutput, type OutputFormat } from '../../output.ts';
31
+ import { resolveDefaultStore } from '../../utils/resolve-default-store.ts';
31
32
 
32
33
  /** Options parsed by Commander for the add command */
33
34
  export interface AddCommandOptions {
@@ -77,7 +78,7 @@ export async function handleAdd(
77
78
  const expiresAt = parseExpiresAt(options.expiresAt);
78
79
  const citations = options.citations ?? [];
79
80
 
80
- const storeResult = ctx.cortex.getStore(storeName ?? 'global');
81
+ const storeResult = ctx.cortex.getStore(resolveDefaultStore(ctx, storeName));
81
82
  if (!storeResult.ok()) {
82
83
  throwCliError(storeResult.error);
83
84
  }
@@ -36,6 +36,7 @@ import { serialize, type CortexContext } from '@yeseh/cortex-core';
36
36
  import type { SubcategoryEntry } from '@yeseh/cortex-core/category';
37
37
  import { type OutputFormat } from '../../output.ts';
38
38
  import { createCliCommandContext } from '../../context.ts';
39
+ import { resolveDefaultStore } from '../../utils/resolve-default-store.ts';
39
40
 
40
41
  /**
41
42
  * Options for the list command.
@@ -116,7 +117,7 @@ export async function handleList(
116
117
  throwCliError(categoryResult.error);
117
118
  }
118
119
 
119
- const storeResult = ctx.cortex.getStore(storeName ?? 'global');
120
+ const storeResult = ctx.cortex.getStore(resolveDefaultStore(ctx, storeName));
120
121
  if (!storeResult.ok()) {
121
122
  throwCliError(storeResult.error);
122
123
  }
@@ -17,6 +17,7 @@ import { throwCliError } from '../../errors.ts';
17
17
  import { MemoryPath, type CortexContext } from '@yeseh/cortex-core';
18
18
  import { createCliCommandContext } from '../../context.ts';
19
19
  import { serializeOutput, type OutputFormat } from '../../output.ts';
20
+ import { resolveDefaultStore } from '../../utils/resolve-default-store.ts';
20
21
 
21
22
  /** Options for the move command. */
22
23
  export interface MoveCommandOptions {
@@ -50,7 +51,7 @@ export async function handleMove(
50
51
  throwCliError(toResult.error);
51
52
  }
52
53
 
53
- const storeResult = ctx.cortex.getStore(storeName ?? 'global');
54
+ const storeResult = ctx.cortex.getStore(resolveDefaultStore(ctx, storeName));
54
55
  if (!storeResult.ok()) {
55
56
  throwCliError(storeResult.error);
56
57
  }
@@ -81,7 +82,10 @@ export async function handleMove(
81
82
  } else {
82
83
  const format = rawFormat as OutputFormat;
83
84
  const serialized = serializeOutput(
84
- { kind: 'moved-memory', value: { from: fromResult.value.toString(), to: toResult.value.toString() } },
85
+ {
86
+ kind: 'moved-memory',
87
+ value: { from: fromResult.value.toString(), to: toResult.value.toString() },
88
+ },
85
89
  format
86
90
  );
87
91
  if (!serialized.ok()) {
@@ -18,6 +18,7 @@ import { throwCliError } from '../../errors.ts';
18
18
  import { MemoryPath, type CortexContext } from '@yeseh/cortex-core';
19
19
  import { createCliCommandContext } from '../../context.ts';
20
20
  import { serializeOutput, type OutputFormat } from '../../output.ts';
21
+ import { resolveDefaultStore } from '../../utils/resolve-default-store.ts';
21
22
 
22
23
  /** Options for the remove command. */
23
24
  export interface RemoveCommandOptions {
@@ -44,7 +45,7 @@ export async function handleRemove(
44
45
  throwCliError(pathResult.error);
45
46
  }
46
47
 
47
- const storeResult = ctx.cortex.getStore(storeName ?? 'global');
48
+ const storeResult = ctx.cortex.getStore(resolveDefaultStore(ctx, storeName));
48
49
  if (!storeResult.ok()) {
49
50
  throwCliError(storeResult.error);
50
51
  }
@@ -74,7 +75,10 @@ export async function handleRemove(
74
75
  out.write(`Removed memory ${pathResult.value.toString()}.\n`);
75
76
  } else {
76
77
  const format = rawFormat as OutputFormat;
77
- const serialized = serializeOutput({kind: 'path', value: { path: pathResult.value.toString() }}, format);
78
+ const serialized = serializeOutput(
79
+ { kind: 'path', value: { path: pathResult.value.toString() } },
80
+ format
81
+ );
78
82
  if (!serialized.ok()) {
79
83
  throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
80
84
  }
@@ -28,6 +28,7 @@ import { defaultTokenizer, MemoryPath, type CortexContext } from '@yeseh/cortex-
28
28
  import { type StoreClient } from '@yeseh/cortex-core/store';
29
29
  import { serializeOutput, type OutputMemory, type OutputFormat } from '../../output.ts';
30
30
  import { createCliCommandContext } from '../../context.ts';
31
+ import { resolveDefaultStore } from '../../utils/resolve-default-store.ts';
31
32
 
32
33
  /**
33
34
  * Options for the show command.
@@ -80,7 +81,7 @@ export async function handleShow(
80
81
  throwCliError(pathResult.error);
81
82
  }
82
83
 
83
- const storeResult = ctx.cortex.getStore(storeName ?? 'global');
84
+ const storeResult = ctx.cortex.getStore(resolveDefaultStore(ctx, storeName));
84
85
  if (!storeResult.ok()) {
85
86
  throwCliError(storeResult.error);
86
87
  }
@@ -126,7 +127,7 @@ export async function handleShow(
126
127
  };
127
128
 
128
129
  const format = (options.format as OutputFormat) ?? 'yaml';
129
- const serialized = serializeOutput({kind: 'memory', value: outputMemory}, format);
130
+ const serialized = serializeOutput({ kind: 'memory', value: outputMemory }, format);
130
131
  if (!serialized.ok()) {
131
132
  throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
132
133
  }
@@ -33,6 +33,7 @@ import { resolveInput as resolveCliContent } from '../../utils/input.ts';
33
33
  import { parseExpiresAt, parseTags } from '../parsing.ts';
34
34
  import { createCliCommandContext } from '../../context.ts';
35
35
  import { serializeOutput, type OutputFormat } from '../../output.ts';
36
+ import { resolveDefaultStore } from '../../utils/resolve-default-store.ts';
36
37
 
37
38
  /** Options parsed by Commander for the update command */
38
39
  export interface UpdateCommandOptions {
@@ -148,7 +149,7 @@ export async function handleUpdate(
148
149
  const expiresAt = parseUpdateExpiresAt(options.expiresAt);
149
150
  const updates = buildUpdates(content, tags, expiresAt, options.citation);
150
151
 
151
- const storeResult = ctx.cortex.getStore(storeName ?? 'global');
152
+ const storeResult = ctx.cortex.getStore(resolveDefaultStore(ctx, storeName));
152
153
  if (!storeResult.ok()) {
153
154
  throwCliError(storeResult.error);
154
155
  }
package/src/run.ts CHANGED
File without changes
@@ -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,