nou-cli 0.5.2 → 0.5.3
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 +12 -7
- package/dist/src/agent/index.js +33 -3
- package/dist/src/cli.js +3 -1
- package/dist/src/commands/index.js +1 -1
- package/dist/src/config/index.js +1 -1
- package/dist/src/conversations/external.js +165 -0
- package/dist/src/conversations/index.js +71 -0
- package/dist/src/engines/claude.js +10 -0
- package/dist/src/engines/codex.js +60 -5
- package/dist/src/providers/anthropic.js +3 -14
- package/dist/src/terminal/index.js +127 -0
- package/dist/src/ui/App.js +258 -39
- package/dist/src/ui/Chat.js +63 -5
- package/dist/src/ui/ChatInput.js +5 -0
- package/dist/src/ui/CodeView.js +1 -1
- package/dist/src/ui/FileTree.js +5 -4
- package/dist/src/ui/GamePanel.js +18 -11
- package/dist/src/ui/Tabs.js +6 -5
- package/dist/src/ui/Terminal.js +84 -0
- package/dist/src/ui/commands.js +3 -0
- package/dist/src/ui/render-options.js +4 -0
- package/dist/src/ui/text.js +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { existsSync, statSync } from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { stripVTControlCharacters } from "util";
|
|
6
|
+
import { stopProcess } from "../commands/index.js";
|
|
7
|
+
/** Lines of output kept (the oldest go first). */
|
|
8
|
+
const KEEP = 3000;
|
|
9
|
+
/**
|
|
10
|
+
* The terminal panel's shell: runs what you type with your shell in the folder you've `cd`'d to, and
|
|
11
|
+
* keeps the output. Each command is its own process (no native pseudo-terminal), so full-screen programs
|
|
12
|
+
* like vim won't draw, but builds, tests, git and servers work, and what you type while one runs goes
|
|
13
|
+
* to it.
|
|
14
|
+
*/
|
|
15
|
+
export class Shell {
|
|
16
|
+
onChange;
|
|
17
|
+
lines = [];
|
|
18
|
+
cwd = process.cwd();
|
|
19
|
+
child = null;
|
|
20
|
+
/** The line being written (output without its newline yet; a \r starts it over, like progress bars). */
|
|
21
|
+
partial = "";
|
|
22
|
+
constructor(onChange) {
|
|
23
|
+
this.onChange = onChange;
|
|
24
|
+
process.once("exit", () => this.interrupt()); // nothing it started is left running after Nou
|
|
25
|
+
}
|
|
26
|
+
get running() {
|
|
27
|
+
return !!this.child;
|
|
28
|
+
}
|
|
29
|
+
/** Runs a command line; `cd` and `clear` are handled here so they stick. */
|
|
30
|
+
run(command) {
|
|
31
|
+
const line = command.trim();
|
|
32
|
+
if (!line)
|
|
33
|
+
return;
|
|
34
|
+
if (this.child)
|
|
35
|
+
return this.input(command);
|
|
36
|
+
this.push({ text: `${this.prompt()}${line}`, kind: "command" });
|
|
37
|
+
if (line === "clear" || line === "cls") {
|
|
38
|
+
this.lines = [];
|
|
39
|
+
return this.onChange();
|
|
40
|
+
}
|
|
41
|
+
const cd = /^cd(?:\s+(.*))?$/.exec(line);
|
|
42
|
+
if (cd)
|
|
43
|
+
return this.changeDir(cd[1]?.trim().replace(/^["']|["']$/g, "") || os.homedir());
|
|
44
|
+
const posix = process.platform !== "win32";
|
|
45
|
+
const child = spawn(line, {
|
|
46
|
+
cwd: this.cwd,
|
|
47
|
+
shell: posix ? process.env.SHELL || true : true,
|
|
48
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
49
|
+
detached: posix, // its own process group, so Ctrl+C stops everything it started
|
|
50
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
51
|
+
});
|
|
52
|
+
this.child = child;
|
|
53
|
+
const write = (chunk) => this.write(chunk.toString("utf-8"));
|
|
54
|
+
child.stdout?.on("data", write);
|
|
55
|
+
child.stderr?.on("data", write);
|
|
56
|
+
child.stdin?.on("error", () => { }); // it may exit before reading what you typed
|
|
57
|
+
child.on("error", (error) => this.push({ text: error.message, kind: "error" }));
|
|
58
|
+
child.on("close", (code, signal) => {
|
|
59
|
+
this.flush();
|
|
60
|
+
if (signal)
|
|
61
|
+
this.push({ text: "stopped", kind: "info" });
|
|
62
|
+
else if (code)
|
|
63
|
+
this.push({ text: `exit ${code}`, kind: "error" });
|
|
64
|
+
this.child = null;
|
|
65
|
+
this.onChange();
|
|
66
|
+
});
|
|
67
|
+
this.onChange();
|
|
68
|
+
}
|
|
69
|
+
/** Text for the running command's input (it gets a newline, like pressing Enter in a terminal). */
|
|
70
|
+
input(text) {
|
|
71
|
+
if (!this.child?.stdin?.writable)
|
|
72
|
+
return;
|
|
73
|
+
this.push({ text, kind: "command" });
|
|
74
|
+
this.child.stdin.write(`${text}\n`);
|
|
75
|
+
}
|
|
76
|
+
/** Ctrl+C: stops the running command. */
|
|
77
|
+
interrupt() {
|
|
78
|
+
if (this.child)
|
|
79
|
+
stopProcess(this.child);
|
|
80
|
+
}
|
|
81
|
+
clear() {
|
|
82
|
+
this.lines = [];
|
|
83
|
+
this.partial = "";
|
|
84
|
+
this.onChange();
|
|
85
|
+
}
|
|
86
|
+
dispose() {
|
|
87
|
+
this.interrupt();
|
|
88
|
+
}
|
|
89
|
+
prompt() {
|
|
90
|
+
const home = os.homedir();
|
|
91
|
+
const where = this.cwd === home ? "~" : this.cwd.startsWith(home + path.sep) ? `~${this.cwd.slice(home.length)}` : this.cwd;
|
|
92
|
+
return `${where.length > 30 ? `…${path.sep}${path.basename(where)}` : where} $ `;
|
|
93
|
+
}
|
|
94
|
+
changeDir(target) {
|
|
95
|
+
const next = path.resolve(this.cwd, target.startsWith("~") ? path.join(os.homedir(), target.slice(1)) : target);
|
|
96
|
+
if (!existsSync(next) || !statSync(next).isDirectory())
|
|
97
|
+
this.push({ text: `cd: no such folder: ${target}`, kind: "error" });
|
|
98
|
+
else
|
|
99
|
+
this.cwd = next;
|
|
100
|
+
this.onChange();
|
|
101
|
+
}
|
|
102
|
+
write(text) {
|
|
103
|
+
const parts = stripVTControlCharacters(text).replace(/\r\n/g, "\n").split("\n");
|
|
104
|
+
for (let i = 0; i < parts.length; i++) {
|
|
105
|
+
const part = parts[i];
|
|
106
|
+
// A carriage return goes back to the start of the line: what follows replaces it.
|
|
107
|
+
this.partial = part.includes("\r") ? part.slice(part.lastIndexOf("\r") + 1) || this.partial : this.partial + part;
|
|
108
|
+
if (i < parts.length - 1)
|
|
109
|
+
this.flush(true);
|
|
110
|
+
}
|
|
111
|
+
this.onChange();
|
|
112
|
+
}
|
|
113
|
+
flush(always = false) {
|
|
114
|
+
if (this.partial || always)
|
|
115
|
+
this.push({ text: this.partial.replace(/\t/g, " "), kind: "out" });
|
|
116
|
+
this.partial = "";
|
|
117
|
+
}
|
|
118
|
+
/** The output so far, with the line still being written. */
|
|
119
|
+
view() {
|
|
120
|
+
return this.partial ? [...this.lines, { text: this.partial, kind: "out" }] : this.lines;
|
|
121
|
+
}
|
|
122
|
+
push(line) {
|
|
123
|
+
this.lines.push(line);
|
|
124
|
+
if (this.lines.length > KEEP)
|
|
125
|
+
this.lines.splice(0, this.lines.length - KEEP);
|
|
126
|
+
}
|
|
127
|
+
}
|