@teammates/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -22
- package/dist/adapter.d.ts +1 -1
- package/dist/adapter.js +68 -56
- package/dist/adapter.test.js +34 -21
- package/dist/adapters/cli-proxy.d.ts +11 -4
- package/dist/adapters/cli-proxy.js +176 -162
- package/dist/adapters/copilot.d.ts +50 -0
- package/dist/adapters/copilot.js +210 -0
- package/dist/adapters/echo.d.ts +2 -2
- package/dist/adapters/echo.js +2 -1
- package/dist/adapters/echo.test.js +4 -2
- package/dist/cli-utils.d.ts +21 -0
- package/dist/cli-utils.js +74 -0
- package/dist/cli-utils.test.d.ts +1 -0
- package/dist/cli-utils.test.js +179 -0
- package/dist/cli.js +3160 -961
- package/dist/compact.d.ts +39 -0
- package/dist/compact.js +269 -0
- package/dist/compact.test.d.ts +1 -0
- package/dist/compact.test.js +198 -0
- package/dist/console/ansi.d.ts +18 -0
- package/dist/console/ansi.js +20 -0
- package/dist/console/ansi.test.d.ts +1 -0
- package/dist/console/ansi.test.js +50 -0
- package/dist/console/dropdown.d.ts +23 -0
- package/dist/console/dropdown.js +63 -0
- package/dist/console/file-drop.d.ts +59 -0
- package/dist/console/file-drop.js +186 -0
- package/dist/console/file-drop.test.d.ts +1 -0
- package/dist/console/file-drop.test.js +145 -0
- package/dist/console/index.d.ts +22 -0
- package/dist/console/index.js +23 -0
- package/dist/console/interactive-readline.d.ts +65 -0
- package/dist/console/interactive-readline.js +132 -0
- package/dist/console/markdown-table.d.ts +17 -0
- package/dist/console/markdown-table.js +270 -0
- package/dist/console/markdown-table.test.d.ts +1 -0
- package/dist/console/markdown-table.test.js +130 -0
- package/dist/console/mutable-output.d.ts +21 -0
- package/dist/console/mutable-output.js +51 -0
- package/dist/console/paste-handler.d.ts +63 -0
- package/dist/console/paste-handler.js +177 -0
- package/dist/console/prompt-box.d.ts +55 -0
- package/dist/console/prompt-box.js +120 -0
- package/dist/console/prompt-input.d.ts +136 -0
- package/dist/console/prompt-input.js +618 -0
- package/dist/console/startup.d.ts +20 -0
- package/dist/console/startup.js +138 -0
- package/dist/console/startup.test.d.ts +1 -0
- package/dist/console/startup.test.js +41 -0
- package/dist/console/wordwheel.d.ts +75 -0
- package/dist/console/wordwheel.js +123 -0
- package/dist/dropdown.js +4 -21
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/onboard.d.ts +24 -0
- package/dist/onboard.js +174 -11
- package/dist/orchestrator.d.ts +8 -11
- package/dist/orchestrator.js +33 -81
- package/dist/orchestrator.test.js +59 -79
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +56 -12
- package/dist/registry.test.js +57 -13
- package/dist/theme.d.ts +56 -0
- package/dist/theme.js +54 -0
- package/dist/types.d.ts +18 -13
- package/package.json +8 -3
- package/template/CROSS-TEAM.md +2 -2
- package/template/PROTOCOL.md +72 -15
- package/template/README.md +2 -2
- package/template/TEMPLATE.md +118 -15
- package/template/example/SOUL.md +2 -1
- package/template/example/WISDOM.md +9 -0
- package/dist/adapters/codex.d.ts +0 -50
- package/dist/adapters/codex.js +0 -213
- package/template/example/MEMORIES.md +0 -26
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dropdown — renders lines below the readline prompt without disrupting input.
|
|
3
|
+
*
|
|
4
|
+
* Hooks readline's internal _refreshLine to append dropdown content below
|
|
5
|
+
* the prompt, then repositions the cursor back to the input line.
|
|
6
|
+
*
|
|
7
|
+
* Works on both Windows and macOS terminals.
|
|
8
|
+
*/
|
|
9
|
+
import { esc, stripAnsi, truncateAnsi } from "@teammates/consolonia";
|
|
10
|
+
import { cursorToCol } from "./ansi.js";
|
|
11
|
+
export class Dropdown {
|
|
12
|
+
rl;
|
|
13
|
+
lines = [];
|
|
14
|
+
out = process.stdout;
|
|
15
|
+
refreshing = false;
|
|
16
|
+
constructor(rl) {
|
|
17
|
+
this.rl = rl;
|
|
18
|
+
this.installHook();
|
|
19
|
+
}
|
|
20
|
+
/** Number of lines currently rendered below the prompt. */
|
|
21
|
+
get rendered() {
|
|
22
|
+
return this.lines.length;
|
|
23
|
+
}
|
|
24
|
+
/** Set dropdown content and trigger a re-render. */
|
|
25
|
+
render(newLines) {
|
|
26
|
+
this.lines = newLines;
|
|
27
|
+
this.rl._refreshLine();
|
|
28
|
+
}
|
|
29
|
+
/** Clear dropdown content. Next _refreshLine won't append anything. */
|
|
30
|
+
clear() {
|
|
31
|
+
this.lines = [];
|
|
32
|
+
}
|
|
33
|
+
installHook() {
|
|
34
|
+
const origRefresh = this.rl._refreshLine.bind(this.rl);
|
|
35
|
+
this.rl._refreshLine = () => {
|
|
36
|
+
if (this.refreshing) {
|
|
37
|
+
origRefresh();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.refreshing = true;
|
|
41
|
+
// Run original: clears below, writes prompt, positions cursor
|
|
42
|
+
origRefresh();
|
|
43
|
+
// Append dropdown lines below the prompt
|
|
44
|
+
if (this.lines.length > 0) {
|
|
45
|
+
const cols = this.out.columns || 120;
|
|
46
|
+
let buf = "";
|
|
47
|
+
for (const line of this.lines) {
|
|
48
|
+
buf += `\n${truncateAnsi(line, cols - 1)}`;
|
|
49
|
+
}
|
|
50
|
+
this.out.write(buf);
|
|
51
|
+
// Move cursor back to the prompt line
|
|
52
|
+
const n = this.lines.length;
|
|
53
|
+
this.out.write(esc.moveUp(n));
|
|
54
|
+
// Restore cursor column position
|
|
55
|
+
const promptText = this.rl._prompt ?? "";
|
|
56
|
+
const promptLen = stripAnsi(promptText).length;
|
|
57
|
+
const cursor = this.rl.cursor ?? 0;
|
|
58
|
+
this.out.write(cursorToCol(promptLen + cursor + 1));
|
|
59
|
+
}
|
|
60
|
+
this.refreshing = false;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileDropHandler — detects dragged/dropped files in terminal input.
|
|
3
|
+
*
|
|
4
|
+
* When a file is dragged into a terminal, the OS pastes the file path as text.
|
|
5
|
+
* This handler detects path-like input and converts it to file attachment tags.
|
|
6
|
+
*
|
|
7
|
+
* Supports images (png, jpg, gif, webp, svg) and general files.
|
|
8
|
+
* Works on both Windows (C:\...) and macOS/Linux (/...) terminals.
|
|
9
|
+
*/
|
|
10
|
+
export interface FileAttachment {
|
|
11
|
+
/** Unique ID for this attachment. */
|
|
12
|
+
id: number;
|
|
13
|
+
/** Absolute path to the file. */
|
|
14
|
+
path: string;
|
|
15
|
+
/** File basename. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Whether this is an image file. */
|
|
18
|
+
isImage: boolean;
|
|
19
|
+
/** File size in bytes. */
|
|
20
|
+
size: number;
|
|
21
|
+
}
|
|
22
|
+
export declare class FileDropHandler {
|
|
23
|
+
private nextId;
|
|
24
|
+
private attachments;
|
|
25
|
+
/** All current attachments. */
|
|
26
|
+
getAll(): FileAttachment[];
|
|
27
|
+
/** Get a specific attachment by ID. */
|
|
28
|
+
get(id: number): FileAttachment | undefined;
|
|
29
|
+
/** Remove an attachment by ID. */
|
|
30
|
+
remove(id: number): boolean;
|
|
31
|
+
/** Clear all attachments. */
|
|
32
|
+
clear(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Check if a string looks like a file path that was drag-and-dropped.
|
|
35
|
+
* Returns the cleaned path if it's a valid file, null otherwise.
|
|
36
|
+
*/
|
|
37
|
+
detectFilePath(input: string): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Try to convert input text into file attachments.
|
|
40
|
+
* Returns the modified input (with file tags inserted) and whether any files were detected.
|
|
41
|
+
*/
|
|
42
|
+
processInput(input: string): {
|
|
43
|
+
text: string;
|
|
44
|
+
filesDetected: boolean;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Expand file tags in a string, replacing [Image #N] / [File #N] with
|
|
48
|
+
* the actual file path for downstream processing.
|
|
49
|
+
*/
|
|
50
|
+
expandTags(input: string): {
|
|
51
|
+
text: string;
|
|
52
|
+
attachments: FileAttachment[];
|
|
53
|
+
};
|
|
54
|
+
private addFile;
|
|
55
|
+
/** Format an attachment as an inline tag. */
|
|
56
|
+
formatTag(attachment: FileAttachment): string;
|
|
57
|
+
/** Format a human-readable summary of an attachment. */
|
|
58
|
+
formatSummary(attachment: FileAttachment): string;
|
|
59
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileDropHandler — detects dragged/dropped files in terminal input.
|
|
3
|
+
*
|
|
4
|
+
* When a file is dragged into a terminal, the OS pastes the file path as text.
|
|
5
|
+
* This handler detects path-like input and converts it to file attachment tags.
|
|
6
|
+
*
|
|
7
|
+
* Supports images (png, jpg, gif, webp, svg) and general files.
|
|
8
|
+
* Works on both Windows (C:\...) and macOS/Linux (/...) terminals.
|
|
9
|
+
*/
|
|
10
|
+
import { statSync } from "node:fs";
|
|
11
|
+
import { basename, extname, resolve } from "node:path";
|
|
12
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
13
|
+
".png",
|
|
14
|
+
".jpg",
|
|
15
|
+
".jpeg",
|
|
16
|
+
".gif",
|
|
17
|
+
".webp",
|
|
18
|
+
".svg",
|
|
19
|
+
".bmp",
|
|
20
|
+
".ico",
|
|
21
|
+
".tiff",
|
|
22
|
+
".tif",
|
|
23
|
+
]);
|
|
24
|
+
const _KNOWN_EXTENSIONS = new Set([
|
|
25
|
+
...IMAGE_EXTENSIONS,
|
|
26
|
+
".pdf",
|
|
27
|
+
".txt",
|
|
28
|
+
".md",
|
|
29
|
+
".json",
|
|
30
|
+
".csv",
|
|
31
|
+
".xml",
|
|
32
|
+
".yaml",
|
|
33
|
+
".yml",
|
|
34
|
+
".ts",
|
|
35
|
+
".js",
|
|
36
|
+
".py",
|
|
37
|
+
".go",
|
|
38
|
+
".rs",
|
|
39
|
+
".java",
|
|
40
|
+
".c",
|
|
41
|
+
".cpp",
|
|
42
|
+
".h",
|
|
43
|
+
".html",
|
|
44
|
+
".css",
|
|
45
|
+
".sql",
|
|
46
|
+
".sh",
|
|
47
|
+
".bat",
|
|
48
|
+
".ps1",
|
|
49
|
+
".log",
|
|
50
|
+
".env",
|
|
51
|
+
".toml",
|
|
52
|
+
".ini",
|
|
53
|
+
".cfg",
|
|
54
|
+
]);
|
|
55
|
+
export class FileDropHandler {
|
|
56
|
+
nextId = 1;
|
|
57
|
+
attachments = new Map();
|
|
58
|
+
/** All current attachments. */
|
|
59
|
+
getAll() {
|
|
60
|
+
return [...this.attachments.values()];
|
|
61
|
+
}
|
|
62
|
+
/** Get a specific attachment by ID. */
|
|
63
|
+
get(id) {
|
|
64
|
+
return this.attachments.get(id);
|
|
65
|
+
}
|
|
66
|
+
/** Remove an attachment by ID. */
|
|
67
|
+
remove(id) {
|
|
68
|
+
return this.attachments.delete(id);
|
|
69
|
+
}
|
|
70
|
+
/** Clear all attachments. */
|
|
71
|
+
clear() {
|
|
72
|
+
this.attachments.clear();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if a string looks like a file path that was drag-and-dropped.
|
|
76
|
+
* Returns the cleaned path if it's a valid file, null otherwise.
|
|
77
|
+
*/
|
|
78
|
+
detectFilePath(input) {
|
|
79
|
+
let candidate = input.trim();
|
|
80
|
+
// Strip surrounding quotes (Windows often wraps paths in quotes)
|
|
81
|
+
if ((candidate.startsWith('"') && candidate.endsWith('"')) ||
|
|
82
|
+
(candidate.startsWith("'") && candidate.endsWith("'"))) {
|
|
83
|
+
candidate = candidate.slice(1, -1);
|
|
84
|
+
}
|
|
85
|
+
// Must look like an absolute path
|
|
86
|
+
const isAbsoluteWindows = /^[A-Za-z]:\\/.test(candidate);
|
|
87
|
+
const isAbsoluteUnix = candidate.startsWith("/");
|
|
88
|
+
if (!isAbsoluteWindows && !isAbsoluteUnix)
|
|
89
|
+
return null;
|
|
90
|
+
// Should not contain newlines or control characters
|
|
91
|
+
if (/[\n\r\t]/.test(candidate))
|
|
92
|
+
return null;
|
|
93
|
+
// Resolve and check existence
|
|
94
|
+
const resolved = resolve(candidate);
|
|
95
|
+
try {
|
|
96
|
+
const st = statSync(resolved);
|
|
97
|
+
if (st.isFile())
|
|
98
|
+
return resolved;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// File doesn't exist
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Try to convert input text into file attachments.
|
|
107
|
+
* Returns the modified input (with file tags inserted) and whether any files were detected.
|
|
108
|
+
*/
|
|
109
|
+
processInput(input) {
|
|
110
|
+
// Check if the entire input is a single file path
|
|
111
|
+
const singlePath = this.detectFilePath(input);
|
|
112
|
+
if (singlePath) {
|
|
113
|
+
const attachment = this.addFile(singlePath);
|
|
114
|
+
const tag = this.formatTag(attachment);
|
|
115
|
+
return { text: `${tag} `, filesDetected: true };
|
|
116
|
+
}
|
|
117
|
+
// Check for file paths mixed with text — look for path-like tokens
|
|
118
|
+
let modified = input;
|
|
119
|
+
let detected = false;
|
|
120
|
+
// Match Windows paths: C:\... or "C:\..."
|
|
121
|
+
// Match Unix paths: /home/... or "/home/..."
|
|
122
|
+
const pathPattern = /(?:"([A-Za-z]:\\[^"]+)"|'([A-Za-z]:\\[^']+)'|([A-Za-z]:\\[^\s]+)|"(\/[^"]+)"|'(\/[^']+)'|(\/[^\s]+\.\w{1,5}))/g;
|
|
123
|
+
let match;
|
|
124
|
+
const replacements = [];
|
|
125
|
+
while ((match = pathPattern.exec(input)) !== null) {
|
|
126
|
+
const rawMatch = match[0];
|
|
127
|
+
const path = match[1] ?? match[2] ?? match[3] ?? match[4] ?? match[5] ?? match[6];
|
|
128
|
+
const resolved = this.detectFilePath(path);
|
|
129
|
+
if (resolved) {
|
|
130
|
+
const attachment = this.addFile(resolved);
|
|
131
|
+
replacements.push({ from: rawMatch, to: this.formatTag(attachment) });
|
|
132
|
+
detected = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const { from, to } of replacements) {
|
|
136
|
+
modified = modified.replace(from, to);
|
|
137
|
+
}
|
|
138
|
+
return { text: modified, filesDetected: detected };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Expand file tags in a string, replacing [Image #N] / [File #N] with
|
|
142
|
+
* the actual file path for downstream processing.
|
|
143
|
+
*/
|
|
144
|
+
expandTags(input) {
|
|
145
|
+
const used = [];
|
|
146
|
+
const expanded = input.replace(/\[(Image|File) #(\d+)\]/g, (_match, _type, num) => {
|
|
147
|
+
const id = parseInt(num, 10);
|
|
148
|
+
const attachment = this.attachments.get(id);
|
|
149
|
+
if (attachment) {
|
|
150
|
+
used.push(attachment);
|
|
151
|
+
return attachment.path;
|
|
152
|
+
}
|
|
153
|
+
return _match;
|
|
154
|
+
});
|
|
155
|
+
return { text: expanded, attachments: used };
|
|
156
|
+
}
|
|
157
|
+
addFile(filePath) {
|
|
158
|
+
// Check if this file is already attached
|
|
159
|
+
for (const existing of this.attachments.values()) {
|
|
160
|
+
if (existing.path === filePath)
|
|
161
|
+
return existing;
|
|
162
|
+
}
|
|
163
|
+
const ext = extname(filePath).toLowerCase();
|
|
164
|
+
const st = statSync(filePath);
|
|
165
|
+
const attachment = {
|
|
166
|
+
id: this.nextId++,
|
|
167
|
+
path: filePath,
|
|
168
|
+
name: basename(filePath),
|
|
169
|
+
isImage: IMAGE_EXTENSIONS.has(ext),
|
|
170
|
+
size: st.size,
|
|
171
|
+
};
|
|
172
|
+
this.attachments.set(attachment.id, attachment);
|
|
173
|
+
return attachment;
|
|
174
|
+
}
|
|
175
|
+
/** Format an attachment as an inline tag. */
|
|
176
|
+
formatTag(attachment) {
|
|
177
|
+
const type = attachment.isImage ? "Image" : "File";
|
|
178
|
+
return `[${type} #${attachment.id}]`;
|
|
179
|
+
}
|
|
180
|
+
/** Format a human-readable summary of an attachment. */
|
|
181
|
+
formatSummary(attachment) {
|
|
182
|
+
const type = attachment.isImage ? "Image" : "File";
|
|
183
|
+
const sizeKB = (attachment.size / 1024).toFixed(1);
|
|
184
|
+
return `${type} #${attachment.id}: ${attachment.name} (${sizeKB}KB)`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { FileDropHandler } from "./file-drop.js";
|
|
6
|
+
let handler;
|
|
7
|
+
let testDir;
|
|
8
|
+
let testFile;
|
|
9
|
+
let testImage;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
handler = new FileDropHandler();
|
|
12
|
+
testDir = join(tmpdir(), `file-drop-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
13
|
+
mkdirSync(testDir, { recursive: true });
|
|
14
|
+
testFile = join(testDir, "example.txt");
|
|
15
|
+
testImage = join(testDir, "photo.png");
|
|
16
|
+
writeFileSync(testFile, "hello world");
|
|
17
|
+
writeFileSync(testImage, "fake png data");
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
describe("detectFilePath", () => {
|
|
23
|
+
it("detects a valid absolute path", () => {
|
|
24
|
+
const result = handler.detectFilePath(testFile);
|
|
25
|
+
expect(result).toBe(testFile);
|
|
26
|
+
});
|
|
27
|
+
it("detects a quoted absolute path", () => {
|
|
28
|
+
const result = handler.detectFilePath(`"${testFile}"`);
|
|
29
|
+
expect(result).toBe(testFile);
|
|
30
|
+
});
|
|
31
|
+
it("detects a single-quoted absolute path", () => {
|
|
32
|
+
const result = handler.detectFilePath(`'${testFile}'`);
|
|
33
|
+
expect(result).toBe(testFile);
|
|
34
|
+
});
|
|
35
|
+
it("returns null for relative paths", () => {
|
|
36
|
+
const result = handler.detectFilePath("relative/path.txt");
|
|
37
|
+
expect(result).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
it("returns null for URLs", () => {
|
|
40
|
+
expect(handler.detectFilePath("https://example.com/file.txt")).toBeNull();
|
|
41
|
+
expect(handler.detectFilePath("http://localhost:3000")).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
it("returns null for empty strings", () => {
|
|
44
|
+
expect(handler.detectFilePath("")).toBeNull();
|
|
45
|
+
expect(handler.detectFilePath(" ")).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
it("returns null for paths with newlines", () => {
|
|
48
|
+
expect(handler.detectFilePath("/some/path\nwith\nnewlines")).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
it("returns null for non-existent files", () => {
|
|
51
|
+
const result = handler.detectFilePath(join(testDir, "nonexistent.txt"));
|
|
52
|
+
expect(result).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("processInput", () => {
|
|
56
|
+
it("detects a single file path", () => {
|
|
57
|
+
const result = handler.processInput(testFile);
|
|
58
|
+
expect(result.filesDetected).toBe(true);
|
|
59
|
+
expect(result.text).toContain("[File #1]");
|
|
60
|
+
});
|
|
61
|
+
it("detects an image file", () => {
|
|
62
|
+
const result = handler.processInput(testImage);
|
|
63
|
+
expect(result.filesDetected).toBe(true);
|
|
64
|
+
expect(result.text).toContain("[Image #1]");
|
|
65
|
+
});
|
|
66
|
+
it("passes through non-file text", () => {
|
|
67
|
+
const result = handler.processInput("just some regular text");
|
|
68
|
+
expect(result.filesDetected).toBe(false);
|
|
69
|
+
expect(result.text).toBe("just some regular text");
|
|
70
|
+
});
|
|
71
|
+
it("does not re-add the same file twice", () => {
|
|
72
|
+
handler.processInput(testFile);
|
|
73
|
+
handler.processInput(testFile);
|
|
74
|
+
const all = handler.getAll();
|
|
75
|
+
expect(all).toHaveLength(1);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe("expandTags", () => {
|
|
79
|
+
it("expands [File #N] tags to paths", () => {
|
|
80
|
+
handler.processInput(testFile);
|
|
81
|
+
const { text, attachments } = handler.expandTags("Please review [File #1]");
|
|
82
|
+
expect(text).toContain(testFile);
|
|
83
|
+
expect(text).not.toContain("[File #1]");
|
|
84
|
+
expect(attachments).toHaveLength(1);
|
|
85
|
+
expect(attachments[0].path).toBe(testFile);
|
|
86
|
+
});
|
|
87
|
+
it("expands [Image #N] tags to paths", () => {
|
|
88
|
+
handler.processInput(testImage);
|
|
89
|
+
const { text, attachments } = handler.expandTags("Look at [Image #1]");
|
|
90
|
+
expect(text).toContain(testImage);
|
|
91
|
+
expect(text).not.toContain("[Image #1]");
|
|
92
|
+
expect(attachments).toHaveLength(1);
|
|
93
|
+
expect(attachments[0].isImage).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
it("leaves unknown tags untouched", () => {
|
|
96
|
+
const { text } = handler.expandTags("Check [File #999]");
|
|
97
|
+
expect(text).toBe("Check [File #999]");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe("formatTag", () => {
|
|
101
|
+
it("formats a file attachment as [File #N]", () => {
|
|
102
|
+
handler.processInput(testFile);
|
|
103
|
+
const attachment = handler.get(1);
|
|
104
|
+
expect(handler.formatTag(attachment)).toBe("[File #1]");
|
|
105
|
+
});
|
|
106
|
+
it("formats an image attachment as [Image #N]", () => {
|
|
107
|
+
handler.processInput(testImage);
|
|
108
|
+
const attachment = handler.get(1);
|
|
109
|
+
expect(handler.formatTag(attachment)).toBe("[Image #1]");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe("formatSummary", () => {
|
|
113
|
+
it("produces a human-readable summary for a file", () => {
|
|
114
|
+
handler.processInput(testFile);
|
|
115
|
+
const attachment = handler.get(1);
|
|
116
|
+
const summary = handler.formatSummary(attachment);
|
|
117
|
+
expect(summary).toContain("File #1");
|
|
118
|
+
expect(summary).toContain("example.txt");
|
|
119
|
+
expect(summary).toContain("KB");
|
|
120
|
+
});
|
|
121
|
+
it("produces a human-readable summary for an image", () => {
|
|
122
|
+
handler.processInput(testImage);
|
|
123
|
+
const attachment = handler.get(1);
|
|
124
|
+
const summary = handler.formatSummary(attachment);
|
|
125
|
+
expect(summary).toContain("Image #1");
|
|
126
|
+
expect(summary).toContain("photo.png");
|
|
127
|
+
expect(summary).toContain("KB");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe("attachment management", () => {
|
|
131
|
+
it("get() returns undefined for missing IDs", () => {
|
|
132
|
+
expect(handler.get(999)).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
it("remove() deletes an attachment", () => {
|
|
135
|
+
handler.processInput(testFile);
|
|
136
|
+
expect(handler.remove(1)).toBe(true);
|
|
137
|
+
expect(handler.get(1)).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
it("clear() removes all attachments", () => {
|
|
140
|
+
handler.processInput(testFile);
|
|
141
|
+
handler.processInput(testImage);
|
|
142
|
+
handler.clear();
|
|
143
|
+
expect(handler.getAll()).toHaveLength(0);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @teammates/console — reusable interactive console components for Node.js CLIs.
|
|
3
|
+
*
|
|
4
|
+
* Components:
|
|
5
|
+
* - InteractiveReadline: batteries-included REPL with paste handling + autocomplete
|
|
6
|
+
* - MutableOutput: writable stream that can be muted/unmuted
|
|
7
|
+
* - PasteHandler: detects and manages pasted text
|
|
8
|
+
* - Dropdown: renders content below the readline prompt
|
|
9
|
+
* - Wordwheel: autocomplete engine with keyboard navigation
|
|
10
|
+
* - ANSI helpers: re-exported from @teammates/consolonia + CLI-specific extras
|
|
11
|
+
*/
|
|
12
|
+
export { stripAnsi, truncateAnsi, visibleLength } from "@teammates/consolonia";
|
|
13
|
+
export { cr, cursorDown, cursorHome, cursorToCol, cursorUp, eraseDown, eraseLine, eraseScreen, eraseToEnd, } from "./ansi.js";
|
|
14
|
+
export { Dropdown } from "./dropdown.js";
|
|
15
|
+
export { type FileAttachment, FileDropHandler } from "./file-drop.js";
|
|
16
|
+
export { InteractiveReadline, type InteractiveReadlineOptions, } from "./interactive-readline.js";
|
|
17
|
+
export { renderMarkdownTables } from "./markdown-table.js";
|
|
18
|
+
export { MutableOutput } from "./mutable-output.js";
|
|
19
|
+
export { PasteHandler, type PasteHandlerOptions, type PasteResult, } from "./paste-handler.js";
|
|
20
|
+
export { PromptBox, type PromptBoxOptions } from "./prompt-box.js";
|
|
21
|
+
export { PromptInput, type PromptInputOptions } from "./prompt-input.js";
|
|
22
|
+
export { Wordwheel, type WordwheelItem, type WordwheelOptions, } from "./wordwheel.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @teammates/console — reusable interactive console components for Node.js CLIs.
|
|
3
|
+
*
|
|
4
|
+
* Components:
|
|
5
|
+
* - InteractiveReadline: batteries-included REPL with paste handling + autocomplete
|
|
6
|
+
* - MutableOutput: writable stream that can be muted/unmuted
|
|
7
|
+
* - PasteHandler: detects and manages pasted text
|
|
8
|
+
* - Dropdown: renders content below the readline prompt
|
|
9
|
+
* - Wordwheel: autocomplete engine with keyboard navigation
|
|
10
|
+
* - ANSI helpers: re-exported from @teammates/consolonia + CLI-specific extras
|
|
11
|
+
*/
|
|
12
|
+
// ANSI helpers — consolonia re-exports + CLI-specific extras
|
|
13
|
+
export { stripAnsi, truncateAnsi, visibleLength } from "@teammates/consolonia";
|
|
14
|
+
export { cr, cursorDown, cursorHome, cursorToCol, cursorUp, eraseDown, eraseLine, eraseScreen, eraseToEnd, } from "./ansi.js";
|
|
15
|
+
export { Dropdown } from "./dropdown.js";
|
|
16
|
+
export { FileDropHandler } from "./file-drop.js";
|
|
17
|
+
export { InteractiveReadline, } from "./interactive-readline.js";
|
|
18
|
+
export { renderMarkdownTables } from "./markdown-table.js";
|
|
19
|
+
export { MutableOutput } from "./mutable-output.js";
|
|
20
|
+
export { PasteHandler, } from "./paste-handler.js";
|
|
21
|
+
export { PromptBox } from "./prompt-box.js";
|
|
22
|
+
export { PromptInput } from "./prompt-input.js";
|
|
23
|
+
export { Wordwheel, } from "./wordwheel.js";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InteractiveReadline — a batteries-included readline wrapper for CLI REPLs.
|
|
3
|
+
*
|
|
4
|
+
* Composes MutableOutput, PasteHandler, Dropdown, and Wordwheel into a
|
|
5
|
+
* single cohesive readline experience with:
|
|
6
|
+
*
|
|
7
|
+
* - Paste detection (multi-line collapse, long single-line truncation)
|
|
8
|
+
* - Autocomplete dropdown with keyboard navigation
|
|
9
|
+
* - Mutable output for suppressing echo
|
|
10
|
+
* - Cross-platform (Windows + macOS)
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const irl = new InteractiveReadline({
|
|
14
|
+
* prompt: "my-app> ",
|
|
15
|
+
* getItems: (line, cursor) => [...],
|
|
16
|
+
* onLine: async (input) => { ... },
|
|
17
|
+
* });
|
|
18
|
+
* await irl.start();
|
|
19
|
+
*/
|
|
20
|
+
import { type Interface as ReadlineInterface } from "node:readline";
|
|
21
|
+
import { Dropdown } from "./dropdown.js";
|
|
22
|
+
import type { FileAttachment } from "./file-drop.js";
|
|
23
|
+
import { MutableOutput } from "./mutable-output.js";
|
|
24
|
+
import { PasteHandler } from "./paste-handler.js";
|
|
25
|
+
import { Wordwheel, type WordwheelItem } from "./wordwheel.js";
|
|
26
|
+
export interface InteractiveReadlineOptions {
|
|
27
|
+
/** Prompt string (may include ANSI color codes). */
|
|
28
|
+
prompt: string;
|
|
29
|
+
/** Return completion items for the current line/cursor. */
|
|
30
|
+
getItems?: (line: string, cursor: number) => WordwheelItem[];
|
|
31
|
+
/** Called when a line is ready to dispatch. */
|
|
32
|
+
onLine: (input: string, attachments?: FileAttachment[]) => Promise<void> | void;
|
|
33
|
+
/** Called when readline closes (Ctrl+D). */
|
|
34
|
+
onClose?: () => void;
|
|
35
|
+
/** Format a highlighted wordwheel item. */
|
|
36
|
+
formatHighlighted?: (item: WordwheelItem) => string;
|
|
37
|
+
/** Format a normal wordwheel item. */
|
|
38
|
+
formatNormal?: (item: WordwheelItem) => string;
|
|
39
|
+
/** Paste debounce timeout in ms (default: 30). */
|
|
40
|
+
pasteDebounceMs?: number;
|
|
41
|
+
/** Minimum chunk size to consider a single-line paste (default: 100). */
|
|
42
|
+
longPasteThreshold?: number;
|
|
43
|
+
}
|
|
44
|
+
export declare class InteractiveReadline {
|
|
45
|
+
readonly rl: ReadlineInterface;
|
|
46
|
+
readonly output: MutableOutput;
|
|
47
|
+
readonly dropdown: Dropdown;
|
|
48
|
+
readonly wordwheel: Wordwheel;
|
|
49
|
+
readonly pasteHandler: PasteHandler;
|
|
50
|
+
private dispatching;
|
|
51
|
+
private prompt;
|
|
52
|
+
private onLine;
|
|
53
|
+
constructor(options: InteractiveReadlineOptions);
|
|
54
|
+
/** Start the REPL — shows the prompt. */
|
|
55
|
+
start(): void;
|
|
56
|
+
/** Clear the terminal and re-show the prompt. */
|
|
57
|
+
clearScreen(): void;
|
|
58
|
+
/** Reset all state (paste buffers, wordwheel, etc.). */
|
|
59
|
+
reset(): void;
|
|
60
|
+
/** Access the current line text. */
|
|
61
|
+
get line(): string;
|
|
62
|
+
/** Set the current line text and cursor. */
|
|
63
|
+
setLine(text: string): void;
|
|
64
|
+
private installKeyHandler;
|
|
65
|
+
}
|