@yeseh/cortex-cli 0.6.8 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/program.js +1538 -5
  2. package/dist/program.js.map +32 -3
  3. package/dist/run.d.ts +0 -1
  4. package/dist/run.d.ts.map +1 -1
  5. package/dist/run.js +3 -4
  6. package/dist/run.js.map +3 -3
  7. package/package.json +4 -6
  8. package/dist/chunk-dsfj4baj.js +0 -1543
  9. package/dist/chunk-dsfj4baj.js.map +0 -38
  10. package/src/category/commands/create.spec.ts +0 -139
  11. package/src/category/commands/create.ts +0 -119
  12. package/src/category/index.ts +0 -24
  13. package/src/commands/init.spec.ts +0 -203
  14. package/src/commands/init.ts +0 -301
  15. package/src/context.spec.ts +0 -60
  16. package/src/context.ts +0 -170
  17. package/src/errors.spec.ts +0 -264
  18. package/src/errors.ts +0 -105
  19. package/src/memory/commands/add.spec.ts +0 -169
  20. package/src/memory/commands/add.ts +0 -158
  21. package/src/memory/commands/definitions.spec.ts +0 -80
  22. package/src/memory/commands/list.spec.ts +0 -123
  23. package/src/memory/commands/list.ts +0 -269
  24. package/src/memory/commands/move.spec.ts +0 -85
  25. package/src/memory/commands/move.ts +0 -119
  26. package/src/memory/commands/remove.spec.ts +0 -79
  27. package/src/memory/commands/remove.ts +0 -108
  28. package/src/memory/commands/show.spec.ts +0 -71
  29. package/src/memory/commands/show.ts +0 -165
  30. package/src/memory/commands/test-helpers.spec.ts +0 -127
  31. package/src/memory/commands/update.spec.ts +0 -86
  32. package/src/memory/commands/update.ts +0 -230
  33. package/src/memory/index.spec.ts +0 -59
  34. package/src/memory/index.ts +0 -44
  35. package/src/memory/parsing.spec.ts +0 -105
  36. package/src/memory/parsing.ts +0 -22
  37. package/src/observability.spec.ts +0 -126
  38. package/src/observability.ts +0 -82
  39. package/src/output.spec.ts +0 -835
  40. package/src/output.ts +0 -119
  41. package/src/program.spec.ts +0 -46
  42. package/src/program.ts +0 -75
  43. package/src/run.spec.ts +0 -31
  44. package/src/run.ts +0 -9
  45. package/src/store/commands/add.spec.ts +0 -131
  46. package/src/store/commands/add.ts +0 -231
  47. package/src/store/commands/init.spec.ts +0 -220
  48. package/src/store/commands/init.ts +0 -272
  49. package/src/store/commands/list.spec.ts +0 -175
  50. package/src/store/commands/list.ts +0 -102
  51. package/src/store/commands/prune.spec.ts +0 -120
  52. package/src/store/commands/prune.ts +0 -152
  53. package/src/store/commands/reindexs.spec.ts +0 -94
  54. package/src/store/commands/reindexs.ts +0 -97
  55. package/src/store/commands/remove.spec.ts +0 -97
  56. package/src/store/commands/remove.ts +0 -189
  57. package/src/store/index.spec.ts +0 -60
  58. package/src/store/index.ts +0 -49
  59. package/src/store/utils/resolve-store-name.spec.ts +0 -62
  60. package/src/store/utils/resolve-store-name.ts +0 -79
  61. package/src/test-helpers.spec.ts +0 -430
  62. package/src/tests/cli.integration.spec.ts +0 -1306
  63. package/src/toon.spec.ts +0 -183
  64. package/src/toon.ts +0 -462
  65. package/src/utils/git.spec.ts +0 -95
  66. package/src/utils/git.ts +0 -51
  67. package/src/utils/input.spec.ts +0 -326
  68. package/src/utils/input.ts +0 -150
  69. package/src/utils/paths.spec.ts +0 -235
  70. package/src/utils/paths.ts +0 -75
  71. package/src/utils/prompts.spec.ts +0 -23
  72. package/src/utils/prompts.ts +0 -88
  73. package/src/utils/resolve-default-store.spec.ts +0 -135
  74. package/src/utils/resolve-default-store.ts +0 -74
