@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
package/src/output.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output format types and serialization helpers.
|
|
3
|
-
*
|
|
4
|
-
* This module provides type definitions for CLI output payloads and a thin
|
|
5
|
-
* wrapper around the core serialize function. Validation is expected to happen
|
|
6
|
-
* at object construction time, not during serialization.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { err, ok, type Result } from '@yeseh/cortex-core';
|
|
10
|
-
import { serialize, type OutputFormat } from '@yeseh/cortex-core';
|
|
11
|
-
|
|
12
|
-
// Re-export OutputFormat from core
|
|
13
|
-
export type { OutputFormat };
|
|
14
|
-
|
|
15
|
-
export interface OutputMemoryMetadata {
|
|
16
|
-
createdAt: Date;
|
|
17
|
-
updatedAt?: Date;
|
|
18
|
-
tags: string[];
|
|
19
|
-
source?: string;
|
|
20
|
-
tokenEstimate?: number;
|
|
21
|
-
expiresAt?: Date;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface OutputPath {
|
|
25
|
-
path: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface OutputMemory {
|
|
29
|
-
path: string;
|
|
30
|
-
metadata: OutputMemoryMetadata;
|
|
31
|
-
content: string;
|
|
32
|
-
}
|
|
33
|
-
export interface OutputMovedMemory {
|
|
34
|
-
from: string;
|
|
35
|
-
to: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface OutputCategoryMemory {
|
|
39
|
-
path: string;
|
|
40
|
-
tokenEstimate?: number;
|
|
41
|
-
summary?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface OutputSubcategory {
|
|
45
|
-
path: string;
|
|
46
|
-
memoryCount: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface OutputCategory {
|
|
50
|
-
path: string;
|
|
51
|
-
memories: OutputCategoryMemory[];
|
|
52
|
-
subcategories: OutputSubcategory[];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface OutputCreatedCategory {
|
|
56
|
-
path: string;
|
|
57
|
-
created: boolean;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface OutputStore {
|
|
61
|
-
name: string;
|
|
62
|
-
path: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface OutputStoreRegistry {
|
|
66
|
-
stores: OutputStore[];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface OutputStoreInit {
|
|
70
|
-
path: string;
|
|
71
|
-
name: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface OutputInit {
|
|
75
|
-
path: string;
|
|
76
|
-
categories: string[];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export type OutputPayload =
|
|
80
|
-
| { kind: 'memory'; value: OutputMemory }
|
|
81
|
-
| { kind: 'moved-memory'; value: OutputMovedMemory }
|
|
82
|
-
| { kind: 'path'; value: OutputPath }
|
|
83
|
-
| { kind: 'category'; value: OutputCategory }
|
|
84
|
-
| { kind: 'created-category'; value: OutputCreatedCategory}
|
|
85
|
-
| { kind: 'store'; value: OutputStore }
|
|
86
|
-
| { kind: 'store-registry'; value: OutputStoreRegistry }
|
|
87
|
-
| { kind: 'store-init'; value: OutputStoreInit }
|
|
88
|
-
| { kind: 'init'; value: OutputInit };
|
|
89
|
-
|
|
90
|
-
export interface OutputSerializeError {
|
|
91
|
-
code: 'INVALID_FORMAT' | 'SERIALIZE_FAILED';
|
|
92
|
-
message: string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Serialize an output payload to the specified format.
|
|
97
|
-
*
|
|
98
|
-
* This is a thin wrapper around the core serialize function that extracts
|
|
99
|
-
* the value from the discriminated union and handles errors.
|
|
100
|
-
*
|
|
101
|
-
* @param payload - The output payload to serialize
|
|
102
|
-
* @param format - The output format ('yaml', 'json', or 'toon')
|
|
103
|
-
* @returns Result with serialized string or error
|
|
104
|
-
*/
|
|
105
|
-
export const serializeOutput = (
|
|
106
|
-
payload: OutputPayload,
|
|
107
|
-
format: OutputFormat,
|
|
108
|
-
): Result<string, OutputSerializeError> => {
|
|
109
|
-
const result = serialize(payload, format);
|
|
110
|
-
|
|
111
|
-
if (result.ok()) {
|
|
112
|
-
return ok(result.value);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return err({
|
|
116
|
-
code: result.error.code === 'INVALID_FORMAT' ? 'INVALID_FORMAT' : 'SERIALIZE_FAILED',
|
|
117
|
-
message: result.error.message,
|
|
118
|
-
});
|
|
119
|
-
};
|
package/src/program.spec.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the Commander.js program setup and runProgram entrypoint.
|
|
3
|
-
*
|
|
4
|
-
* Verifies that the program is correctly wired with the expected name,
|
|
5
|
-
* version, and top-level subcommands, and that `runProgram` is exported
|
|
6
|
-
* as a callable function.
|
|
7
|
-
*
|
|
8
|
-
* @module cli/program.spec
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect } from 'bun:test';
|
|
12
|
-
import { program, runProgram } from './program.ts';
|
|
13
|
-
import packageInfo from '../package.json'
|
|
14
|
-
|
|
15
|
-
describe('program', () => {
|
|
16
|
-
it('should have name "cortex"', () => {
|
|
17
|
-
expect(program.name()).toBe('cortex');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it(`should have version "${packageInfo.version}"`, () => {
|
|
21
|
-
expect(program.version()).toBe(packageInfo.version);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('should have "memory" command registered', () => {
|
|
25
|
-
const names = program.commands.map((c) => c.name());
|
|
26
|
-
expect(names).toContain('memory');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should have "store" command registered', () => {
|
|
30
|
-
const names = program.commands.map((c) => c.name());
|
|
31
|
-
expect(names).toContain('store');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should have "init" command registered', () => {
|
|
35
|
-
const names = program.commands.map((c) => c.name());
|
|
36
|
-
expect(names).toContain('init');
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('runProgram', () => {
|
|
41
|
-
it('should be a function', () => {
|
|
42
|
-
// Do NOT call runProgram() — it would parse process.argv and trigger
|
|
43
|
-
// real command execution. We only verify it is exported correctly.
|
|
44
|
-
expect(typeof runProgram).toBe('function');
|
|
45
|
-
});
|
|
46
|
-
});
|
package/src/program.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Commander.js program setup for Cortex CLI.
|
|
3
|
-
*
|
|
4
|
-
* This module sets up the main Commander program and wires together
|
|
5
|
-
* all command groups. It serves as the entry point for the CLI.
|
|
6
|
-
*
|
|
7
|
-
* @module cli/program
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { Command } from '@commander-js/extra-typings';
|
|
11
|
-
|
|
12
|
-
import { memoryCommand } from './memory/index.ts';
|
|
13
|
-
import { storeCommand } from './store/index.ts';
|
|
14
|
-
import { initCommand } from './commands/init.ts';
|
|
15
|
-
import { categoryCommand } from './category/index.ts';
|
|
16
|
-
import { createCliLogger } from './observability.ts';
|
|
17
|
-
import packageInfo from '../package.json'
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The main Commander program instance for Cortex CLI.
|
|
21
|
-
*
|
|
22
|
-
* Configured with:
|
|
23
|
-
* - Name: cortex
|
|
24
|
-
* - Description: Memory system for AI agents
|
|
25
|
-
* - Version: 0.1.0
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```ts
|
|
29
|
-
* import { program } from './program.ts';
|
|
30
|
-
* await program.parseAsync(process.argv);
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
const program = new Command()
|
|
34
|
-
.name('cortex')
|
|
35
|
-
.description('Memory system for AI agents')
|
|
36
|
-
.version(packageInfo.version);
|
|
37
|
-
|
|
38
|
-
program.addCommand(memoryCommand);
|
|
39
|
-
program.addCommand(categoryCommand);
|
|
40
|
-
program.addCommand(storeCommand);
|
|
41
|
-
program.addCommand(initCommand);
|
|
42
|
-
|
|
43
|
-
export { program };
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Runs the CLI program by parsing command-line arguments.
|
|
47
|
-
*
|
|
48
|
-
* This function handles errors gracefully and sets the appropriate
|
|
49
|
-
* exit code on failure.
|
|
50
|
-
*
|
|
51
|
-
* @returns A promise that resolves when the program completes
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* ```ts
|
|
55
|
-
* import { runProgram } from './program.ts';
|
|
56
|
-
* await runProgram();
|
|
57
|
-
* ```
|
|
58
|
-
*/
|
|
59
|
-
export const runProgram = async (): Promise<void> => {
|
|
60
|
-
try {
|
|
61
|
-
await program.parseAsync(process.argv);
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
// Commander.js handles most errors by writing to stderr and exiting.
|
|
65
|
-
// This catch handles any unexpected errors that slip through.
|
|
66
|
-
const logger = createCliLogger();
|
|
67
|
-
if (error instanceof Error) {
|
|
68
|
-
logger.error(error.message);
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
logger.error('An unexpected error occurred');
|
|
72
|
-
}
|
|
73
|
-
process.exitCode = 1;
|
|
74
|
-
}
|
|
75
|
-
};
|
package/src/run.spec.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the run.ts CLI entrypoint.
|
|
3
|
-
*
|
|
4
|
-
* `run.ts` is a side-effect-only module that calls `runProgram()` on import.
|
|
5
|
-
* Because importing it would immediately parse `process.argv`, we do NOT
|
|
6
|
-
* import it directly in tests. Instead we verify that the function it
|
|
7
|
-
* delegates to (`runProgram`) is correctly exported from `program.ts`.
|
|
8
|
-
*
|
|
9
|
-
* @module cli/run.spec
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, it, expect } from 'bun:test';
|
|
13
|
-
|
|
14
|
-
import { runProgram } from './program.ts';
|
|
15
|
-
|
|
16
|
-
describe('run module', () => {
|
|
17
|
-
it('should delegate to runProgram from program.ts', () => {
|
|
18
|
-
// run.ts calls runProgram() — verify the delegated function is callable
|
|
19
|
-
expect(typeof runProgram).toBe('function');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('runProgram should return a Promise', () => {
|
|
23
|
-
// Verify the return type without actually executing
|
|
24
|
-
// (calling it would parse process.argv)
|
|
25
|
-
const returnType = runProgram.constructor.name;
|
|
26
|
-
// AsyncFunction or Function — both are acceptable
|
|
27
|
-
expect([
|
|
28
|
-
'Function', 'AsyncFunction',
|
|
29
|
-
]).toContain(returnType);
|
|
30
|
-
});
|
|
31
|
-
});
|
package/src/run.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the store add command handler.
|
|
3
|
-
*
|
|
4
|
-
* Note: `handleAdd` 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 (e.g. 'INVALID' → 'invalid', 'my store' → 'my-store'). Only
|
|
12
|
-
* empty/whitespace strings fail `Slug.from()` and produce `InvalidArgumentError`.
|
|
13
|
-
*
|
|
14
|
-
* @module cli/store/commands/add.spec
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { describe, it } from 'bun:test';
|
|
18
|
-
import { handleAdd } from './add.ts';
|
|
19
|
-
import {
|
|
20
|
-
createMockContext,
|
|
21
|
-
expectInvalidArgumentError,
|
|
22
|
-
expectCommanderError,
|
|
23
|
-
} from '../../test-helpers.spec.ts';
|
|
24
|
-
|
|
25
|
-
describe('handleAdd', () => {
|
|
26
|
-
describe('store name validation', () => {
|
|
27
|
-
it('should throw InvalidArgumentError for an empty store name', async () => {
|
|
28
|
-
const { ctx } = createMockContext({ stores: {} });
|
|
29
|
-
|
|
30
|
-
await expectInvalidArgumentError(
|
|
31
|
-
() => handleAdd(ctx, '', '/some/path'),
|
|
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({ stores: {} });
|
|
38
|
-
|
|
39
|
-
await expectInvalidArgumentError(
|
|
40
|
-
() => handleAdd(ctx, ' ', '/some/path'),
|
|
41
|
-
'Store name must be a lowercase slug',
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('store path validation', () => {
|
|
47
|
-
it('should throw InvalidArgumentError for an empty store path', async () => {
|
|
48
|
-
const { ctx } = createMockContext({ stores: {} });
|
|
49
|
-
|
|
50
|
-
await expectInvalidArgumentError(
|
|
51
|
-
() => handleAdd(ctx, 'valid-name', ''),
|
|
52
|
-
'Store path is required',
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should throw InvalidArgumentError for a whitespace-only store path', async () => {
|
|
57
|
-
const { ctx } = createMockContext({ stores: {} });
|
|
58
|
-
|
|
59
|
-
await expectInvalidArgumentError(
|
|
60
|
-
() => handleAdd(ctx, 'valid-name', ' '),
|
|
61
|
-
'Store path is required',
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('store already exists check', () => {
|
|
67
|
-
it('should throw CommanderError when the store name already exists in context', async () => {
|
|
68
|
-
// 'global' is pre-registered in the mock context
|
|
69
|
-
const { ctx } = createMockContext({
|
|
70
|
-
stores: {
|
|
71
|
-
global: {
|
|
72
|
-
kind: 'filesystem',
|
|
73
|
-
categoryMode: 'free',
|
|
74
|
-
categories: {},
|
|
75
|
-
properties: { path: '/existing/path' },
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
await expectCommanderError(
|
|
81
|
-
() => handleAdd(ctx, 'global', '/new/path'),
|
|
82
|
-
'STORE_ALREADY_EXISTS',
|
|
83
|
-
'already registered',
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should throw CommanderError with store name in the error message', async () => {
|
|
88
|
-
const { ctx } = createMockContext({
|
|
89
|
-
stores: {
|
|
90
|
-
'my-store': {
|
|
91
|
-
kind: 'filesystem',
|
|
92
|
-
categoryMode: 'free',
|
|
93
|
-
categories: {},
|
|
94
|
-
properties: { path: '/some/path' },
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
await expectCommanderError(
|
|
100
|
-
() => handleAdd(ctx, 'my-store', '/new/path'),
|
|
101
|
-
'STORE_ALREADY_EXISTS',
|
|
102
|
-
'my-store',
|
|
103
|
-
);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should normalize the store name before checking for existence', async () => {
|
|
107
|
-
// Slug.from('MY-STORE') → 'my-store', so 'MY-STORE' matches 'my-store'
|
|
108
|
-
const { ctx } = createMockContext({
|
|
109
|
-
stores: {
|
|
110
|
-
'my-store': {
|
|
111
|
-
kind: 'filesystem',
|
|
112
|
-
categoryMode: 'free',
|
|
113
|
-
categories: {},
|
|
114
|
-
properties: { path: '/some/path' },
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
await expectCommanderError(
|
|
120
|
-
() => handleAdd(ctx, 'MY-STORE', '/new/path'),
|
|
121
|
-
'STORE_ALREADY_EXISTS',
|
|
122
|
-
);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// NOTE: The success path (steps 4–6) requires reading and writing the global
|
|
127
|
-
// config file via Bun.file() / Bun.write(). These calls use the hardcoded
|
|
128
|
-
// getDefaultConfigPath() with no injection point, so they cannot be tested
|
|
129
|
-
// at the unit level without filesystem mocking (which is prohibited).
|
|
130
|
-
// The success path should be covered by integration tests.
|
|
131
|
-
});
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Store add command for registering a new store.
|
|
3
|
-
*
|
|
4
|
-
* This command registers a new store in the global registry with a given
|
|
5
|
-
* name and filesystem path. Paths are resolved relative to the current
|
|
6
|
-
* working directory, with support for tilde expansion.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```bash
|
|
10
|
-
* # Register a store with an absolute path
|
|
11
|
-
* cortex store add work /path/to/work/memories
|
|
12
|
-
*
|
|
13
|
-
* # Register a store with a relative path
|
|
14
|
-
* cortex store add project ./cortex
|
|
15
|
-
*
|
|
16
|
-
* # Register a store with tilde expansion
|
|
17
|
-
* cortex store add personal ~/memories
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { mkdir } from 'node:fs/promises';
|
|
22
|
-
import { dirname } from 'node:path';
|
|
23
|
-
import { Command } from '@commander-js/extra-typings';
|
|
24
|
-
import { throwCliError } from '../../errors.ts';
|
|
25
|
-
import { getDefaultConfigPath } from '../../utils/paths.ts';
|
|
26
|
-
import { serializeOutput, type OutputStore, type OutputFormat } from '../../output.ts';
|
|
27
|
-
import { resolveUserPath } from '../../utils/paths.ts';
|
|
28
|
-
import { Slug, parseConfig, type CortexContext, type ConfigStore } from '@yeseh/cortex-core';
|
|
29
|
-
import { createCliCommandContext } from '../../context.ts';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Options for the add command.
|
|
33
|
-
*/
|
|
34
|
-
export interface AddCommandOptions {
|
|
35
|
-
/** Output format (yaml, json, toon) */
|
|
36
|
-
format?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Dependencies for the add command handler.
|
|
41
|
-
* Allows injection for testing.
|
|
42
|
-
*/
|
|
43
|
-
export interface AddHandlerDeps {
|
|
44
|
-
/** Output stream for writing results (defaults to process.stdout) */
|
|
45
|
-
stdout?: NodeJS.WritableStream;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Validates store name input.
|
|
50
|
-
*
|
|
51
|
-
* @param name - The raw store name input
|
|
52
|
-
* @returns The validated, trimmed store name
|
|
53
|
-
* @throws {InvalidArgumentError} When the store name is empty or invalid
|
|
54
|
-
*/
|
|
55
|
-
function validateStoreName(name: string): string {
|
|
56
|
-
const slugResult = Slug.from(name);
|
|
57
|
-
if (!slugResult.ok()) {
|
|
58
|
-
throwCliError({
|
|
59
|
-
code: 'INVALID_STORE_NAME',
|
|
60
|
-
message: 'Store name must be a lowercase slug (letters, numbers, hyphens).',
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return slugResult.value.toString();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Validates and resolves store path input.
|
|
69
|
-
*
|
|
70
|
-
* @param storePath - The raw store path input
|
|
71
|
-
* @param cwd - The current working directory for relative path resolution
|
|
72
|
-
* @returns The resolved absolute path
|
|
73
|
-
* @throws {InvalidArgumentError} When the store path is empty
|
|
74
|
-
*/
|
|
75
|
-
function validateAndResolvePath(storePath: string, cwd: string): string {
|
|
76
|
-
const trimmed = storePath.trim();
|
|
77
|
-
if (!trimmed) {
|
|
78
|
-
throwCliError({ code: 'INVALID_STORE_PATH', message: 'Store path is required.' });
|
|
79
|
-
}
|
|
80
|
-
return resolveUserPath(trimmed, cwd);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Writes the serialized output to the output stream.
|
|
85
|
-
*
|
|
86
|
-
* @param output - The store output payload
|
|
87
|
-
* @param format - The output format
|
|
88
|
-
* @param stdout - The output stream
|
|
89
|
-
*/
|
|
90
|
-
function writeOutput(
|
|
91
|
-
output: OutputStore,
|
|
92
|
-
format: OutputFormat,
|
|
93
|
-
stdout: NodeJS.WritableStream,
|
|
94
|
-
): void {
|
|
95
|
-
const serialized = serializeOutput({ kind: 'store', value: output }, format);
|
|
96
|
-
if (!serialized.ok()) {
|
|
97
|
-
throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
|
|
98
|
-
}
|
|
99
|
-
stdout.write(serialized.value + '\n');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Handles the add command execution.
|
|
104
|
-
*
|
|
105
|
-
* This function:
|
|
106
|
-
* 1. Validates the store name format
|
|
107
|
-
* 2. Validates and resolves the store path
|
|
108
|
-
* 3. Checks if store already exists in context
|
|
109
|
-
* 4. Reads current config file
|
|
110
|
-
* 5. Adds the store to the config and saves
|
|
111
|
-
* 6. Outputs the result
|
|
112
|
-
*
|
|
113
|
-
* @param ctx - The CortexContext with loaded configuration
|
|
114
|
-
* @param name - The store name to register
|
|
115
|
-
* @param storePath - The filesystem path to the store
|
|
116
|
-
* @param options - Command options (format)
|
|
117
|
-
* @param deps - Optional dependencies for testing
|
|
118
|
-
* @throws {InvalidArgumentError} When the store name or path is invalid
|
|
119
|
-
* @throws {CommanderError} When the store already exists or config operations fail
|
|
120
|
-
*/
|
|
121
|
-
export async function handleAdd(
|
|
122
|
-
ctx: CortexContext,
|
|
123
|
-
name: string,
|
|
124
|
-
storePath: string,
|
|
125
|
-
options: AddCommandOptions = {},
|
|
126
|
-
deps: AddHandlerDeps = {},
|
|
127
|
-
): Promise<void> {
|
|
128
|
-
const cwd = ctx.cwd ?? process.cwd();
|
|
129
|
-
const stdout = deps.stdout ?? ctx.stdout ?? process.stdout;
|
|
130
|
-
const configPath = getDefaultConfigPath();
|
|
131
|
-
|
|
132
|
-
// 1. Validate inputs
|
|
133
|
-
const trimmedName = validateStoreName(name);
|
|
134
|
-
const resolvedPath = validateAndResolvePath(storePath, cwd);
|
|
135
|
-
|
|
136
|
-
// 2. Check if store already exists in context
|
|
137
|
-
if (ctx.stores[trimmedName]) {
|
|
138
|
-
throwCliError({
|
|
139
|
-
code: 'STORE_ALREADY_EXISTS',
|
|
140
|
-
message: `Store '${trimmedName}' is already registered.`,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// 3. Read current config file (or start with empty config if it doesn't exist)
|
|
145
|
-
const configFile = Bun.file(configPath);
|
|
146
|
-
let parsedConfig: { settings?: unknown; stores: Record<string, unknown> };
|
|
147
|
-
|
|
148
|
-
if (await configFile.exists()) {
|
|
149
|
-
let configContents: string;
|
|
150
|
-
try {
|
|
151
|
-
configContents = await configFile.text();
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
throwCliError({
|
|
155
|
-
code: 'CONFIG_READ_FAILED',
|
|
156
|
-
message: `Failed to read config at ${configPath}`,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const parsed = parseConfig(configContents!);
|
|
161
|
-
if (!parsed.ok()) {
|
|
162
|
-
throwCliError(parsed.error);
|
|
163
|
-
}
|
|
164
|
-
parsedConfig = parsed.value;
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
// Config doesn't exist yet — start with empty config
|
|
168
|
-
parsedConfig = { settings: undefined, stores: {} };
|
|
169
|
-
// Ensure config directory exists
|
|
170
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 4. Add new store to config
|
|
174
|
-
const newStore: ConfigStore = {
|
|
175
|
-
kind: 'filesystem',
|
|
176
|
-
categoryMode: 'free',
|
|
177
|
-
categories: {},
|
|
178
|
-
properties: { path: resolvedPath },
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const updatedConfig = {
|
|
182
|
-
...parsedConfig,
|
|
183
|
-
stores: {
|
|
184
|
-
...parsedConfig.stores,
|
|
185
|
-
[trimmedName]: newStore,
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// 5. Write updated config
|
|
190
|
-
const serialized = Bun.YAML.stringify(updatedConfig, null, 2);
|
|
191
|
-
try {
|
|
192
|
-
await Bun.write(configPath, serialized);
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
throwCliError({
|
|
196
|
-
code: 'CONFIG_WRITE_FAILED',
|
|
197
|
-
message: `Failed to write config at ${configPath}`,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// 6. Output result
|
|
202
|
-
const output: OutputStore = { name: trimmedName, path: resolvedPath };
|
|
203
|
-
const format: OutputFormat = (options.format as OutputFormat) ?? 'yaml';
|
|
204
|
-
writeOutput(output, format, stdout);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* The `add` subcommand for registering a new store.
|
|
209
|
-
*
|
|
210
|
-
* Registers a store with the given name and filesystem path. The path is
|
|
211
|
-
* resolved relative to the current working directory, with support for
|
|
212
|
-
* tilde expansion.
|
|
213
|
-
*
|
|
214
|
-
* @example
|
|
215
|
-
* ```bash
|
|
216
|
-
* cortex store add work /path/to/work/memories
|
|
217
|
-
* cortex store add project ./cortex --format json
|
|
218
|
-
* ```
|
|
219
|
-
*/
|
|
220
|
-
export const addCommand = new Command('add')
|
|
221
|
-
.description('Register a new store')
|
|
222
|
-
.argument('<name>', 'Store name (lowercase slug)')
|
|
223
|
-
.argument('<path>', 'Filesystem path to the store')
|
|
224
|
-
.option('-o, --format <format>', 'Output format (yaml, json, toon)', 'yaml')
|
|
225
|
-
.action(async (name, path, options) => {
|
|
226
|
-
const context = await createCliCommandContext();
|
|
227
|
-
if (!context.ok()) {
|
|
228
|
-
throwCliError(context.error);
|
|
229
|
-
}
|
|
230
|
-
await handleAdd(context.value, name, path, options);
|
|
231
|
-
});
|