@yeseh/cortex-cli 0.6.0

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 (189) hide show
  1. package/README.md +144 -0
  2. package/dist/category/commands/create.d.ts +44 -0
  3. package/dist/category/commands/create.d.ts.map +1 -0
  4. package/dist/category/commands/create.spec.d.ts +7 -0
  5. package/dist/category/commands/create.spec.d.ts.map +1 -0
  6. package/dist/category/index.d.ts +19 -0
  7. package/dist/category/index.d.ts.map +1 -0
  8. package/dist/commands/init.d.ts +58 -0
  9. package/dist/commands/init.d.ts.map +1 -0
  10. package/dist/commands/init.spec.d.ts +2 -0
  11. package/dist/commands/init.spec.d.ts.map +1 -0
  12. package/dist/context.d.ts +18 -0
  13. package/dist/context.d.ts.map +1 -0
  14. package/dist/context.spec.d.ts +2 -0
  15. package/dist/context.spec.d.ts.map +1 -0
  16. package/dist/create-cli-command.d.ts +23 -0
  17. package/dist/create-cli-command.d.ts.map +1 -0
  18. package/dist/create-cli-command.spec.d.ts +10 -0
  19. package/dist/create-cli-command.spec.d.ts.map +1 -0
  20. package/dist/errors.d.ts +57 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.spec.d.ts +2 -0
  23. package/dist/errors.spec.d.ts.map +1 -0
  24. package/dist/input.d.ts +42 -0
  25. package/dist/input.d.ts.map +1 -0
  26. package/dist/input.spec.d.ts +2 -0
  27. package/dist/input.spec.d.ts.map +1 -0
  28. package/dist/memory/commands/add.d.ts +62 -0
  29. package/dist/memory/commands/add.d.ts.map +1 -0
  30. package/dist/memory/commands/add.spec.d.ts +7 -0
  31. package/dist/memory/commands/add.spec.d.ts.map +1 -0
  32. package/dist/memory/commands/definitions.spec.d.ts +10 -0
  33. package/dist/memory/commands/definitions.spec.d.ts.map +1 -0
  34. package/dist/memory/commands/handlers.spec.d.ts +2 -0
  35. package/dist/memory/commands/handlers.spec.d.ts.map +1 -0
  36. package/dist/memory/commands/list.d.ts +119 -0
  37. package/dist/memory/commands/list.d.ts.map +1 -0
  38. package/dist/memory/commands/list.spec.d.ts +2 -0
  39. package/dist/memory/commands/list.spec.d.ts.map +1 -0
  40. package/dist/memory/commands/move.d.ts +42 -0
  41. package/dist/memory/commands/move.d.ts.map +1 -0
  42. package/dist/memory/commands/move.spec.d.ts +2 -0
  43. package/dist/memory/commands/move.spec.d.ts.map +1 -0
  44. package/dist/memory/commands/remove.d.ts +41 -0
  45. package/dist/memory/commands/remove.d.ts.map +1 -0
  46. package/dist/memory/commands/remove.spec.d.ts +2 -0
  47. package/dist/memory/commands/remove.spec.d.ts.map +1 -0
  48. package/dist/memory/commands/show.d.ts +81 -0
  49. package/dist/memory/commands/show.d.ts.map +1 -0
  50. package/dist/memory/commands/show.spec.d.ts +2 -0
  51. package/dist/memory/commands/show.spec.d.ts.map +1 -0
  52. package/dist/memory/commands/test-helpers.spec.d.ts +19 -0
  53. package/dist/memory/commands/test-helpers.spec.d.ts.map +1 -0
  54. package/dist/memory/commands/update.d.ts +73 -0
  55. package/dist/memory/commands/update.d.ts.map +1 -0
  56. package/dist/memory/commands/update.spec.d.ts +2 -0
  57. package/dist/memory/commands/update.spec.d.ts.map +1 -0
  58. package/dist/memory/index.d.ts +29 -0
  59. package/dist/memory/index.d.ts.map +1 -0
  60. package/dist/memory/index.spec.d.ts +10 -0
  61. package/dist/memory/index.spec.d.ts.map +1 -0
  62. package/dist/memory/parsing.d.ts +3 -0
  63. package/dist/memory/parsing.d.ts.map +1 -0
  64. package/dist/memory/parsing.spec.d.ts +7 -0
  65. package/dist/memory/parsing.spec.d.ts.map +1 -0
  66. package/dist/output.d.ts +87 -0
  67. package/dist/output.d.ts.map +1 -0
  68. package/dist/output.spec.d.ts +2 -0
  69. package/dist/output.spec.d.ts.map +1 -0
  70. package/dist/paths.d.ts +27 -0
  71. package/dist/paths.d.ts.map +1 -0
  72. package/dist/paths.spec.d.ts +7 -0
  73. package/dist/paths.spec.d.ts.map +1 -0
  74. package/dist/program.d.ts +41 -0
  75. package/dist/program.d.ts.map +1 -0
  76. package/dist/program.spec.d.ts +11 -0
  77. package/dist/program.spec.d.ts.map +1 -0
  78. package/dist/run.d.ts +7 -0
  79. package/dist/run.d.ts.map +1 -0
  80. package/dist/run.spec.d.ts +12 -0
  81. package/dist/run.spec.d.ts.map +1 -0
  82. package/dist/store/commands/add.d.ts +73 -0
  83. package/dist/store/commands/add.d.ts.map +1 -0
  84. package/dist/store/commands/add.spec.d.ts +17 -0
  85. package/dist/store/commands/add.spec.d.ts.map +1 -0
  86. package/dist/store/commands/init.d.ts +75 -0
  87. package/dist/store/commands/init.d.ts.map +1 -0
  88. package/dist/store/commands/init.spec.d.ts +7 -0
  89. package/dist/store/commands/init.spec.d.ts.map +1 -0
  90. package/dist/store/commands/list.d.ts +62 -0
  91. package/dist/store/commands/list.d.ts.map +1 -0
  92. package/dist/store/commands/list.spec.d.ts +7 -0
  93. package/dist/store/commands/list.spec.d.ts.map +1 -0
  94. package/dist/store/commands/prune.d.ts +92 -0
  95. package/dist/store/commands/prune.d.ts.map +1 -0
  96. package/dist/store/commands/prune.spec.d.ts +7 -0
  97. package/dist/store/commands/prune.spec.d.ts.map +1 -0
  98. package/dist/store/commands/reindexs.d.ts +54 -0
  99. package/dist/store/commands/reindexs.d.ts.map +1 -0
  100. package/dist/store/commands/reindexs.spec.d.ts +7 -0
  101. package/dist/store/commands/reindexs.spec.d.ts.map +1 -0
  102. package/dist/store/commands/remove.d.ts +63 -0
  103. package/dist/store/commands/remove.d.ts.map +1 -0
  104. package/dist/store/commands/remove.spec.d.ts +17 -0
  105. package/dist/store/commands/remove.spec.d.ts.map +1 -0
  106. package/dist/store/index.d.ts +32 -0
  107. package/dist/store/index.d.ts.map +1 -0
  108. package/dist/store/index.spec.d.ts +9 -0
  109. package/dist/store/index.spec.d.ts.map +1 -0
  110. package/dist/store/utils/resolve-store-name.d.ts +30 -0
  111. package/dist/store/utils/resolve-store-name.d.ts.map +1 -0
  112. package/dist/store/utils/resolve-store-name.spec.d.ts +2 -0
  113. package/dist/store/utils/resolve-store-name.spec.d.ts.map +1 -0
  114. package/dist/test-helpers.spec.d.ts +224 -0
  115. package/dist/test-helpers.spec.d.ts.map +1 -0
  116. package/dist/tests/cli.integration.spec.d.ts +11 -0
  117. package/dist/tests/cli.integration.spec.d.ts.map +1 -0
  118. package/dist/toon.d.ts +197 -0
  119. package/dist/toon.d.ts.map +1 -0
  120. package/dist/toon.spec.d.ts +9 -0
  121. package/dist/toon.spec.d.ts.map +1 -0
  122. package/dist/utils/git.d.ts +20 -0
  123. package/dist/utils/git.d.ts.map +1 -0
  124. package/dist/utils/git.spec.d.ts +7 -0
  125. package/dist/utils/git.spec.d.ts.map +1 -0
  126. package/package.json +45 -0
  127. package/src/category/commands/create.spec.ts +139 -0
  128. package/src/category/commands/create.ts +115 -0
  129. package/src/category/index.ts +24 -0
  130. package/src/commands/init.spec.ts +203 -0
  131. package/src/commands/init.ts +301 -0
  132. package/src/context.spec.ts +60 -0
  133. package/src/context.ts +175 -0
  134. package/src/errors.spec.ts +264 -0
  135. package/src/errors.ts +105 -0
  136. package/src/memory/commands/add.spec.ts +169 -0
  137. package/src/memory/commands/add.ts +157 -0
  138. package/src/memory/commands/definitions.spec.ts +80 -0
  139. package/src/memory/commands/list.spec.ts +123 -0
  140. package/src/memory/commands/list.ts +268 -0
  141. package/src/memory/commands/move.spec.ts +85 -0
  142. package/src/memory/commands/move.ts +115 -0
  143. package/src/memory/commands/remove.spec.ts +79 -0
  144. package/src/memory/commands/remove.ts +104 -0
  145. package/src/memory/commands/show.spec.ts +71 -0
  146. package/src/memory/commands/show.ts +164 -0
  147. package/src/memory/commands/test-helpers.spec.ts +127 -0
  148. package/src/memory/commands/update.spec.ts +86 -0
  149. package/src/memory/commands/update.ts +229 -0
  150. package/src/memory/index.spec.ts +59 -0
  151. package/src/memory/index.ts +44 -0
  152. package/src/memory/parsing.spec.ts +105 -0
  153. package/src/memory/parsing.ts +22 -0
  154. package/src/observability.spec.ts +139 -0
  155. package/src/observability.ts +63 -0
  156. package/src/output.spec.ts +835 -0
  157. package/src/output.ts +119 -0
  158. package/src/program.spec.ts +46 -0
  159. package/src/program.ts +75 -0
  160. package/src/run.spec.ts +31 -0
  161. package/src/run.ts +9 -0
  162. package/src/store/commands/add.spec.ts +131 -0
  163. package/src/store/commands/add.ts +231 -0
  164. package/src/store/commands/init.spec.ts +236 -0
  165. package/src/store/commands/init.ts +256 -0
  166. package/src/store/commands/list.spec.ts +175 -0
  167. package/src/store/commands/list.ts +102 -0
  168. package/src/store/commands/prune.spec.ts +120 -0
  169. package/src/store/commands/prune.ts +152 -0
  170. package/src/store/commands/reindexs.spec.ts +94 -0
  171. package/src/store/commands/reindexs.ts +96 -0
  172. package/src/store/commands/remove.spec.ts +97 -0
  173. package/src/store/commands/remove.ts +189 -0
  174. package/src/store/index.spec.ts +60 -0
  175. package/src/store/index.ts +49 -0
  176. package/src/store/utils/resolve-store-name.spec.ts +62 -0
  177. package/src/store/utils/resolve-store-name.ts +79 -0
  178. package/src/test-helpers.spec.ts +430 -0
  179. package/src/tests/cli.integration.spec.ts +1170 -0
  180. package/src/toon.spec.ts +183 -0
  181. package/src/toon.ts +462 -0
  182. package/src/utils/git.spec.ts +95 -0
  183. package/src/utils/git.ts +51 -0
  184. package/src/utils/input.spec.ts +326 -0
  185. package/src/utils/input.ts +145 -0
  186. package/src/utils/paths.spec.ts +235 -0
  187. package/src/utils/paths.ts +75 -0
  188. package/src/utils/prompts.spec.ts +23 -0
  189. package/src/utils/prompts.ts +88 -0
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Unit tests for the handleCreate command handler.
3
+ *
4
+ * @module cli/category/commands/create.spec
5
+ */
6
+
7
+ import { describe, it, expect, afterEach, beforeEach } from 'bun:test';
8
+ import { CommanderError, InvalidArgumentError } from '@commander-js/extra-typings';
9
+ import { mkdtemp, rm } from 'node:fs/promises';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { err, ok } from '@yeseh/cortex-core';
13
+ import { type AdapterFactory } from '@yeseh/cortex-core';
14
+
15
+ import { handleCreate } from './create.ts';
16
+ import {
17
+ createCaptureStream,
18
+ createMemoryCommandContext,
19
+ createMockMemoryCommandAdapter,
20
+ } from '../../memory/commands/test-helpers.spec.ts';
21
+
22
+ describe('handleCreate', () => {
23
+ let tempDir: string;
24
+
25
+ beforeEach(async () => {
26
+ tempDir = await mkdtemp(join(tmpdir(), 'cortex-category-create-'));
27
+ });
28
+
29
+ afterEach(async () => {
30
+ await rm(tempDir, { recursive: true, force: true });
31
+ });
32
+
33
+ it('should write "Created <path>" to stdout when category is new', async () => {
34
+ const capture = createCaptureStream();
35
+ const ctx = createMemoryCommandContext({
36
+ adapter: createMockMemoryCommandAdapter({
37
+ categories: {
38
+ exists: async () => ok(false),
39
+ ensure: async () => ok(undefined),
40
+ },
41
+ }),
42
+ storePath: tempDir,
43
+ stdout: capture.stream,
44
+ });
45
+
46
+ await handleCreate(ctx, undefined, 'standards/typescript');
47
+
48
+ const output = capture.getOutput();
49
+ expect(output).toContain('Created');
50
+ expect(output).toContain('standards/typescript');
51
+ });
52
+
53
+ it('should write already exists message when category exists', async () => {
54
+ const capture = createCaptureStream();
55
+ const ctx = createMemoryCommandContext({
56
+ adapter: createMockMemoryCommandAdapter({
57
+ categories: {
58
+ exists: async () => ok(true),
59
+ },
60
+ }),
61
+ storePath: tempDir,
62
+ stdout: capture.stream,
63
+ });
64
+
65
+ await handleCreate(ctx, undefined, 'standards/typescript');
66
+
67
+ const output = capture.getOutput();
68
+ expect(output).toContain('Category already exists');
69
+ expect(output).toContain('standards/typescript');
70
+ });
71
+
72
+ it('should serialize as JSON when --format json is passed', async () => {
73
+ const capture = createCaptureStream();
74
+ const ctx = createMemoryCommandContext({
75
+ adapter: createMockMemoryCommandAdapter({
76
+ categories: {
77
+ exists: async () => ok(false),
78
+ ensure: async () => ok(undefined),
79
+ },
80
+ }),
81
+ storePath: tempDir,
82
+ stdout: capture.stream,
83
+ });
84
+
85
+ await handleCreate(ctx, undefined, 'standards', { format: 'json' });
86
+
87
+ const parsed = JSON.parse(capture.getOutput());
88
+ expect(parsed.value.path).toBe('standards');
89
+ expect(parsed.value.created).toBeTrue();
90
+ });
91
+
92
+ it('should throw CommanderError when store not found', async () => {
93
+ const failingFactory = (() => undefined) as unknown as AdapterFactory;
94
+
95
+ const ctx = createMemoryCommandContext({
96
+ adapter: createMockMemoryCommandAdapter(),
97
+ storePath: tempDir,
98
+ adapterFactory: failingFactory,
99
+ });
100
+
101
+ await expect(handleCreate(ctx, 'nonexistent', 'standards')).rejects.toThrow(CommanderError);
102
+ });
103
+
104
+ it('should throw InvalidArgumentError for INVALID_PATH errors', async () => {
105
+ const ctx = createMemoryCommandContext({
106
+ adapter: createMockMemoryCommandAdapter({
107
+ categories: {
108
+ exists: async () => ok(false),
109
+ ensure: async () => err({
110
+ code: 'INVALID_PATH',
111
+ message: 'Invalid category path: /',
112
+ }),
113
+ },
114
+ }),
115
+ storePath: tempDir,
116
+ });
117
+
118
+ await expect(handleCreate(ctx, undefined, 'standards')).rejects.toThrow(InvalidArgumentError);
119
+ });
120
+
121
+ it('should throw CommanderError when storage ensure fails', async () => {
122
+ const ctx = createMemoryCommandContext({
123
+ adapter: createMockMemoryCommandAdapter({
124
+ categories: {
125
+ exists: async () => ok(false),
126
+ ensure: async () => err({
127
+ code: 'WRITE_FAILED',
128
+ message: 'Failed to write category index.',
129
+ }),
130
+ },
131
+ }),
132
+ storePath: tempDir,
133
+ });
134
+
135
+ await expect(handleCreate(ctx, undefined, 'standards/typescript')).rejects.toThrow(
136
+ CommanderError,
137
+ );
138
+ });
139
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Category create command.
3
+ *
4
+ * Creates a category at the specified path, including any missing ancestors.
5
+ *
6
+ * @example
7
+ * ```bash
8
+ * # Create a category in default store
9
+ * cortex category create standards/typescript
10
+ *
11
+ * # Create a category in a specific store
12
+ * cortex category --store my-store create standards/typescript
13
+ *
14
+ * # Serialize output as JSON
15
+ * cortex category create standards --format json
16
+ * ```
17
+ */
18
+
19
+ import { Command } from '@commander-js/extra-typings';
20
+ import { type CortexContext, type Result } from '@yeseh/cortex-core';
21
+
22
+ import { createCliCommandContext } from '../../context.ts';
23
+ import { throwCliError } from '../../errors.ts';
24
+ import { serializeOutput, type OutputFormat } from '../../output.ts';
25
+
26
+ /** Options parsed by Commander for the create command */
27
+ export interface CreateCommandOptions {
28
+ description?: string;
29
+ format?: string;
30
+ }
31
+
32
+ /**
33
+ * Writes command output to stdout.
34
+ *
35
+ * @param payload - Category creation result payload
36
+ * @param options - Command options
37
+ * @param stdout - Output stream
38
+ */
39
+ function writeCreateOutput(
40
+ payload: { path: string; created: boolean },
41
+ options: CreateCommandOptions,
42
+ stdout: NodeJS.WritableStream,
43
+ ): void {
44
+ const rawFormat = options.format;
45
+
46
+ if (!rawFormat) {
47
+ const verb = payload.created ? 'Created' : 'Category already exists:';
48
+ stdout.write(`${verb} ${payload.path}\n`);
49
+ return;
50
+ }
51
+
52
+ const serialized = serializeOutput({ kind: 'created-category', value: payload }, rawFormat as OutputFormat);
53
+ if (!serialized.ok()) {
54
+ throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
55
+ }
56
+ stdout.write(serialized.value + '\n');
57
+ }
58
+
59
+ /**
60
+ * Unwrap a Result and throw a mapped CLI error on failure.
61
+ *
62
+ * @param result - Result value
63
+ * @returns Unwrapped value
64
+ */
65
+ function unwrapOrThrow<T, E extends { code: string; message: string }>(result: Result<T, E>): T {
66
+ if (!result.ok()) {
67
+ throwCliError(result.error);
68
+ }
69
+
70
+ return result.value;
71
+ }
72
+
73
+ /**
74
+ * Handler for the category create command.
75
+ * Exported for direct testing without Commander parsing.
76
+ *
77
+ * @param ctx - CLI execution context
78
+ * @param storeName - Optional store name from parent command
79
+ * @param path - Category path (e.g., "standards/typescript")
80
+ * @param options - Command options from Commander
81
+ */
82
+ export async function handleCreate(
83
+ ctx: CortexContext,
84
+ storeName: string | undefined,
85
+ path: string,
86
+ options: CreateCommandOptions = {}
87
+ ): Promise<void> {
88
+ const store = unwrapOrThrow(ctx.cortex.getStore(storeName ?? 'global'));
89
+ const root = unwrapOrThrow(store.root());
90
+ const category = unwrapOrThrow(root.getCategory(path));
91
+ const result = unwrapOrThrow(await category.create());
92
+
93
+ const out = ctx.stdout ?? process.stdout;
94
+ writeCreateOutput(result, options, out);
95
+ }
96
+
97
+ /**
98
+ * The `category create` subcommand.
99
+ *
100
+ * Creates a category and any missing ancestors.
101
+ */
102
+ export const createCommand = new Command('create')
103
+ .description('Create a category (and any missing ancestors)')
104
+ .argument('<path>', 'Category path (e.g., standards/typescript)')
105
+ .option('-d, --description <text>', 'Optional description for the category')
106
+ .option('-o, --format <format>', 'Output format (yaml, json, toon)')
107
+ .action(async (path, options, command) => {
108
+ const parentOpts = command.parent?.opts() as { store?: string } | undefined;
109
+ const context = await createCliCommandContext();
110
+ if (!context.ok()) {
111
+ throwCliError(context.error);
112
+ }
113
+
114
+ await handleCreate(context.value, parentOpts?.store, path, options);
115
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Category command group for the CLI.
3
+ *
4
+ * This module defines the `category` command group, which provides operations
5
+ * for managing categories in the Cortex memory system. The `--store` option
6
+ * is defined at the group level and inherited by all subcommands.
7
+ */
8
+
9
+ import { Command } from '@commander-js/extra-typings';
10
+
11
+ import { createCommand } from './commands/create';
12
+
13
+ /**
14
+ * The `category` command group.
15
+ *
16
+ * Provides category management operations. The `--store` option allows
17
+ * targeting a specific named store instead of the default store.
18
+ * This option is inherited by all subcommands.
19
+ */
20
+ export const categoryCommand = new Command('category')
21
+ .description('Category operations')
22
+ .option('-s, --store <name>', 'Use a specific named store');
23
+
24
+ categoryCommand.addCommand(createCommand);
@@ -0,0 +1,203 @@
1
+ import { describe, expect, it, mock } from 'bun:test';
2
+ import { CommanderError } from '@commander-js/extra-typings';
3
+ import { PassThrough } from 'node:stream';
4
+ import { err, ok, type ConfigStore, type CortexContext } from '@yeseh/cortex-core';
5
+
6
+ import { handleInit } from './init.ts';
7
+ import type { PromptDeps } from '../utils/prompts.ts';
8
+
9
+ const existingGlobalStore: ConfigStore = {
10
+ kind: 'filesystem',
11
+ categoryMode: 'free',
12
+ categories: {},
13
+ properties: { path: '/tmp/existing-global-store' },
14
+ };
15
+
16
+ const createContext = (config: CortexContext['config']): { ctx: CortexContext; stdout: PassThrough } => {
17
+ const stdout = new PassThrough();
18
+ let output = '';
19
+
20
+ stdout.on('data', (chunk: Buffer | string) => {
21
+ output += chunk.toString();
22
+ });
23
+
24
+ const ctx = {
25
+ cortex: {} as CortexContext['cortex'],
26
+ config,
27
+ settings: {},
28
+ stores: {},
29
+ now: () => new Date('2025-01-01T00:00:00.000Z'),
30
+ stdin: new PassThrough() as unknown as NodeJS.ReadStream,
31
+ stdout: stdout as unknown as NodeJS.WriteStream,
32
+ } as CortexContext;
33
+
34
+ return {
35
+ ctx,
36
+ stdout: Object.assign(stdout, {
37
+ getOutput: () => output,
38
+ }) as PassThrough,
39
+ };
40
+ };
41
+
42
+ describe('handleInit', () => {
43
+ it('should initialize config and create global store when missing', async () => {
44
+ const initializeConfig = mock(async () => ok(undefined));
45
+ const getStore = mock(async () => ok(null));
46
+ const saveStore = mock(async () => ok(undefined));
47
+
48
+ const { ctx, stdout } = createContext({
49
+ path: '/tmp/test-config.yaml',
50
+ data: null,
51
+ stores: null,
52
+ settings: null,
53
+ initializeConfig,
54
+ getSettings: async () => ok({}),
55
+ getStores: async () => ok({}),
56
+ getStore,
57
+ saveStore,
58
+ });
59
+
60
+ await handleInit(ctx, { format: 'json' });
61
+
62
+ expect(initializeConfig).toHaveBeenCalledTimes(1);
63
+ expect(saveStore).toHaveBeenCalledTimes(1);
64
+
65
+ const output = (stdout as PassThrough & { getOutput: () => string }).getOutput();
66
+ const parsed = JSON.parse(output) as { value: { path: string } };
67
+ expect(parsed.value.path).toContain('/.config/cortex/memory');
68
+ });
69
+
70
+ it('should throw when global store already exists without force', async () => {
71
+ const saveStore = mock(async () => ok(undefined));
72
+
73
+ const { ctx } = createContext({
74
+ path: '/tmp/test-config.yaml',
75
+ data: null,
76
+ stores: null,
77
+ settings: null,
78
+ initializeConfig: async () => ok(undefined),
79
+ getSettings: async () => ok({}),
80
+ getStores: async () => ok({}),
81
+ getStore: async () => ok(existingGlobalStore),
82
+ saveStore,
83
+ });
84
+
85
+ await expect(handleInit(ctx, { format: 'yaml' })).rejects.toThrow(CommanderError);
86
+ expect(saveStore).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it('should allow force and skip saveStore when global store already exists', async () => {
90
+ const saveStore = mock(async () => ok(undefined));
91
+
92
+ const { ctx } = createContext({
93
+ path: '/tmp/test-config.yaml',
94
+ data: null,
95
+ stores: null,
96
+ settings: null,
97
+ initializeConfig: async () => ok(undefined),
98
+ getSettings: async () => ok({}),
99
+ getStores: async () => ok({}),
100
+ getStore: async () => ok(existingGlobalStore),
101
+ saveStore,
102
+ });
103
+
104
+ await expect(handleInit(ctx, { force: true, format: 'yaml' })).resolves.toBeUndefined();
105
+ expect(saveStore).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it('should surface config initialization failures', async () => {
109
+ const { ctx } = createContext({
110
+ path: '/tmp/test-config.yaml',
111
+ data: null,
112
+ stores: null,
113
+ settings: null,
114
+ initializeConfig: async () =>
115
+ err({
116
+ code: 'CONFIG_WRITE_FAILED',
117
+ message: 'Failed to write config file',
118
+ }),
119
+ getSettings: async () => ok({}),
120
+ getStores: async () => ok({}),
121
+ getStore: async () => ok(null),
122
+ saveStore: async () => ok(undefined),
123
+ });
124
+
125
+ await expect(handleInit(ctx, { format: 'yaml' })).rejects.toThrow(CommanderError);
126
+ });
127
+ });
128
+
129
+ describe('handleInit - interactive mode', () => {
130
+ it('should call promptDeps.input twice when stdin is a TTY and no flags are given', async () => {
131
+ const inputMock = mock(async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value');
132
+ const confirmMock = mock(async () => true);
133
+ const promptDeps: PromptDeps = { input: inputMock, confirm: confirmMock };
134
+
135
+ const { ctx } = createContext({
136
+ path: '/tmp/test-config.yaml',
137
+ data: null,
138
+ stores: null,
139
+ settings: null,
140
+ initializeConfig: async () => ok(undefined),
141
+ getSettings: async () => ok({}),
142
+ getStores: async () => ok({}),
143
+ getStore: async () => ok(null),
144
+ saveStore: async () => ok(undefined),
145
+ });
146
+ (ctx.stdin as unknown as { isTTY: boolean }).isTTY = true;
147
+
148
+ await handleInit(ctx, { format: 'json' }, promptDeps);
149
+
150
+ expect(inputMock).toHaveBeenCalledTimes(2);
151
+ });
152
+
153
+ it('should NOT call promptDeps.input when stdin is NOT a TTY', async () => {
154
+ const inputMock = mock(async ({ default: d }: { message: string; default?: string }) => d ?? 'prompted-value');
155
+ const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
156
+
157
+ const { ctx } = createContext({
158
+ path: '/tmp/test-config.yaml',
159
+ data: null,
160
+ stores: null,
161
+ settings: null,
162
+ initializeConfig: async () => ok(undefined),
163
+ getSettings: async () => ok({}),
164
+ getStores: async () => ok({}),
165
+ getStore: async () => ok(null),
166
+ saveStore: async () => ok(undefined),
167
+ });
168
+ // ctx.stdin is a PassThrough with isTTY = undefined (non-TTY)
169
+
170
+ await handleInit(ctx, { format: 'json' }, promptDeps);
171
+
172
+ expect(inputMock).not.toHaveBeenCalled();
173
+ });
174
+
175
+ it('should use prompted store name in saveStore call when TTY', async () => {
176
+ const promptedName = 'my-custom-name';
177
+ const inputMock = mock(async ({ message, default: d }: { message: string; default?: string }) => {
178
+ // Second call is for store name
179
+ if (message.includes('name')) return promptedName;
180
+ return d ?? 'default-path';
181
+ });
182
+ const promptDeps: PromptDeps = { input: inputMock, confirm: mock(async () => true) };
183
+
184
+ const saveStore = mock(async () => ok(undefined));
185
+ const { ctx } = createContext({
186
+ path: '/tmp/test-config.yaml',
187
+ data: null,
188
+ stores: null,
189
+ settings: null,
190
+ initializeConfig: async () => ok(undefined),
191
+ getSettings: async () => ok({}),
192
+ getStores: async () => ok({}),
193
+ getStore: async () => ok(null),
194
+ saveStore,
195
+ });
196
+ (ctx.stdin as unknown as { isTTY: boolean }).isTTY = true;
197
+
198
+ await handleInit(ctx, { format: 'json' }, promptDeps);
199
+
200
+ // saveStore should be called with the prompted name
201
+ expect(saveStore).toHaveBeenCalledWith(promptedName, expect.any(Object));
202
+ });
203
+ });