@@ -1,102 +0,0 @@
1
- /**
2
- * Store list command for displaying all registered stores.
3
- *
4
- * This command reads the store registry and displays all registered
5
- * stores sorted alphabetically by name.
6
- *
7
- * @example
8
- * ```bash
9
- * # List all stores in YAML format (default)
10
- * cortex store list
11
- *
12
- * # List all stores in JSON format
13
- * cortex store list --format json
14
- * ```
15
- */
16
-
17
- import { Command } from '@commander-js/extra-typings';
18
- import { type CortexContext } from '@yeseh/cortex-core';
19
- import { throwCliError } from '../../errors.ts';
20
- import { createCliCommandContext } from '../../context.ts';
21
- import { serializeOutput, type OutputStoreRegistry, type OutputFormat } from '../../output.ts';
22
-
23
- /**
24
- * Options for the list command.
25
- */
26
- export interface ListCommandOptions {
27
- /** Output format (yaml, json, toon) */
28
- format?: string;
29
- }
30
-
31
- /**
32
- * Dependencies for the list command handler.
33
- * Allows injection for testing.
34
- */
35
- export interface ListHandlerDeps {
36
- /** Output stream for writing results (defaults to process.stdout) */
37
- stdout?: NodeJS.WritableStream;
38
- }
39
-
40
- /**
41
- * Handles the list command execution.
42
- *
43
- * This function:
44
- * 1. Gets stores from the context
45
- * 2. Formats the stores as a sorted list
46
- * 3. Serializes and outputs the result
47
- *
48
- * @param ctx - The Cortex context containing stores configuration
49
- * @param options - Command options (format)
50
- * @param deps - Optional dependencies for testing
51
- * @throws {CommanderError} When serialization fails
52
- */
53
- export async function handleList(
54
- ctx: CortexContext,
55
- options: ListCommandOptions,
56
- deps: ListHandlerDeps = {},
57
- ): Promise<void> {
58
- // 1. Get stores from context
59
- const stores = Object.entries(ctx.stores)
60
- .map(([
61
- name, def,
62
- ]) => ({
63
- name,
64
- path: (def.properties as { path: string }).path,
65
- }))
66
- .sort((a, b) => a.name.localeCompare(b.name));
67
-
68
- const output: OutputStoreRegistry = { stores };
69
-
70
- // 2. Serialize and output
71
- const format: OutputFormat = (options.format as OutputFormat) ?? 'yaml';
72
- const serialized = serializeOutput({ kind: 'store-registry', value: output }, format);
73
- if (!serialized.ok()) {
74
- throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
75
- }
76
-
77
- const out = deps.stdout ?? process.stdout;
78
- out.write(serialized.value + '\n');
79
- }
80
-
81
- /**
82
- * The `list` subcommand for displaying all registered stores.
83
- *
84
- * Reads the store registry and displays all stores sorted alphabetically
85
- * by name in the specified format.
86
- *
87
- * @example
88
- * ```bash
89
- * cortex store list
90
- * cortex store list --format json
91
- * ```
92
- */
93
- export const listCommand = new Command('list')
94
- .description('List all registered stores')
95
- .option('-o, --format <format>', 'Output format (yaml, json, toon)', 'yaml')
96
- .action(async (options) => {
97
- const context = await createCliCommandContext();
98
- if (!context.ok()) {
99
- throwCliError(context.error);
100
- }
101
- await handleList(context.value, options);
102
- });
@@ -1,120 +0,0 @@
1
- /**
2
- * Unit tests for the store prune command handler.
3
- *
4
- * @module cli/store/commands/prune.spec
5
- */
6
-
7
- import { describe, it, expect } from 'bun:test';
8
- import { handlePrune } from './prune.ts';
9
- import {
10
- createMockContext,
11
- captureOutput,
12
- expectCommanderError,
13
- errResult,
14
- okResult,
15
- } from '../../test-helpers.spec.ts';
16
-
17
- describe('handlePrune', () => {
18
- it('should use default store when no store name is provided', async () => {
19
- const { ctx, stdout } = createMockContext();
20
- const calls: string[] = [];
21
-
22
- const root = {
23
- prune: async () => okResult({ pruned: [] }),
24
- };
25
-
26
- (ctx.cortex as unknown as { getStore: (name: string) => unknown }).getStore = (name: string) => {
27
- calls.push(name);
28
- return okResult({ root: () => okResult(root) });
29
- };
30
-
31
- await handlePrune(ctx, undefined, {}, { stdout });
32
-
33
- expect(calls).toEqual(['global']);
34
- expect(captureOutput(stdout)).toContain('No expired memories found.');
35
- });
36
-
37
- it('should output dry-run preview when --dry-run is enabled', async () => {
38
- const { ctx, stdout } = createMockContext();
39
-
40
- const root = {
41
- prune: async () =>
42
- okResult({
43
- pruned: [{ path: 'project/old-memory' }, { path: 'notes/expired' }],
44
- }),
45
- };
46
-
47
- (ctx.cortex as unknown as { getStore: () => unknown }).getStore = () =>
48
- okResult({ root: () => okResult(root) });
49
-
50
- await handlePrune(ctx, 'work', { dryRun: true }, { stdout });
51
-
52
- const output = captureOutput(stdout);
53
- expect(output).toContain('Would prune 2 expired memories:');
54
- expect(output).toContain('project/old-memory');
55
- expect(output).toContain('notes/expired');
56
- });
57
-
58
- it('should output prune summary when dry-run is disabled', async () => {
59
- const { ctx, stdout } = createMockContext();
60
-
61
- const root = {
62
- prune: async () =>
63
- okResult({
64
- pruned: [{ path: 'inbox/obsolete' }],
65
- }),
66
- };
67
-
68
- (ctx.cortex as unknown as { getStore: () => unknown }).getStore = () =>
69
- okResult({ root: () => okResult(root) });
70
-
71
- await handlePrune(ctx, 'work', { dryRun: false }, { stdout });
72
-
73
- const output = captureOutput(stdout);
74
- expect(output).toContain('Pruned 1 expired memories:');
75
- expect(output).toContain('inbox/obsolete');
76
- });
77
-
78
- it('should throw CommanderError when store resolution fails', async () => {
79
- const { ctx } = createMockContext();
80
-
81
- (ctx.cortex as unknown as { getStore: () => unknown }).getStore = () =>
82
- errResult({ code: 'STORE_NOT_FOUND', message: 'Store not found' });
83
-
84
- await expectCommanderError(
85
- () => handlePrune(ctx, 'missing', {}, {}),
86
- 'STORE_NOT_FOUND',
87
- 'Store not found',
88
- );
89
- });
90
-
91
- it('should throw CommanderError when root category cannot be loaded', async () => {
92
- const { ctx } = createMockContext();
93
-
94
- (ctx.cortex as unknown as { getStore: () => unknown }).getStore = () =>
95
- okResult({ root: () => errResult({ code: 'CATEGORY_NOT_FOUND', message: 'Root missing' }) });
96
-
97
- await expectCommanderError(
98
- () => handlePrune(ctx, 'global', {}, {}),
99
- 'CATEGORY_NOT_FOUND',
100
- 'Root missing',
101
- );
102
- });
103
-
104
- it('should throw CommanderError when prune operation fails', async () => {
105
- const { ctx } = createMockContext();
106
-
107
- const root = {
108
- prune: async () => errResult({ code: 'PRUNE_FAILED', message: 'Unable to prune' }),
109
- };
110
-
111
- (ctx.cortex as unknown as { getStore: () => unknown }).getStore = () =>
112
- okResult({ root: () => okResult(root) });
113
-
114
- await expectCommanderError(
115
- () => handlePrune(ctx, 'global', {}, {}),
116
- 'PRUNE_FAILED',
117
- 'Unable to prune',
118
- );
119
- });
120
- });
@@ -1,152 +0,0 @@
1
- /**
2
- * Store prune command for removing expired memories.
3
- *
4
- * This command removes expired memories from the current store or a specified
5
- * store. It supports a dry-run mode to preview what would be deleted.
6
- *
7
- * @module cli/commands/store/prune
8
- *
9
- * @example
10
- * ```bash
11
- * # Prune expired memories from the current store
12
- * cortex store prune
13
- *
14
- * # Preview what would be pruned (dry-run)
15
- * cortex store prune --dry-run
16
- *
17
- * # Prune from a specific store
18
- * cortex --store work store prune
19
- * ```
20
- */
21
-
22
- import { Command } from '@commander-js/extra-typings';
23
- import { throwCliError } from '../../errors.ts';
24
- import { type CortexContext } from '@yeseh/cortex-core';
25
- import { createCliCommandContext } from '../../context.ts';
26
- import { resolveDefaultStore } from '../../utils/resolve-default-store.ts';
27
-
28
- /**
29
- * Options for the prune command.
30
- */
31
- export interface PruneCommandOptions {
32
- /** Show what would be pruned without actually deleting */
33
- dryRun?: boolean;
34
- }
35
-
36
- /**
37
- * Dependencies for the prune command handler.
38
- * Allows injection for testing.
39
- */
40
- export interface PruneHandlerDeps {
41
- /** Output stream for writing results (defaults to process.stdout) */
42
- stdout?: NodeJS.WritableStream;
43
- /** Current time for expiry checks (defaults to ctx.now()) */
44
- now?: Date;
45
- }
46
-
47
- /**
48
- * Handles the prune command execution.
49
- *
50
- * Thin CLI handler that delegates all business logic to the core
51
- * prune operation via CategoryClient. This handler is responsible
52
- * only for:
53
- * 1. Resolving the store via CortexContext
54
- * 2. Calling the category prune operation
55
- * 3. Formatting output for the CLI (dry-run preview vs. deletion summary)
56
- *
57
- * After pruning, the core operation automatically triggers a reindex to
58
- * clean up category indexes for removed memories.
59
- *
60
- * @module cli/commands/store/prune
61
- *
62
- * @param ctx - CortexContext providing access to Cortex client
63
- * @param storeName - Optional store name from the parent `--store` flag;
64
- * when `undefined`, resolves the default store
65
- * @param options - Command options controlling pruning behavior
66
- * @param options.dryRun - When `true`, lists expired memories without deleting
67
- * @param deps - Optional injected dependencies for testing
68
- * @throws {InvalidArgumentError} When the store cannot be resolved
69
- * (e.g., store name does not exist)
70
- * @throws {CommanderError} When the core prune operation fails
71
- * (e.g., I/O errors, serialization failures)
72
- *
73
- * @example
74
- * ```typescript
75
- * // Direct invocation in tests
76
- * const out = new PassThrough();
77
- * await handlePrune(ctx, 'my-store', { dryRun: true }, {
78
- * stdout: out,
79
- * now: new Date('2025-01-01'),
80
- * });
81
- * ```
82
- */
83
- export async function handlePrune(
84
- ctx: CortexContext,
85
- storeName: string | undefined,
86
- options: PruneCommandOptions,
87
- deps: PruneHandlerDeps = {}
88
- ): Promise<void> {
89
- const now = deps.now ?? ctx.now();
90
- const stdout = deps.stdout ?? ctx.stdout ?? process.stdout;
91
-
92
- // Get store through Cortex client
93
- const storeResult = ctx.cortex.getStore(resolveDefaultStore(ctx, storeName));
94
- if (!storeResult.ok()) {
95
- throwCliError(storeResult.error);
96
- }
97
-
98
- const store = storeResult.value;
99
- const rootResult = store.root();
100
- if (!rootResult.ok()) {
101
- throwCliError(rootResult.error);
102
- }
103
-
104
- // Use the category's prune method
105
- const result = await rootResult.value.prune({
106
- dryRun: options.dryRun,
107
- now,
108
- });
109
-
110
- if (!result.ok()) {
111
- throwCliError(result.error);
112
- }
113
-
114
- const pruned = result.value.pruned;
115
-
116
- // Format output
117
- if (pruned.length === 0) {
118
- stdout.write('No expired memories found.\n');
119
- return;
120
- }
121
-
122
- const paths = pruned.map((entry) => entry.path).join('\n ');
123
- if (options.dryRun) {
124
- stdout.write(`Would prune ${pruned.length} expired memories:\n ${paths}\n`);
125
- } else {
126
- stdout.write(`Pruned ${pruned.length} expired memories:\n ${paths}\n`);
127
- }
128
- }
129
-
130
- /**
131
- * The `prune` subcommand for removing expired memories.
132
- *
133
- * Removes all expired memories from the store. Use --dry-run to preview
134
- * what would be deleted without actually removing anything.
135
- *
136
- * @example
137
- * ```bash
138
- * cortex store prune
139
- * cortex store prune --dry-run
140
- * ```
141
- */
142
- export const pruneCommand = new Command('prune')
143
- .description('Remove expired memories from the store')
144
- .option('--dry-run', 'Show what would be pruned without deleting')
145
- .action(async (options, command) => {
146
- const parentOpts = command.parent?.opts() as { store?: string } | undefined;
147
- const context = await createCliCommandContext();
148
- if (!context.ok()) {
149
- throwCliError(context.error);
150
- }
151
- await handlePrune(context.value, parentOpts?.store, options);
152
- });
@@ -1,94 +0,0 @@
1
- /**
2
- * Unit tests for the store reindex command handler.
3
- *
4
- * @module cli/store/commands/reindexs.spec
5
- */
6
-
7
- import { describe, it, expect } from 'bun:test';
8
- import { handleReindex } from './reindexs.ts';
9
- import {
10
- createMockContext,
11
- captureOutput,
12
- expectCommanderError,
13
- errResult,
14
- okResult,
15
- } from '../../test-helpers.spec.ts';
16
-
17
- describe('handleReindex', () => {
18
- it('should use default store when no store name is provided', async () => {
19
- const { ctx, stdout } = createMockContext();
20
- const calls: string[] = [];
21
-
22
- const root = {
23
- reindex: async () => okResult({ warnings: [] }),
24
- };
25
-
26
- (ctx.cortex as unknown as { getStore: (name: string) => unknown }).getStore = (name: string) => {
27
- calls.push(name);
28
- return okResult({ root: () => okResult(root) });
29
- };
30
-
31
- await handleReindex(ctx, undefined, { stdout });
32
-
33
- expect(calls).toEqual(['global']);
34
- expect(captureOutput(stdout)).toContain("Reindexed category indexes for store 'global'.");
35
- });
36
-
37
- it('should output success message for an explicit store', async () => {
38
- const { ctx, stdout } = createMockContext();
39
-
40
- const root = {
41
- reindex: async () => okResult({ warnings: [] }),
42
- };
43
-
44
- (ctx.cortex as unknown as { getStore: () => unknown }).getStore = () =>
45
- okResult({ root: () => okResult(root) });
46
-
47
- await handleReindex(ctx, 'work', { stdout });
48
-
49
- expect(captureOutput(stdout)).toContain("Reindexed category indexes for store 'work'.");
50
- });
51
-
52
- it('should throw CommanderError when store resolution fails', async () => {
53
- const { ctx } = createMockContext();
54
-
55
- (ctx.cortex as unknown as { getStore: () => unknown }).getStore = () =>
56
- errResult({ code: 'STORE_NOT_FOUND', message: 'Store not found' });
57
-
58
- await expectCommanderError(
59
- () => handleReindex(ctx, 'missing'),
60
- 'STORE_NOT_FOUND',
61
- 'Store not found',
62
- );
63
- });
64
-
65
- it('should throw CommanderError when root category cannot be loaded', async () => {
66
- const { ctx } = createMockContext();
67
-
68
- (ctx.cortex as unknown as { getStore: () => unknown }).getStore = () =>
69
- okResult({ root: () => errResult({ code: 'CATEGORY_NOT_FOUND', message: 'Root missing' }) });
70
-
71
- await expectCommanderError(
72
- () => handleReindex(ctx, 'global'),
73
- 'CATEGORY_NOT_FOUND',
74
- 'Root missing',
75
- );
76
- });
77
-
78
- it('should map reindex failures to REINDEX_FAILED', async () => {
79
- const { ctx } = createMockContext();
80
-
81
- const root = {
82
- reindex: async () => errResult({ code: 'INDEX_WRITE_FAILED', message: 'Index write failed' }),
83
- };
84
-
85
- (ctx.cortex as unknown as { getStore: () => unknown }).getStore = () =>
86
- okResult({ root: () => okResult(root) });
87
-
88
- await expectCommanderError(
89
- () => handleReindex(ctx, 'global'),
90
- 'REINDEX_FAILED',
91
- 'Index write failed',
92
- );
93
- });
94
- });
@@ -1,97 +0,0 @@
1
- /**
2
- * Store reindex command for rebuilding category indexes.
3
- *
4
- * This command rebuilds the category indexes for a store, which can help
5
- * repair corrupted indexes or synchronize them after manual file changes.
6
- *
7
- * @example
8
- * ```bash
9
- * # Reindex the default store
10
- * cortex store reindex
11
- *
12
- * # Reindex a specific named store
13
- * cortex store --store work reindex
14
- * ```
15
- */
16
-
17
- import { Command } from '@commander-js/extra-typings';
18
- import { throwCliError } from '../../errors.ts';
19
- import { type CortexContext } from '@yeseh/cortex-core';
20
- import { createCliCommandContext } from '../../context.ts';
21
- import { resolveDefaultStore } from '../../utils/resolve-default-store.ts';
22
-
23
- /**
24
- * Dependencies for the reindex command handler.
25
- * Allows injection for testing.
26
- */
27
- export interface ReindexHandlerDeps {
28
- /** Output stream for writing results (defaults to process.stdout) */
29
- stdout?: NodeJS.WritableStream;
30
- }
31
-
32
- /**
33
- * Handles the reindex command execution.
34
- *
35
- * This function:
36
- * 1. Resolves the store context (from --store option or default resolution)
37
- * 2. Gets the root category for the store
38
- * 3. Rebuilds the category indexes
39
- * 4. Outputs the result
40
- *
41
- * @param ctx - The Cortex context
42
- * @param storeName - Optional store name from parent --store option
43
- * @param deps - Optional dependencies for testing
44
- * @throws {CommanderError} When store resolution or reindexing fails
45
- */
46
- export async function handleReindex(
47
- ctx: CortexContext,
48
- storeName: string | undefined,
49
- deps: ReindexHandlerDeps = {}
50
- ): Promise<void> {
51
- const stdout = deps.stdout ?? ctx.stdout ?? process.stdout;
52
-
53
- // Get store through Cortex client
54
- const effectiveStoreName = resolveDefaultStore(ctx, storeName);
55
- const storeResult = ctx.cortex.getStore(effectiveStoreName);
56
- if (!storeResult.ok()) {
57
- throwCliError(storeResult.error);
58
- }
59
-
60
- const store = storeResult.value;
61
- const rootResult = store.root();
62
- if (!rootResult.ok()) {
63
- throwCliError(rootResult.error);
64
- }
65
-
66
- // Use the category's reindex method
67
- const reindexResult = await rootResult.value.reindex();
68
- if (!reindexResult.ok()) {
69
- throwCliError({ code: 'REINDEX_FAILED', message: reindexResult.error.message });
70
- }
71
-
72
- // Output result
73
- stdout.write(`Reindexed category indexes for store '${effectiveStoreName}'.\n`);
74
- }
75
-
76
- /**
77
- * The `reindex` subcommand for rebuilding category indexes.
78
- *
79
- * Rebuilds the category indexes for a store, which can help repair corrupted
80
- * indexes or synchronize them after manual file changes.
81
- *
82
- * @example
83
- * ```bash
84
- * cortex store reindex
85
- * cortex store --store work reindex
86
- * ```
87
- */
88
- export const reindexCommand = new Command('reindex')
89
- .description('Rebuild category indexes for the store')
90
- .action(async (_options, command) => {
91
- const parentOpts = command.parent?.opts() as { store?: string } | undefined;
92
- const context = await createCliCommandContext();
93
- if (!context.ok()) {
94
- throwCliError(context.error);
95
- }
96
- await handleReindex(context.value, parentOpts?.store);
97
- });
@@ -1,97 +0,0 @@
1
- /**
2
- * Unit tests for the store remove command handler.
3
- *
4
- * Note: `handleRemove` reads and writes the global config file via `Bun.file()` and
5
- * `Bun.write()`. These I/O calls are not dependency-injected, so the success
6
- * path is not testable at the unit level without filesystem mocking (prohibited
7
- * by project rules). Only the validation paths that throw before any I/O are
8
- * covered here.
9
- *
10
- * Slug normalization behavior: `Slug.from()` normalizes most inputs rather than
11
- * rejecting them. Only empty/whitespace strings fail `Slug.from()` and produce
12
- * `InvalidArgumentError`.
13
- *
14
- * @module cli/store/commands/remove.spec
15
- */
16
-
17
- import { describe, it } from 'bun:test';
18
- import { handleRemove } from './remove.ts';
19
- import {
20
- createMockContext,
21
- expectInvalidArgumentError,
22
- expectCommanderError,
23
- } from '../../test-helpers.spec.ts';
24
-
25
- describe('handleRemove', () => {
26
- describe('store name validation', () => {
27
- it('should throw InvalidArgumentError for an empty store name', async () => {
28
- const { ctx } = createMockContext();
29
-
30
- await expectInvalidArgumentError(
31
- () => handleRemove(ctx, ''),
32
- 'Store name must be a lowercase slug',
33
- );
34
- });
35
-
36
- it('should throw InvalidArgumentError for a whitespace-only store name', async () => {
37
- const { ctx } = createMockContext();
38
-
39
- await expectInvalidArgumentError(
40
- () => handleRemove(ctx, ' '),
41
- 'Store name must be a lowercase slug',
42
- );
43
- });
44
- });
45
-
46
- describe('store not found check', () => {
47
- it('should throw CommanderError when the store does not exist in context', async () => {
48
- // Provide an empty store registry so any store name fails the check
49
- const { ctx } = createMockContext({ stores: {} });
50
-
51
- await expectCommanderError(
52
- () => handleRemove(ctx, 'nonexistent'),
53
- 'STORE_NOT_FOUND',
54
- 'not registered',
55
- );
56
- });
57
-
58
- it('should throw CommanderError with the store name included in message', async () => {
59
- const { ctx } = createMockContext({ stores: {} });
60
-
61
- await expectCommanderError(
62
- () => handleRemove(ctx, 'my-store'),
63
- 'STORE_NOT_FOUND',
64
- 'my-store',
65
- );
66
- });
67
-
68
- it('should throw CommanderError for a valid name not in context', async () => {
69
- const { ctx } = createMockContext({
70
- stores: {
71
- global: {
72
- kind: 'filesystem',
73
- categoryMode: 'free',
74
- categories: {},
75
- properties: { path: '/default/path' },
76
- },
77
- },
78
- });
79
-
80
- // 'work' is a valid slug but not registered in this context
81
- await expectCommanderError(() => handleRemove(ctx, 'work'), 'STORE_NOT_FOUND');
82
- });
83
-
84
- it('should normalize the name before checking for store existence', async () => {
85
- // Slug.from('WORK') → 'work'; if 'work' is not in stores, STORE_NOT_FOUND
86
- const { ctx } = createMockContext({ stores: {} });
87
-
88
- await expectCommanderError(() => handleRemove(ctx, 'WORK'), 'STORE_NOT_FOUND');
89
- });
90
- });
91
-
92
- // NOTE: The success path (steps 3–6) requires reading and writing the global
93
- // config file via Bun.file() / Bun.write(). These calls use the hardcoded
94
- // getDefaultConfigPath() with no injection point, so they cannot be tested
95
- // at the unit level without filesystem mocking (which is prohibited).
96
- // The success path should be covered by integration tests.
97
- });