caroushell 0.1.0 → 0.1.1
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 +8 -8
- package/dist/app.js +37 -16
- package/dist/carousel.js +38 -0
- package/dist/keyboard.js +1 -0
- package/dist/spawner.js +65 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# Caroushell
|
|
2
2
|
|
|
3
3
|
Caroushell is an interactive terminal carousel that suggests commands from your
|
|
4
|
-
history,
|
|
5
|
-
command without leaving the keyboard.
|
|
4
|
+
history, and AI suggestions as you type.
|
|
6
5
|
|
|
7
6
|
## Features
|
|
8
7
|
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
-
|
|
8
|
+
- The top panel of the carousel shows history
|
|
9
|
+
- The bottom panel of the carousel shows AI-generated command suggestions.
|
|
10
|
+
- Go up and down the carousel with arrow keys.
|
|
11
|
+
- Press `Enter` to run the highlighted command.
|
|
12
12
|
- Logs activity under `~/.caroushell/logs` for easy troubleshooting.
|
|
13
13
|
- Extensible config file (`~/.caroushell/config.json`) so you can point the CLI
|
|
14
14
|
at different API keys or settings.
|
|
@@ -58,9 +58,9 @@ need to debug AI suggestions or the terminal renderer. Configuration lives at
|
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
npm install
|
|
61
|
-
npm run dev
|
|
62
|
-
npm run build
|
|
63
|
-
npm run test:generate
|
|
61
|
+
npm run dev
|
|
62
|
+
npm run build
|
|
63
|
+
npm run test:generate # tests ai text generation
|
|
64
64
|
npm publish --dry-run # verify package contents before publishing
|
|
65
65
|
```
|
|
66
66
|
|
package/dist/app.js
CHANGED
|
@@ -6,7 +6,7 @@ const keyboard_1 = require("./keyboard");
|
|
|
6
6
|
const carousel_1 = require("./carousel");
|
|
7
7
|
const history_suggester_1 = require("./history-suggester");
|
|
8
8
|
const ai_suggester_1 = require("./ai-suggester");
|
|
9
|
-
const
|
|
9
|
+
const spawner_1 = require("./spawner");
|
|
10
10
|
function debounce(fn, ms) {
|
|
11
11
|
// Debounce function to limit the rate at which a function can fire
|
|
12
12
|
let t = null;
|
|
@@ -41,10 +41,30 @@ class App {
|
|
|
41
41
|
await this.carousel.updateSuggestions();
|
|
42
42
|
}, 300);
|
|
43
43
|
const handlers = {
|
|
44
|
-
"ctrl-c": () =>
|
|
44
|
+
"ctrl-c": () => {
|
|
45
|
+
if (this.carousel.isPromptRowSelected() &&
|
|
46
|
+
!this.carousel.hasInput()) {
|
|
47
|
+
this.exit();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.carousel.clearInput();
|
|
51
|
+
this.render();
|
|
52
|
+
updateSuggestions();
|
|
53
|
+
},
|
|
45
54
|
"ctrl-d": () => {
|
|
46
|
-
if (this.carousel.
|
|
55
|
+
if (this.carousel.isPromptRowSelected() &&
|
|
56
|
+
!this.carousel.hasInput()) {
|
|
47
57
|
this.exit();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.carousel.deleteAtCursor();
|
|
61
|
+
this.render();
|
|
62
|
+
updateSuggestions();
|
|
63
|
+
},
|
|
64
|
+
"ctrl-u": () => {
|
|
65
|
+
this.carousel.deleteToLineStart();
|
|
66
|
+
this.render();
|
|
67
|
+
updateSuggestions();
|
|
48
68
|
},
|
|
49
69
|
backspace: () => {
|
|
50
70
|
this.carousel.deleteBeforeCursor();
|
|
@@ -83,9 +103,19 @@ class App {
|
|
|
83
103
|
this.carousel.moveCursorRight();
|
|
84
104
|
this.render();
|
|
85
105
|
},
|
|
86
|
-
home: () => {
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
home: () => {
|
|
107
|
+
this.carousel.moveCursorHome();
|
|
108
|
+
this.render();
|
|
109
|
+
},
|
|
110
|
+
end: () => {
|
|
111
|
+
this.carousel.moveCursorEnd();
|
|
112
|
+
this.render();
|
|
113
|
+
},
|
|
114
|
+
delete: () => {
|
|
115
|
+
this.carousel.deleteAtCursor();
|
|
116
|
+
this.render();
|
|
117
|
+
updateSuggestions();
|
|
118
|
+
},
|
|
89
119
|
escape: () => { },
|
|
90
120
|
};
|
|
91
121
|
this.keyboard.on("key", async (evt) => {
|
|
@@ -117,16 +147,7 @@ class App {
|
|
|
117
147
|
// Ensure command output starts on the next line
|
|
118
148
|
this.terminal.write("\n");
|
|
119
149
|
await this.history.add(cmd);
|
|
120
|
-
|
|
121
|
-
const isWin = process.platform === "win32";
|
|
122
|
-
const proc = (0, child_process_1.spawn)(isWin ? "cmd.exe" : "/bin/bash", [isWin ? "/c" : "-lc", cmd], {
|
|
123
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
124
|
-
});
|
|
125
|
-
await new Promise((resolve) => {
|
|
126
|
-
proc.stdout.on("data", (d) => process.stdout.write(d));
|
|
127
|
-
proc.stderr.on("data", (d) => process.stderr.write(d));
|
|
128
|
-
proc.on("close", () => resolve());
|
|
129
|
-
});
|
|
150
|
+
await (0, spawner_1.runUserCommand)(cmd);
|
|
130
151
|
// After arbitrary output, reset render block tracking
|
|
131
152
|
this.terminal.resetBlockTracking();
|
|
132
153
|
}
|
package/dist/carousel.js
CHANGED
|
@@ -80,6 +80,10 @@ class Carousel {
|
|
|
80
80
|
prefix = "> ";
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
if (rowIndex !== 0 && !rowStr) {
|
|
84
|
+
// The edge of the top or bottom panel
|
|
85
|
+
prefix = "---";
|
|
86
|
+
}
|
|
83
87
|
return `${color}${prefix}${rowStr}${reset}`;
|
|
84
88
|
}
|
|
85
89
|
getCurrentRow() {
|
|
@@ -129,6 +133,40 @@ class Carousel {
|
|
|
129
133
|
return;
|
|
130
134
|
this.inputCursor += 1;
|
|
131
135
|
}
|
|
136
|
+
moveCursorHome() {
|
|
137
|
+
this.adoptSelectionIntoInput();
|
|
138
|
+
this.inputCursor = 0;
|
|
139
|
+
}
|
|
140
|
+
moveCursorEnd() {
|
|
141
|
+
this.adoptSelectionIntoInput();
|
|
142
|
+
this.inputCursor = this.inputBuffer.length;
|
|
143
|
+
}
|
|
144
|
+
deleteAtCursor() {
|
|
145
|
+
this.adoptSelectionIntoInput();
|
|
146
|
+
if (this.inputCursor >= this.inputBuffer.length)
|
|
147
|
+
return;
|
|
148
|
+
const before = this.inputBuffer.slice(0, this.inputCursor);
|
|
149
|
+
const after = this.inputBuffer.slice(this.inputCursor + 1);
|
|
150
|
+
this.inputBuffer = `${before}${after}`;
|
|
151
|
+
}
|
|
152
|
+
deleteToLineStart() {
|
|
153
|
+
this.adoptSelectionIntoInput();
|
|
154
|
+
if (this.inputCursor === 0)
|
|
155
|
+
return;
|
|
156
|
+
const after = this.inputBuffer.slice(this.inputCursor);
|
|
157
|
+
this.setInputBuffer(after, 0);
|
|
158
|
+
}
|
|
159
|
+
clearInput() {
|
|
160
|
+
this.adoptSelectionIntoInput();
|
|
161
|
+
this.setInputBuffer("", 0);
|
|
162
|
+
this.index = 0;
|
|
163
|
+
}
|
|
164
|
+
hasInput() {
|
|
165
|
+
return this.inputBuffer.length > 0;
|
|
166
|
+
}
|
|
167
|
+
isPromptRowSelected() {
|
|
168
|
+
return this.index === 0;
|
|
169
|
+
}
|
|
132
170
|
getPromptCursorColumn() {
|
|
133
171
|
const prefix = this.getPrefixByIndex(0);
|
|
134
172
|
return prefix.length + this.inputCursor;
|
package/dist/keyboard.js
CHANGED
|
@@ -7,6 +7,7 @@ const KEYMAP = {
|
|
|
7
7
|
// Control keys
|
|
8
8
|
'\u0003': { name: 'ctrl-c', ctrl: true }, // ^C
|
|
9
9
|
'\u0004': { name: 'ctrl-d', ctrl: true }, // ^D
|
|
10
|
+
'\u0015': { name: 'ctrl-u', ctrl: true }, // ^U
|
|
10
11
|
'\r': { name: 'enter' },
|
|
11
12
|
'\n': { name: 'enter' },
|
|
12
13
|
'\u007f': { name: 'backspace' }, // DEL
|
package/dist/spawner.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runUserCommand = runUserCommand;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const process_1 = require("process");
|
|
6
|
+
const isWin = process.platform === "win32";
|
|
7
|
+
const shellBinary = isWin ? "cmd.exe" : "/bin/bash";
|
|
8
|
+
const shellArgs = isWin ? ["/c"] : ["-lc"];
|
|
9
|
+
const builtInCommands = {
|
|
10
|
+
cd: async (args) => {
|
|
11
|
+
if (args.length === 1) {
|
|
12
|
+
process.stdout.write(process.cwd() + "\n");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const dest = expandVars(args[1]);
|
|
16
|
+
try {
|
|
17
|
+
process.chdir(dest);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
process.stderr.write(`cd: ${err.message}\n`);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
exit: async () => {
|
|
24
|
+
(0, process_1.exit)(0);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
function expandVars(input) {
|
|
28
|
+
let out = input;
|
|
29
|
+
if (isWin) {
|
|
30
|
+
// cmd-style %VAR% expansion
|
|
31
|
+
out = out.replace(/%([^%]+)%/g, (_m, name) => {
|
|
32
|
+
const v = process.env[String(name)];
|
|
33
|
+
return v !== undefined ? v : "";
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// POSIX-style $VAR and ${VAR} expansion
|
|
38
|
+
out = out.replace(/\$(\w+)|\${(\w+)}/g, (_m, a, b) => {
|
|
39
|
+
const name = a || b;
|
|
40
|
+
const v = process.env[name];
|
|
41
|
+
return v !== undefined ? v : "";
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
async function runUserCommand(command) {
|
|
47
|
+
const trimmed = command.trim();
|
|
48
|
+
if (!trimmed)
|
|
49
|
+
return;
|
|
50
|
+
const args = command.split(/\s+/);
|
|
51
|
+
if (typeof args[0] === "string" && builtInCommands[args[0]]) {
|
|
52
|
+
await builtInCommands[args[0]](args);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const proc = (0, child_process_1.spawn)(shellBinary, [...shellArgs, command], {
|
|
56
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
57
|
+
shell: true,
|
|
58
|
+
});
|
|
59
|
+
await new Promise((resolve, reject) => {
|
|
60
|
+
proc.stdout.on("data", (data) => process.stdout.write(data));
|
|
61
|
+
proc.stderr.on("data", (data) => process.stderr.write(data));
|
|
62
|
+
proc.on("error", reject);
|
|
63
|
+
proc.on("close", () => resolve());
|
|
64
|
+
});
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caroushell",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Terminal carousel that suggests commands from history, config, and AI.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^24.10.0",
|
|
43
|
+
"@types/shell-quote": "^1.7.5",
|
|
43
44
|
"tsx": "^4.19.2",
|
|
44
45
|
"typescript": "^5.6.3"
|
|
45
46
|
}
|