caroushell 0.1.23 → 0.1.29
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 +33 -0
- package/dist/app.js +22 -8
- package/dist/carousel.js +13 -2
- package/dist/config.js +33 -4
- package/dist/hello-new-user.js +102 -35
- package/dist/history-suggester.js +2 -1
- package/dist/main.js +7 -1
- package/dist/spawner.js +13 -4
- package/dist/terminal.js +36 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,6 +64,39 @@ OpenAI, etc.) will work as long as the URL, key, and model are valid. If you
|
|
|
64
64
|
only provide a Gemini API key in the config, Caroushell will default to the
|
|
65
65
|
Gemini Flash Lite 2.5 endpoint and model.
|
|
66
66
|
|
|
67
|
+
## Prompt Display
|
|
68
|
+
|
|
69
|
+
Caroushell can render a custom prompt using a template string in
|
|
70
|
+
`~/.caroushell/config.toml`:
|
|
71
|
+
|
|
72
|
+
```toml
|
|
73
|
+
prompt = "{hostname} {short-directory} $>"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Available tokens:
|
|
77
|
+
|
|
78
|
+
- `{hostname}`
|
|
79
|
+
- `{directory}`
|
|
80
|
+
- `{short-directory}`
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
|
|
84
|
+
```toml
|
|
85
|
+
prompt = "$> "
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```toml
|
|
89
|
+
prompt = "{directory} $> "
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```toml
|
|
93
|
+
prompt = "{hostname} {short-directory} $>"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`{short-directory}` keeps the final directory name and shortens parent
|
|
97
|
+
directories to their first letter. For example, `/home/user/projects/my-app`
|
|
98
|
+
becomes `/h/u/p/my-app`.
|
|
99
|
+
|
|
67
100
|
## Installation
|
|
68
101
|
|
|
69
102
|
Install globally (recommended):
|
package/dist/app.js
CHANGED
|
@@ -5,7 +5,6 @@ const terminal_1 = require("./terminal");
|
|
|
5
5
|
const keyboard_1 = require("./keyboard");
|
|
6
6
|
const carousel_1 = require("./carousel");
|
|
7
7
|
const history_suggester_1 = require("./history-suggester");
|
|
8
|
-
const ai_suggester_1 = require("./ai-suggester");
|
|
9
8
|
const file_suggester_1 = require("./file-suggester");
|
|
10
9
|
const spawner_1 = require("./spawner");
|
|
11
10
|
const logs_1 = require("./logs");
|
|
@@ -18,15 +17,16 @@ class App {
|
|
|
18
17
|
this.terminal = deps.terminal ?? new terminal_1.Terminal();
|
|
19
18
|
this.keyboard = deps.keyboard ?? new keyboard_1.Keyboard();
|
|
20
19
|
this.history = deps.topPanel ?? new history_suggester_1.HistorySuggester();
|
|
21
|
-
this.
|
|
20
|
+
this.bottomSuggester = deps.bottomPanel ?? new carousel_1.NullSuggester();
|
|
22
21
|
this.files = deps.files ?? new file_suggester_1.FileSuggester();
|
|
23
|
-
this.suggesters = deps.suggesters ?? [this.history, this.
|
|
22
|
+
this.suggesters = deps.suggesters ?? [this.history, this.bottomSuggester, this.files];
|
|
24
23
|
this.carousel = new carousel_1.Carousel({
|
|
25
24
|
top: this.history,
|
|
26
|
-
bottom: this.
|
|
25
|
+
bottom: this.bottomSuggester,
|
|
27
26
|
topRows: 2,
|
|
28
|
-
bottomRows: 2,
|
|
27
|
+
bottomRows: this.bottomSuggester instanceof carousel_1.NullSuggester ? 0 : 2,
|
|
29
28
|
terminal: this.terminal,
|
|
29
|
+
promptLine0: deps.promptLine0,
|
|
30
30
|
});
|
|
31
31
|
this.queueUpdateSuggestions = () => {
|
|
32
32
|
void this.carousel.updateSuggestions();
|
|
@@ -137,9 +137,9 @@ class App {
|
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
async init() {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
for (const s of this.suggesters) {
|
|
141
|
+
await s.init();
|
|
142
|
+
}
|
|
143
143
|
}
|
|
144
144
|
async run() {
|
|
145
145
|
await this.init();
|
|
@@ -185,6 +185,7 @@ class App {
|
|
|
185
185
|
this.terminal.write("\n");
|
|
186
186
|
this.keyboard.disableCapture();
|
|
187
187
|
this.terminal.disableWrites();
|
|
188
|
+
await this.preBroadcastCommand(cmd);
|
|
188
189
|
try {
|
|
189
190
|
const storeInHistory = await (0, spawner_1.runUserCommand)(cmd);
|
|
190
191
|
if (storeInHistory) {
|
|
@@ -291,6 +292,19 @@ class App {
|
|
|
291
292
|
this.usingFileSuggestions = true;
|
|
292
293
|
this.carousel.setTopSuggester(this.files);
|
|
293
294
|
}
|
|
295
|
+
async preBroadcastCommand(cmd) {
|
|
296
|
+
const listeners = this.suggesters
|
|
297
|
+
.map((suggester) => suggester.onCommandWillRun?.(cmd))
|
|
298
|
+
.filter(Boolean);
|
|
299
|
+
if (listeners.length === 0)
|
|
300
|
+
return;
|
|
301
|
+
try {
|
|
302
|
+
await Promise.all(listeners);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
(0, logs_1.logLine)("suggester onCommandWillRun error: " + err?.message);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
294
308
|
async broadcastCommand(cmd) {
|
|
295
309
|
const listeners = this.suggesters
|
|
296
310
|
.map((suggester) => suggester.onCommandRan?.(cmd))
|
package/dist/carousel.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Carousel = void 0;
|
|
3
|
+
exports.Carousel = exports.NullSuggester = void 0;
|
|
4
4
|
exports.getDisplayWidth = getDisplayWidth;
|
|
5
5
|
const logs_1 = require("./logs");
|
|
6
6
|
const terminal_1 = require("./terminal");
|
|
@@ -57,6 +57,16 @@ function getDisplayWidth(text) {
|
|
|
57
57
|
}
|
|
58
58
|
return width;
|
|
59
59
|
}
|
|
60
|
+
class NullSuggester {
|
|
61
|
+
constructor() {
|
|
62
|
+
this.prefix = "";
|
|
63
|
+
}
|
|
64
|
+
async init() { }
|
|
65
|
+
async refreshSuggestions() { }
|
|
66
|
+
latest() { return []; }
|
|
67
|
+
descriptionForAi() { return ""; }
|
|
68
|
+
}
|
|
69
|
+
exports.NullSuggester = NullSuggester;
|
|
60
70
|
class Carousel {
|
|
61
71
|
constructor(opts) {
|
|
62
72
|
this.index = 0;
|
|
@@ -67,6 +77,7 @@ class Carousel {
|
|
|
67
77
|
this.bottom = opts.bottom;
|
|
68
78
|
this.topRowCount = opts.topRows;
|
|
69
79
|
this.bottomRowCount = opts.bottomRows;
|
|
80
|
+
this.promptLine0Getter = opts.promptLine0 ?? (() => "$> ");
|
|
70
81
|
}
|
|
71
82
|
async updateSuggestions(input) {
|
|
72
83
|
if (typeof input === "string") {
|
|
@@ -386,7 +397,7 @@ class Carousel {
|
|
|
386
397
|
return start;
|
|
387
398
|
}
|
|
388
399
|
getPromptPrefix(lineIndex) {
|
|
389
|
-
return lineIndex === 0 ?
|
|
400
|
+
return lineIndex === 0 ? this.promptLine0Getter() : "> ";
|
|
390
401
|
}
|
|
391
402
|
render() {
|
|
392
403
|
(0, logs_1.logLine)("Rendering carousel");
|
package/dist/config.js
CHANGED
|
@@ -40,6 +40,7 @@ exports.configFolder = configFolder;
|
|
|
40
40
|
exports.getConfigPath = getConfigPath;
|
|
41
41
|
exports.doesConfigExist = doesConfigExist;
|
|
42
42
|
exports.getConfig = getConfig;
|
|
43
|
+
exports.buildPromptLine0 = buildPromptLine0;
|
|
43
44
|
const fs_1 = require("fs");
|
|
44
45
|
const path = __importStar(require("path"));
|
|
45
46
|
const os = __importStar(require("os"));
|
|
@@ -88,7 +89,6 @@ async function doesConfigExist() {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
async function getConfig() {
|
|
91
|
-
const configPath = getConfigPath();
|
|
92
92
|
const raw = await readConfigFile();
|
|
93
93
|
const envApiKey = process.env.CAROUSHELL_API_KEY || process.env.GEMINI_API_KEY || undefined;
|
|
94
94
|
const envApiUrl = process.env.CAROUSHELL_API_URL || undefined;
|
|
@@ -107,9 +107,6 @@ async function getConfig() {
|
|
|
107
107
|
if (!resolved.model && geminiApiKey) {
|
|
108
108
|
resolved.model = GEMINI_DEFAULT_MODEL;
|
|
109
109
|
}
|
|
110
|
-
if (!resolved.apiUrl || !resolved.apiKey || !resolved.model) {
|
|
111
|
-
throw new Error(`Config at ${configPath} is missing required fields. Please include apiUrl, apiKey, and model (or just GEMINI_API_KEY).`);
|
|
112
|
-
}
|
|
113
110
|
return resolved;
|
|
114
111
|
}
|
|
115
112
|
function isGeminiUrl(url) {
|
|
@@ -117,3 +114,35 @@ function isGeminiUrl(url) {
|
|
|
117
114
|
return (lower.includes("generativelanguage.googleapis.com") ||
|
|
118
115
|
lower.includes("gemini"));
|
|
119
116
|
}
|
|
117
|
+
function normalizeCwd(cwd) {
|
|
118
|
+
const normalized = cwd.replace(/\\/g, "/");
|
|
119
|
+
const home = os.homedir().replace(/\\/g, "/");
|
|
120
|
+
if (normalized === home)
|
|
121
|
+
return "~";
|
|
122
|
+
if (normalized.startsWith(home + "/")) {
|
|
123
|
+
return "~" + normalized.slice(home.length);
|
|
124
|
+
}
|
|
125
|
+
return normalized;
|
|
126
|
+
}
|
|
127
|
+
function shortenPath(p) {
|
|
128
|
+
const parts = p.split("/");
|
|
129
|
+
return parts
|
|
130
|
+
.map((part, i) => {
|
|
131
|
+
if (i === parts.length - 1)
|
|
132
|
+
return part;
|
|
133
|
+
if (part === "" || part === "~")
|
|
134
|
+
return part;
|
|
135
|
+
return part[0];
|
|
136
|
+
})
|
|
137
|
+
.join("/");
|
|
138
|
+
}
|
|
139
|
+
function buildPromptLine0(config) {
|
|
140
|
+
return () => {
|
|
141
|
+
const normalized = normalizeCwd(process.cwd());
|
|
142
|
+
const template = config.prompt ?? "$> ";
|
|
143
|
+
return template
|
|
144
|
+
.replace(/\{hostname\}/g, os.hostname())
|
|
145
|
+
.replace(/\{directory\}/g, normalized)
|
|
146
|
+
.replace(/\{short-directory\}/g, shortenPath(normalized));
|
|
147
|
+
};
|
|
148
|
+
}
|
package/dist/hello-new-user.js
CHANGED
|
@@ -42,6 +42,42 @@ const path = __importStar(require("path"));
|
|
|
42
42
|
const readline_1 = __importDefault(require("readline"));
|
|
43
43
|
const ai_suggester_1 = require("./ai-suggester");
|
|
44
44
|
const preferredModels = ["gemini-2.5-flash-lite", "gpt-4o-mini"];
|
|
45
|
+
const defaultPromptTemplate = "$> ";
|
|
46
|
+
function serializeToml(obj) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
const sections = [];
|
|
49
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
50
|
+
if (value !== undefined && typeof value === "object" && value !== null) {
|
|
51
|
+
sections.push([key, value]);
|
|
52
|
+
}
|
|
53
|
+
else if (value !== undefined) {
|
|
54
|
+
lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const [section, values] of sections) {
|
|
58
|
+
lines.push(`\n[${section}]`);
|
|
59
|
+
for (const [key, value] of Object.entries(values)) {
|
|
60
|
+
if (value !== undefined) {
|
|
61
|
+
lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
async function askPromptConfig(prompter, logFn) {
|
|
68
|
+
logFn("Customize your shell prompt with plain text and tokens.");
|
|
69
|
+
logFn("Available tokens:");
|
|
70
|
+
logFn(" {hostname}");
|
|
71
|
+
logFn(" {directory}");
|
|
72
|
+
logFn(" {short-directory}");
|
|
73
|
+
logFn(`Press Enter to keep the default prompt: ${defaultPromptTemplate}`);
|
|
74
|
+
const answer = await prompter.ask("Prompt template: ");
|
|
75
|
+
const trimmed = answer.trim();
|
|
76
|
+
if (!trimmed || trimmed === defaultPromptTemplate.trim()) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return answer;
|
|
80
|
+
}
|
|
45
81
|
function isInteractive() {
|
|
46
82
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
47
83
|
}
|
|
@@ -50,6 +86,25 @@ async function prompt(question, rl) {
|
|
|
50
86
|
rl.question(question, (answer) => resolve(answer));
|
|
51
87
|
});
|
|
52
88
|
}
|
|
89
|
+
function createReadlineTerminal() {
|
|
90
|
+
return {
|
|
91
|
+
createPrompter() {
|
|
92
|
+
const rl = readline_1.default.createInterface({
|
|
93
|
+
input: process.stdin,
|
|
94
|
+
output: process.stdout,
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
ask(question) {
|
|
98
|
+
return prompt(question, rl);
|
|
99
|
+
},
|
|
100
|
+
close() {
|
|
101
|
+
rl.close();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
isInteractive,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
53
108
|
function findShortestMatches(models, preferredList) {
|
|
54
109
|
const matches = [];
|
|
55
110
|
for (const pref of preferredList) {
|
|
@@ -61,82 +116,94 @@ function findShortestMatches(models, preferredList) {
|
|
|
61
116
|
}
|
|
62
117
|
return [...new Set(matches)];
|
|
63
118
|
}
|
|
64
|
-
async function runHelloNewUserFlow(configPath) {
|
|
65
|
-
|
|
119
|
+
async function runHelloNewUserFlow(configPath, deps = {}) {
|
|
120
|
+
const fileSystem = deps.fsOps ?? fs_1.promises;
|
|
121
|
+
const listModelsFn = deps.listModelsFn ?? ai_suggester_1.listModels;
|
|
122
|
+
const logFn = deps.logFn ?? console.log;
|
|
123
|
+
const terminal = deps.terminal ?? createReadlineTerminal();
|
|
124
|
+
if (!terminal.isInteractive()) {
|
|
66
125
|
throw new Error(`Missing config at ${configPath} and no interactive terminal is available.\n` +
|
|
67
126
|
"Create the file manually or run Caroushell from a TTY.");
|
|
68
127
|
}
|
|
69
128
|
const dir = path.dirname(configPath);
|
|
70
|
-
await
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
129
|
+
await fileSystem.mkdir(dir, { recursive: true });
|
|
130
|
+
logFn("");
|
|
131
|
+
logFn("Welcome to Caroushell!");
|
|
132
|
+
logFn("");
|
|
133
|
+
const prompter = terminal.createPrompter();
|
|
134
|
+
const promptConfig = await askPromptConfig(prompter, logFn);
|
|
135
|
+
const wantsAi = (await prompter.ask("Do you want to set up AI auto-complete? (y/n): "))
|
|
136
|
+
.trim()
|
|
137
|
+
.toLowerCase();
|
|
138
|
+
if (wantsAi !== "y" && wantsAi !== "yes") {
|
|
139
|
+
prompter.close();
|
|
140
|
+
const config = { noAi: true, prompt: promptConfig };
|
|
141
|
+
await fileSystem.writeFile(configPath, serializeToml(config) + "\n", "utf8");
|
|
142
|
+
logFn("\nSkipping AI setup. You can set it up later by editing " + configPath);
|
|
143
|
+
logFn("");
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
logFn(`\nLet's set up AI suggestions. You'll need an API endpoint URL, a key, and model id. These will be stored at ${configPath}`);
|
|
147
|
+
logFn("");
|
|
148
|
+
logFn("Some example endpoints you can paste:");
|
|
149
|
+
logFn(" - OpenRouter: https://openrouter.ai/api/v1");
|
|
150
|
+
logFn(" - OpenAI: https://api.openai.com/v1");
|
|
151
|
+
logFn(" - Google: https://generativelanguage.googleapis.com/v1beta/openai");
|
|
152
|
+
logFn("");
|
|
153
|
+
logFn("Press Ctrl+C any time to abort.\n");
|
|
85
154
|
let apiUrl = "";
|
|
86
155
|
while (!apiUrl) {
|
|
87
|
-
const answer = (await
|
|
156
|
+
const answer = (await prompter.ask("API URL: ")).trim();
|
|
88
157
|
if (answer) {
|
|
89
158
|
apiUrl = answer;
|
|
90
159
|
}
|
|
91
160
|
else {
|
|
92
|
-
|
|
161
|
+
logFn("Please enter a URL (example: https://openrouter.ai/api/v1)");
|
|
93
162
|
}
|
|
94
163
|
}
|
|
95
164
|
let apiKey = "";
|
|
96
165
|
while (!apiKey) {
|
|
97
|
-
const answer = (await
|
|
166
|
+
const answer = (await prompter.ask("API key: ")).trim();
|
|
98
167
|
if (answer) {
|
|
99
168
|
apiKey = answer;
|
|
100
169
|
}
|
|
101
170
|
else {
|
|
102
|
-
|
|
171
|
+
logFn("Please enter an API key. The value is stored in the local config file.");
|
|
103
172
|
}
|
|
104
173
|
}
|
|
105
|
-
const models = await (
|
|
174
|
+
const models = await listModelsFn(apiUrl, apiKey);
|
|
106
175
|
if (models.length > 0) {
|
|
107
176
|
const preferred = findShortestMatches(models, preferredModels);
|
|
108
|
-
|
|
177
|
+
logFn("Here are a few example model ids from your api service. Choose a fast and cheap model because AI suggestions happen as you type.");
|
|
109
178
|
for (const model of models.slice(0, 5)) {
|
|
110
|
-
|
|
179
|
+
logFn(` - ${model}`);
|
|
111
180
|
}
|
|
112
181
|
if (preferred.length) {
|
|
113
|
-
|
|
182
|
+
logFn("Recommended models from your provider:");
|
|
114
183
|
for (const model of preferred) {
|
|
115
|
-
|
|
184
|
+
logFn(` - ${model}`);
|
|
116
185
|
}
|
|
117
186
|
}
|
|
118
187
|
}
|
|
119
188
|
let model = "";
|
|
120
189
|
while (!model) {
|
|
121
|
-
const answer = (await
|
|
190
|
+
const answer = (await prompter.ask("Model id: ")).trim();
|
|
122
191
|
if (answer) {
|
|
123
192
|
model = answer;
|
|
124
193
|
}
|
|
125
194
|
else {
|
|
126
|
-
|
|
195
|
+
logFn("Please enter a model id (example: google/gemini-2.5-flash-lite, mistralai/mistral-small-24b-instruct-2501).");
|
|
127
196
|
}
|
|
128
197
|
}
|
|
129
|
-
|
|
198
|
+
prompter.close();
|
|
130
199
|
const config = {
|
|
131
200
|
apiUrl,
|
|
132
201
|
apiKey,
|
|
133
202
|
model,
|
|
203
|
+
prompt: promptConfig,
|
|
134
204
|
};
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
await fs_1.promises.writeFile(configPath, tomlBody + "\n", "utf8");
|
|
139
|
-
console.log(`\nSaved config to ${configPath}`);
|
|
140
|
-
console.log("You can edit this file later if you want to switch providers.\n");
|
|
205
|
+
await fileSystem.writeFile(configPath, serializeToml(config) + "\n", "utf8");
|
|
206
|
+
logFn(`\nSaved config to ${configPath}`);
|
|
207
|
+
logFn("You can edit this file later if you want to switch providers.\n");
|
|
141
208
|
return config;
|
|
142
209
|
}
|
|
@@ -48,9 +48,10 @@ class HistorySuggester {
|
|
|
48
48
|
.catch(() => { });
|
|
49
49
|
await fs_1.promises.appendFile(this.filePath, this.serializeHistoryEntry(command), "utf8");
|
|
50
50
|
}
|
|
51
|
-
async
|
|
51
|
+
async onCommandWillRun(command) {
|
|
52
52
|
await this.add(command);
|
|
53
53
|
}
|
|
54
|
+
async onCommandRan(command) { }
|
|
54
55
|
latest() {
|
|
55
56
|
return this.filteredItems;
|
|
56
57
|
}
|
package/dist/main.js
CHANGED
|
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
const app_1 = require("./app");
|
|
7
|
+
const ai_suggester_1 = require("./ai-suggester");
|
|
8
|
+
const carousel_1 = require("./carousel");
|
|
7
9
|
const hello_new_user_1 = require("./hello-new-user");
|
|
8
10
|
const logs_1 = require("./logs");
|
|
9
11
|
const config_1 = require("./config");
|
|
@@ -25,7 +27,11 @@ async function main() {
|
|
|
25
27
|
if (!(await (0, config_1.doesConfigExist)())) {
|
|
26
28
|
await (0, hello_new_user_1.runHelloNewUserFlow)((0, config_1.getConfigPath)());
|
|
27
29
|
}
|
|
28
|
-
const
|
|
30
|
+
const config = await (0, config_1.getConfig)();
|
|
31
|
+
const bottomPanel = config.apiUrl && config.apiKey && config.model
|
|
32
|
+
? new ai_suggester_1.AISuggester()
|
|
33
|
+
: new carousel_1.NullSuggester();
|
|
34
|
+
const app = new app_1.App({ bottomPanel, promptLine0: (0, config_1.buildPromptLine0)(config) });
|
|
29
35
|
await app.run();
|
|
30
36
|
}
|
|
31
37
|
main().catch((err) => {
|
package/dist/spawner.js
CHANGED
|
@@ -144,10 +144,19 @@ async function runUserCommand(command) {
|
|
|
144
144
|
stdio: "inherit",
|
|
145
145
|
windowsVerbatimArguments: true,
|
|
146
146
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
// While a user command owns the terminal, Ctrl+C should interrupt that command
|
|
148
|
+
// without taking down the parent Caroushell process.
|
|
149
|
+
const ignoreSigint = () => { };
|
|
150
|
+
process.on("SIGINT", ignoreSigint);
|
|
151
|
+
try {
|
|
152
|
+
await new Promise((resolve, reject) => {
|
|
153
|
+
proc.on("error", reject);
|
|
154
|
+
proc.on("close", () => resolve());
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
process.off("SIGINT", ignoreSigint);
|
|
159
|
+
}
|
|
151
160
|
// Why save failed commands? Well eg sometimes we want to run a test
|
|
152
161
|
// many times until we fix it.
|
|
153
162
|
return true;
|
package/dist/terminal.js
CHANGED
|
@@ -22,6 +22,8 @@ class Terminal {
|
|
|
22
22
|
this.cursorRow = 0;
|
|
23
23
|
this.cursorCol = 0;
|
|
24
24
|
this.writesDisabled = false;
|
|
25
|
+
this.pendingBlock = null;
|
|
26
|
+
this.renderScheduled = false;
|
|
25
27
|
}
|
|
26
28
|
disableWrites() {
|
|
27
29
|
this.writesDisabled = true;
|
|
@@ -70,6 +72,7 @@ class Terminal {
|
|
|
70
72
|
write(text) {
|
|
71
73
|
if (!this.canWrite())
|
|
72
74
|
return;
|
|
75
|
+
this.flushPendingRender();
|
|
73
76
|
this.out.write(text);
|
|
74
77
|
}
|
|
75
78
|
hideCursor() {
|
|
@@ -80,6 +83,26 @@ class Terminal {
|
|
|
80
83
|
}
|
|
81
84
|
// Render a block of lines by clearing previous block (if any) and writing fresh
|
|
82
85
|
renderBlock(lines, cursorRow, cursorCol) {
|
|
86
|
+
if (!this.canWrite())
|
|
87
|
+
return;
|
|
88
|
+
this.pendingBlock = {
|
|
89
|
+
lines: [...lines],
|
|
90
|
+
cursorRow,
|
|
91
|
+
cursorCol,
|
|
92
|
+
};
|
|
93
|
+
if (this.renderScheduled)
|
|
94
|
+
return;
|
|
95
|
+
this.renderScheduled = true;
|
|
96
|
+
queueMicrotask(() => this.flushPendingRender());
|
|
97
|
+
}
|
|
98
|
+
flushPendingRender() {
|
|
99
|
+
if (!this.pendingBlock) {
|
|
100
|
+
this.renderScheduled = false;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const pending = this.pendingBlock;
|
|
104
|
+
this.pendingBlock = null;
|
|
105
|
+
this.renderScheduled = false;
|
|
83
106
|
if (!this.canWrite())
|
|
84
107
|
return;
|
|
85
108
|
this.withCork(() => {
|
|
@@ -88,21 +111,22 @@ class Terminal {
|
|
|
88
111
|
readline_1.default.cursorTo(this.out, 0);
|
|
89
112
|
readline_1.default.clearScreenDown(this.out);
|
|
90
113
|
}
|
|
91
|
-
for (let i = 0; i < lines.length; i++) {
|
|
92
|
-
this.write(lines[i]);
|
|
93
|
-
if (i < lines.length - 1)
|
|
94
|
-
this.write("\n");
|
|
114
|
+
for (let i = 0; i < pending.lines.length; i++) {
|
|
115
|
+
this.out.write(pending.lines[i]);
|
|
116
|
+
if (i < pending.lines.length - 1)
|
|
117
|
+
this.out.write("\n");
|
|
95
118
|
}
|
|
96
|
-
this.activeRows = lines.length;
|
|
119
|
+
this.activeRows = pending.lines.length;
|
|
97
120
|
this.cursorRow = Math.max(0, this.activeRows - 1);
|
|
98
|
-
const lastLine = lines[this.cursorRow] || "";
|
|
121
|
+
const lastLine = pending.lines[this.cursorRow] || "";
|
|
99
122
|
this.cursorCol = lastLine.length;
|
|
100
|
-
const needsPosition = typeof cursorRow === "number" ||
|
|
123
|
+
const needsPosition = typeof pending.cursorRow === "number" ||
|
|
124
|
+
typeof pending.cursorCol === "number";
|
|
101
125
|
if (needsPosition) {
|
|
102
|
-
const targetRow = typeof cursorRow === "number"
|
|
103
|
-
? Math.min(Math.max(cursorRow, 0), Math.max(0, this.activeRows - 1))
|
|
126
|
+
const targetRow = typeof pending.cursorRow === "number"
|
|
127
|
+
? Math.min(Math.max(pending.cursorRow, 0), Math.max(0, this.activeRows - 1))
|
|
104
128
|
: this.cursorRow;
|
|
105
|
-
const targetCol = Math.max(0, cursorCol ?? this.cursorCol);
|
|
129
|
+
const targetCol = Math.max(0, pending.cursorCol ?? this.cursorCol);
|
|
106
130
|
this.moveCursorTo(targetRow, targetCol);
|
|
107
131
|
}
|
|
108
132
|
});
|
|
@@ -125,6 +149,8 @@ class Terminal {
|
|
|
125
149
|
// When we have printed arbitrary output that is not managed by renderBlock,
|
|
126
150
|
// reset internal line tracking so the next render starts fresh.
|
|
127
151
|
resetBlockTracking() {
|
|
152
|
+
this.pendingBlock = null;
|
|
153
|
+
this.renderScheduled = false;
|
|
128
154
|
this.activeRows = 0;
|
|
129
155
|
this.cursorRow = 0;
|
|
130
156
|
this.cursorCol = 0;
|