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 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, AI prompts, and configuration snippets so you can pick the next shell
5
- command without leaving the keyboard.
4
+ history, and AI suggestions as you type.
6
5
 
7
6
  ## Features
8
7
 
9
- - Dual-pane carousel that combines history-based and AI-generated command
10
- suggestions.
11
- - Runs the selected command directly in your current terminal session.
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 # tsx watch mode
62
- npm run build # emits dist/
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 child_process_1 = require("child_process");
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": () => this.exit(),
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.getCurrentRow().length === 0)
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
- end: () => { },
88
- delete: () => { },
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
- // Spawn shell
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
@@ -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.0",
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
  }