@yeseh/cortex-cli 0.6.8 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/program.js +1538 -5
- package/dist/program.js.map +32 -3
- package/dist/run.d.ts +0 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +3 -4
- package/dist/run.js.map +3 -3
- package/package.json +4 -6
- package/dist/chunk-dsfj4baj.js +0 -1543
- package/dist/chunk-dsfj4baj.js.map +0 -38
- package/src/category/commands/create.spec.ts +0 -139
- package/src/category/commands/create.ts +0 -119
- package/src/category/index.ts +0 -24
- package/src/commands/init.spec.ts +0 -203
- package/src/commands/init.ts +0 -301
- package/src/context.spec.ts +0 -60
- package/src/context.ts +0 -170
- package/src/errors.spec.ts +0 -264
- package/src/errors.ts +0 -105
- package/src/memory/commands/add.spec.ts +0 -169
- package/src/memory/commands/add.ts +0 -158
- package/src/memory/commands/definitions.spec.ts +0 -80
- package/src/memory/commands/list.spec.ts +0 -123
- package/src/memory/commands/list.ts +0 -269
- package/src/memory/commands/move.spec.ts +0 -85
- package/src/memory/commands/move.ts +0 -119
- package/src/memory/commands/remove.spec.ts +0 -79
- package/src/memory/commands/remove.ts +0 -108
- package/src/memory/commands/show.spec.ts +0 -71
- package/src/memory/commands/show.ts +0 -165
- package/src/memory/commands/test-helpers.spec.ts +0 -127
- package/src/memory/commands/update.spec.ts +0 -86
- package/src/memory/commands/update.ts +0 -230
- package/src/memory/index.spec.ts +0 -59
- package/src/memory/index.ts +0 -44
- package/src/memory/parsing.spec.ts +0 -105
- package/src/memory/parsing.ts +0 -22
- package/src/observability.spec.ts +0 -126
- package/src/observability.ts +0 -82
- package/src/output.spec.ts +0 -835
- package/src/output.ts +0 -119
- package/src/program.spec.ts +0 -46
- package/src/program.ts +0 -75
- package/src/run.spec.ts +0 -31
- package/src/run.ts +0 -9
- package/src/store/commands/add.spec.ts +0 -131
- package/src/store/commands/add.ts +0 -231
- package/src/store/commands/init.spec.ts +0 -220
- package/src/store/commands/init.ts +0 -272
- package/src/store/commands/list.spec.ts +0 -175
- package/src/store/commands/list.ts +0 -102
- package/src/store/commands/prune.spec.ts +0 -120
- package/src/store/commands/prune.ts +0 -152
- package/src/store/commands/reindexs.spec.ts +0 -94
- package/src/store/commands/reindexs.ts +0 -97
- package/src/store/commands/remove.spec.ts +0 -97
- package/src/store/commands/remove.ts +0 -189
- package/src/store/index.spec.ts +0 -60
- package/src/store/index.ts +0 -49
- package/src/store/utils/resolve-store-name.spec.ts +0 -62
- package/src/store/utils/resolve-store-name.ts +0 -79
- package/src/test-helpers.spec.ts +0 -430
- package/src/tests/cli.integration.spec.ts +0 -1306
- package/src/toon.spec.ts +0 -183
- package/src/toon.ts +0 -462
- package/src/utils/git.spec.ts +0 -95
- package/src/utils/git.ts +0 -51
- package/src/utils/input.spec.ts +0 -326
- package/src/utils/input.ts +0 -150
- package/src/utils/paths.spec.ts +0 -235
- package/src/utils/paths.ts +0 -75
- package/src/utils/prompts.spec.ts +0 -23
- package/src/utils/prompts.ts +0 -88
- package/src/utils/resolve-default-store.spec.ts +0 -135
- 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
|
-
});
|