@yeseh/cortex-cli 0.6.7 → 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.
- 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-tgrm2cc9.js +0 -1543
- package/dist/chunk-tgrm2cc9.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/utils/git.spec.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for utils/git.ts
|
|
3
|
-
*
|
|
4
|
-
* @module cli/utils/git.spec
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, afterEach } from 'bun:test';
|
|
8
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
9
|
-
import { join, basename } from 'node:path';
|
|
10
|
-
import { tmpdir } from 'node:os';
|
|
11
|
-
import { runGitCommand, detectGitRepoName } from './git';
|
|
12
|
-
|
|
13
|
-
// The spec file lives inside a git worktree — use its own directory as a real
|
|
14
|
-
// git repo for tests that require one.
|
|
15
|
-
const THIS_DIR = import.meta.dir;
|
|
16
|
-
|
|
17
|
-
// ============================================================================
|
|
18
|
-
// Temp dir management
|
|
19
|
-
// ============================================================================
|
|
20
|
-
|
|
21
|
-
let tempDirs: string[] = [];
|
|
22
|
-
|
|
23
|
-
async function makeTempDir(): Promise<string> {
|
|
24
|
-
const dir = await mkdtemp(join(tmpdir(), 'cortex-git-test-'));
|
|
25
|
-
tempDirs.push(dir);
|
|
26
|
-
return dir;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
afterEach(async () => {
|
|
30
|
-
for (const dir of tempDirs) {
|
|
31
|
-
await rm(dir, { recursive: true, force: true });
|
|
32
|
-
}
|
|
33
|
-
tempDirs = [];
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// ============================================================================
|
|
37
|
-
// runGitCommand
|
|
38
|
-
// ============================================================================
|
|
39
|
-
|
|
40
|
-
describe('runGitCommand', () => {
|
|
41
|
-
it('should return ok with stdout for a valid git command', async () => {
|
|
42
|
-
const result = await runGitCommand([
|
|
43
|
-
'rev-parse', '--show-toplevel',
|
|
44
|
-
], THIS_DIR);
|
|
45
|
-
expect(result.ok).toBe(true);
|
|
46
|
-
if (result.ok) {
|
|
47
|
-
expect(typeof result.value).toBe('string');
|
|
48
|
-
expect(result.value.length).toBeGreaterThan(0);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should return {ok: false} for an invalid git command', async () => {
|
|
53
|
-
const result = await runGitCommand(['not-a-real-subcommand-xyz'], THIS_DIR);
|
|
54
|
-
expect(result.ok).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should return {ok: false} in a non-git directory', async () => {
|
|
58
|
-
const nonGitDir = await makeTempDir();
|
|
59
|
-
const result = await runGitCommand([
|
|
60
|
-
'rev-parse', '--show-toplevel',
|
|
61
|
-
], nonGitDir);
|
|
62
|
-
expect(result.ok).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// ============================================================================
|
|
67
|
-
// detectGitRepoName
|
|
68
|
-
// ============================================================================
|
|
69
|
-
|
|
70
|
-
describe('detectGitRepoName', () => {
|
|
71
|
-
it('should return a non-empty string for a git repo', async () => {
|
|
72
|
-
const name = await detectGitRepoName(THIS_DIR);
|
|
73
|
-
expect(typeof name).toBe('string');
|
|
74
|
-
expect((name ?? '').length).toBeGreaterThan(0);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should return the toplevel directory basename for a git repo', async () => {
|
|
78
|
-
// Get the toplevel via git, then compare what detectGitRepoName returns
|
|
79
|
-
const toplevelResult = await runGitCommand([
|
|
80
|
-
'rev-parse', '--show-toplevel',
|
|
81
|
-
], THIS_DIR);
|
|
82
|
-
expect(toplevelResult.ok).toBe(true);
|
|
83
|
-
if (toplevelResult.ok) {
|
|
84
|
-
const expectedName = basename(toplevelResult.value);
|
|
85
|
-
const name = await detectGitRepoName(THIS_DIR);
|
|
86
|
-
expect(name).toBe(expectedName);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should return null for a non-git directory', async () => {
|
|
91
|
-
const nonGitDir = await makeTempDir();
|
|
92
|
-
const name = await detectGitRepoName(nonGitDir);
|
|
93
|
-
expect(name).toBeNull();
|
|
94
|
-
});
|
|
95
|
-
});
|
package/src/utils/git.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { basename } from 'node:path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Executes a git command and returns the trimmed stdout.
|
|
6
|
-
*/
|
|
7
|
-
export const runGitCommand = (
|
|
8
|
-
args: string[],
|
|
9
|
-
cwd: string,
|
|
10
|
-
): Promise<{ ok: true; value: string } | { ok: false }> => {
|
|
11
|
-
return new Promise((resolvePromise) => {
|
|
12
|
-
const proc = spawn('git', args, { cwd });
|
|
13
|
-
let stdout = '';
|
|
14
|
-
|
|
15
|
-
proc.stdout.on('data', (data: Buffer) => {
|
|
16
|
-
stdout += data.toString();
|
|
17
|
-
});
|
|
18
|
-
proc.on('close', (code: number | null) => {
|
|
19
|
-
if (code === 0) {
|
|
20
|
-
resolvePromise({ ok: true, value: stdout.trim() });
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
resolvePromise({ ok: false });
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
proc.on('error', () => {
|
|
27
|
-
resolvePromise({ ok: false });
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Detects the git repository name from the current working directory.
|
|
34
|
-
*
|
|
35
|
-
* Uses `git rev-parse --show-toplevel` to find the repository root,
|
|
36
|
-
* then extracts the directory name as the repository name.
|
|
37
|
-
*
|
|
38
|
-
* @param cwd - The current working directory to check for git repository
|
|
39
|
-
* @returns The repository directory name, or `null` if not in a git repository
|
|
40
|
-
*/
|
|
41
|
-
export const detectGitRepoName = async (cwd: string): Promise<string | null> => {
|
|
42
|
-
const result = await runGitCommand([
|
|
43
|
-
'rev-parse', '--show-toplevel',
|
|
44
|
-
], cwd);
|
|
45
|
-
if (!result.ok) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
return basename(result.value);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
|
package/src/utils/input.spec.ts
DELETED
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
-
import * as fs from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { Readable } from 'node:stream';
|
|
6
|
-
|
|
7
|
-
import { resolveInput } from './input.ts';
|
|
8
|
-
|
|
9
|
-
const createMockStdin = (data: string, isTTY = false): NodeJS.ReadableStream => {
|
|
10
|
-
const stream = Readable.from([data]) as NodeJS.ReadableStream & { isTTY?: boolean };
|
|
11
|
-
stream.isTTY = isTTY;
|
|
12
|
-
return stream;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
describe('resolveMemoryContentInput', () => {
|
|
16
|
-
let tempDir: string;
|
|
17
|
-
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
tempDir = await fs.mkdtemp(join(tmpdir(), 'cortex-input-'));
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterEach(async () => {
|
|
23
|
-
if (tempDir) {
|
|
24
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('content flag', () => {
|
|
29
|
-
it('should return content from --content flag', async () => {
|
|
30
|
-
const result = await resolveInput({
|
|
31
|
-
content: 'Hello, world!',
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
expect(result.ok()).toBe(true);
|
|
35
|
-
if (result.ok()) {
|
|
36
|
-
expect(result.value.content).toBe('Hello, world!');
|
|
37
|
-
expect(result.value.source).toBe('flag');
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should return empty string when content is empty', async () => {
|
|
42
|
-
const result = await resolveInput({
|
|
43
|
-
content: '',
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
expect(result.ok()).toBe(true);
|
|
47
|
-
if (result.ok()) {
|
|
48
|
-
expect(result.value.content).toBe('');
|
|
49
|
-
expect(result.value.source).toBe('flag');
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('file input', () => {
|
|
55
|
-
it('should read content from file', async () => {
|
|
56
|
-
const filePath = join(tempDir, 'input.txt');
|
|
57
|
-
await fs.writeFile(filePath, 'File content here');
|
|
58
|
-
|
|
59
|
-
const result = await resolveInput({
|
|
60
|
-
filePath,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
expect(result.ok()).toBe(true);
|
|
64
|
-
if (result.ok()) {
|
|
65
|
-
expect(result.value.content).toBe('File content here');
|
|
66
|
-
expect(result.value.source).toBe('file');
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should return error for non-existent file', async () => {
|
|
71
|
-
const result = await resolveInput({
|
|
72
|
-
filePath: join(tempDir, 'nonexistent.txt'),
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
expect(result.ok()).toBe(false);
|
|
76
|
-
if (!result.ok()) {
|
|
77
|
-
expect(result.error.code).toBe('FILE_READ_FAILED');
|
|
78
|
-
expect(result.error.path).toContain('nonexistent.txt');
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should return error for empty file path', async () => {
|
|
83
|
-
const result = await resolveInput({
|
|
84
|
-
filePath: ' ',
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
expect(result.ok()).toBe(false);
|
|
88
|
-
if (!result.ok()) {
|
|
89
|
-
expect(result.error.code).toBe('INVALID_FILE_PATH');
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should trim file path before reading', async () => {
|
|
94
|
-
const filePath = join(tempDir, 'trimmed.txt');
|
|
95
|
-
await fs.writeFile(filePath, 'Trimmed content');
|
|
96
|
-
|
|
97
|
-
const result = await resolveInput({
|
|
98
|
-
filePath: ` ${filePath} `,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
expect(result.ok()).toBe(true);
|
|
102
|
-
if (result.ok()) {
|
|
103
|
-
expect(result.value.content).toBe('Trimmed content');
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe('stdin input', () => {
|
|
109
|
-
it('should NOT treat an inherited stdin stream as provided unless explicitly requested', async () => {
|
|
110
|
-
const mockStdin = createMockStdin('Should be ignored');
|
|
111
|
-
|
|
112
|
-
const result = await resolveInput({
|
|
113
|
-
stream: mockStdin,
|
|
114
|
-
// stdinRequested intentionally omitted
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
expect(result.ok()).toBe(true);
|
|
118
|
-
if (result.ok()) {
|
|
119
|
-
expect(result.value.content).toBeNull();
|
|
120
|
-
expect(result.value.source).toBe('none');
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should read content from stdin', async () => {
|
|
125
|
-
const mockStdin = createMockStdin('Stdin content');
|
|
126
|
-
|
|
127
|
-
const result = await resolveInput({
|
|
128
|
-
stream: mockStdin,
|
|
129
|
-
stdinRequested: true,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
expect(result.ok()).toBe(true);
|
|
133
|
-
if (result.ok()) {
|
|
134
|
-
expect(result.value.content).toBe('Stdin content');
|
|
135
|
-
expect(result.value.source).toBe('stdin');
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should return null for TTY stdin', async () => {
|
|
140
|
-
const mockStdin = createMockStdin('', true);
|
|
141
|
-
|
|
142
|
-
const result = await resolveInput({
|
|
143
|
-
stream: mockStdin,
|
|
144
|
-
stdinRequested: true,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
expect(result.ok()).toBe(true);
|
|
148
|
-
if (result.ok()) {
|
|
149
|
-
expect(result.value.content).toBeNull();
|
|
150
|
-
expect(result.value.source).toBe('none');
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('should read stdin when stdinRequested is true', async () => {
|
|
155
|
-
const mockStdin = createMockStdin('Requested stdin');
|
|
156
|
-
|
|
157
|
-
const result = await resolveInput({
|
|
158
|
-
stream: mockStdin,
|
|
159
|
-
stdinRequested: true,
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
expect(result.ok()).toBe(true);
|
|
163
|
-
if (result.ok()) {
|
|
164
|
-
expect(result.value.content).toBe('Requested stdin');
|
|
165
|
-
expect(result.value.source).toBe('stdin');
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('should skip stdin when stdinRequested is false', async () => {
|
|
170
|
-
const mockStdin = createMockStdin('Ignored stdin');
|
|
171
|
-
|
|
172
|
-
const result = await resolveInput({
|
|
173
|
-
stream: mockStdin,
|
|
174
|
-
stdinRequested: false,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
expect(result.ok()).toBe(true);
|
|
178
|
-
if (result.ok()) {
|
|
179
|
-
expect(result.value.content).toBeNull();
|
|
180
|
-
expect(result.value.source).toBe('none');
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
describe('multiple sources error', () => {
|
|
186
|
-
it('should error when content and file are both provided', async () => {
|
|
187
|
-
const result = await resolveInput({
|
|
188
|
-
content: 'Content',
|
|
189
|
-
filePath: '/some/file.txt',
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
expect(result.ok()).toBe(false);
|
|
193
|
-
if (!result.ok()) {
|
|
194
|
-
expect(result.error.code).toBe('MULTIPLE_CONTENT_SOURCES');
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('should error when content and stdin are both requested', async () => {
|
|
199
|
-
const result = await resolveInput({
|
|
200
|
-
content: 'Content',
|
|
201
|
-
stream: createMockStdin('Stdin content'),
|
|
202
|
-
stdinRequested: true,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
expect(result.ok()).toBe(false);
|
|
206
|
-
if (!result.ok()) {
|
|
207
|
-
expect(result.error.code).toBe('MULTIPLE_CONTENT_SOURCES');
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('should error when file and stdin are both requested', async () => {
|
|
212
|
-
const result = await resolveInput({
|
|
213
|
-
filePath: '/some/file.txt',
|
|
214
|
-
stream: createMockStdin('Stdin content'),
|
|
215
|
-
stdinRequested: true,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
expect(result.ok()).toBe(false);
|
|
219
|
-
if (!result.ok()) {
|
|
220
|
-
expect(result.error.code).toBe('MULTIPLE_CONTENT_SOURCES');
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('should error when all three sources are provided', async () => {
|
|
225
|
-
const result = await resolveInput({
|
|
226
|
-
content: 'Content',
|
|
227
|
-
filePath: '/some/file.txt',
|
|
228
|
-
stream: createMockStdin('Stdin content'),
|
|
229
|
-
stdinRequested: true,
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
expect(result.ok()).toBe(false);
|
|
233
|
-
if (!result.ok()) {
|
|
234
|
-
expect(result.error.code).toBe('MULTIPLE_CONTENT_SOURCES');
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
describe('missing content error', () => {
|
|
240
|
-
it('should return null content when no source was provided', async () => {
|
|
241
|
-
const result = await resolveInput({
|
|
242
|
-
content: undefined,
|
|
243
|
-
filePath: undefined,
|
|
244
|
-
stream: undefined,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
expect(result.ok()).toBe(true);
|
|
248
|
-
if (result.ok()) {
|
|
249
|
-
expect(result.value.content).toBeNull();
|
|
250
|
-
expect(result.value.source).toBe('none');
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
describe('priority order', () => {
|
|
256
|
-
it('should prefer content flag over file', async () => {
|
|
257
|
-
// This should error because both are provided
|
|
258
|
-
const result = await resolveInput({
|
|
259
|
-
content: 'Flag content',
|
|
260
|
-
filePath: join(tempDir, 'file.txt'),
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
expect(result.ok()).toBe(false);
|
|
264
|
-
if (!result.ok()) {
|
|
265
|
-
expect(result.error.code).toBe('MULTIPLE_CONTENT_SOURCES');
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe('edge cases', () => {
|
|
271
|
-
it('should handle multi-chunk stdin', async () => {
|
|
272
|
-
const mockStdin = Readable.from([
|
|
273
|
-
'chunk1',
|
|
274
|
-
'chunk2',
|
|
275
|
-
'chunk3',
|
|
276
|
-
]) as NodeJS.ReadableStream & { isTTY?: boolean };
|
|
277
|
-
mockStdin.isTTY = false;
|
|
278
|
-
|
|
279
|
-
const result = await resolveInput({
|
|
280
|
-
stream: mockStdin,
|
|
281
|
-
stdinRequested: true,
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
expect(result.ok()).toBe(true);
|
|
285
|
-
if (result.ok()) {
|
|
286
|
-
expect(result.value.content).toBe('chunk1chunk2chunk3');
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('should handle stdin without setEncoding', async () => {
|
|
291
|
-
// Create a minimal stream that doesn't have setEncoding
|
|
292
|
-
const mockStdin = {
|
|
293
|
-
isTTY: false,
|
|
294
|
-
[Symbol.asyncIterator]: async function* () {
|
|
295
|
-
yield 'test data';
|
|
296
|
-
},
|
|
297
|
-
} as unknown as NodeJS.ReadableStream;
|
|
298
|
-
|
|
299
|
-
const result = await resolveInput({
|
|
300
|
-
stream: mockStdin,
|
|
301
|
-
stdinRequested: true,
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
expect(result.ok()).toBe(true);
|
|
305
|
-
if (result.ok()) {
|
|
306
|
-
expect(result.value.content).toBe('test data');
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('should handle empty options', async () => {
|
|
311
|
-
const mockStdin = Readable.from([]) as NodeJS.ReadableStream & { isTTY?: boolean };
|
|
312
|
-
mockStdin.isTTY = true;
|
|
313
|
-
|
|
314
|
-
const result = await resolveInput({
|
|
315
|
-
stream: mockStdin,
|
|
316
|
-
stdinRequested: true,
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
expect(result.ok()).toBe(true);
|
|
320
|
-
if (result.ok()) {
|
|
321
|
-
expect(result.value.content).toBeNull();
|
|
322
|
-
expect(result.value.source).toBe('none');
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
});
|
package/src/utils/input.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI helpers for resolving memory content input.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readFile } from 'node:fs/promises';
|
|
6
|
-
import { err, ok, type Result } from '@yeseh/cortex-core';
|
|
7
|
-
|
|
8
|
-
export type MemoryContentSource = 'flag' | 'file' | 'stdin' | 'none';
|
|
9
|
-
|
|
10
|
-
export type InputSource = {
|
|
11
|
-
content?: string;
|
|
12
|
-
filePath?: string;
|
|
13
|
-
/**
|
|
14
|
-
* stdin stream to read from.
|
|
15
|
-
*
|
|
16
|
-
* NOTE: Passing a stream does not necessarily mean stdin is intended as an input
|
|
17
|
-
* source. Use `stdinRequested: true` when a command semantics include reading from
|
|
18
|
-
* stdin by default (e.g. `memory add`), typically when piping.
|
|
19
|
-
*/
|
|
20
|
-
stream?: NodeJS.ReadableStream;
|
|
21
|
-
/**
|
|
22
|
-
* Explicitly indicates stdin should be considered as an input source.
|
|
23
|
-
*
|
|
24
|
-
* This is `false` by default so that inheriting a non-TTY stdin in test harnesses
|
|
25
|
-
* or subprocess environments does not accidentally count as providing `--stdin`.
|
|
26
|
-
*/
|
|
27
|
-
stdinRequested?: boolean;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export interface InputContent {
|
|
31
|
-
content: string | null;
|
|
32
|
-
source: MemoryContentSource;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type InputErrorCode =
|
|
36
|
-
| 'MULTIPLE_CONTENT_SOURCES'
|
|
37
|
-
| 'FILE_READ_FAILED'
|
|
38
|
-
| 'MISSING_CONTENT'
|
|
39
|
-
| 'INVALID_FILE_PATH';
|
|
40
|
-
|
|
41
|
-
export interface InputError {
|
|
42
|
-
code: InputErrorCode;
|
|
43
|
-
message: string;
|
|
44
|
-
path?: string;
|
|
45
|
-
cause?: unknown;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
type InputResult = Result<InputContent, InputError>;
|
|
49
|
-
type OptionalContentResult = Result<InputContent | null, InputError>;
|
|
50
|
-
|
|
51
|
-
export const readContentFromFile = async (
|
|
52
|
-
filePath: string | undefined,
|
|
53
|
-
): Promise<OptionalContentResult> => {
|
|
54
|
-
if (filePath === undefined) {
|
|
55
|
-
return ok(null);
|
|
56
|
-
}
|
|
57
|
-
const trimmed = filePath.trim();
|
|
58
|
-
if (!trimmed) {
|
|
59
|
-
return err({
|
|
60
|
-
code: 'INVALID_FILE_PATH',
|
|
61
|
-
message: 'File path must be a non-empty string.',
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
const content = await readFile(trimmed, 'utf8');
|
|
66
|
-
return ok({ content, source: 'file' });
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
return err({
|
|
70
|
-
code: 'FILE_READ_FAILED',
|
|
71
|
-
message: `Failed to read content file: ${trimmed}.`,
|
|
72
|
-
path: trimmed,
|
|
73
|
-
cause: error,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export const readContentFromStream = async (
|
|
79
|
-
stream: NodeJS.ReadableStream,
|
|
80
|
-
): Promise<OptionalContentResult> => {
|
|
81
|
-
const isTty = 'isTTY' in stream ? Boolean(stream.isTTY) : false;
|
|
82
|
-
if (isTty) {
|
|
83
|
-
return ok(null);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if ('setEncoding' in stream && typeof stream.setEncoding === 'function') {
|
|
87
|
-
stream.setEncoding('utf8');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let data = '';
|
|
91
|
-
for await (const chunk of stream) {
|
|
92
|
-
data += String(chunk);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return ok({ content: data, source: 'stdin' });
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
/*
|
|
99
|
-
* Main function to resolve content input from various sources.
|
|
100
|
-
*/
|
|
101
|
-
export const resolveInput = async (source: InputSource): Promise<InputResult> => {
|
|
102
|
-
const contentProvided = source.content !== undefined;
|
|
103
|
-
const fileProvided = source.filePath !== undefined && source.filePath.trim() !== '';
|
|
104
|
-
|
|
105
|
-
// stdin must be explicitly requested by the caller. Merely having a stream
|
|
106
|
-
// attached (e.g. inherited stdin in a subprocess) should not count as providing
|
|
107
|
-
// an input source.
|
|
108
|
-
const streamRequested =
|
|
109
|
-
source.stdinRequested === true &&
|
|
110
|
-
source.stream !== null &&
|
|
111
|
-
source.stream !== undefined &&
|
|
112
|
-
!('isTTY' in source.stream && Boolean((source.stream as { isTTY?: boolean }).isTTY));
|
|
113
|
-
|
|
114
|
-
const requestedSources = [
|
|
115
|
-
contentProvided,
|
|
116
|
-
fileProvided,
|
|
117
|
-
streamRequested,
|
|
118
|
-
].filter(Boolean);
|
|
119
|
-
|
|
120
|
-
if (requestedSources.length > 1) {
|
|
121
|
-
return err({
|
|
122
|
-
code: 'MULTIPLE_CONTENT_SOURCES',
|
|
123
|
-
message: 'Provide either --content, --file, or --stdin, not multiple sources.',
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (contentProvided) {
|
|
128
|
-
return ok({ content: source.content ?? '', source: 'flag' });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const fileResult = await readContentFromFile(source.filePath);
|
|
132
|
-
if (!fileResult.ok()) {
|
|
133
|
-
return fileResult;
|
|
134
|
-
}
|
|
135
|
-
if (fileResult.value) {
|
|
136
|
-
return ok(fileResult.value);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (streamRequested) {
|
|
140
|
-
const stdinResult = await readContentFromStream(source.stream!);
|
|
141
|
-
if (!stdinResult.ok()) {
|
|
142
|
-
return stdinResult;
|
|
143
|
-
}
|
|
144
|
-
if (stdinResult.value) {
|
|
145
|
-
return ok(stdinResult.value);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return ok({ content: null, source: 'none' });
|
|
150
|
-
};
|