nou-cli 0.5.4 → 0.5.10

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.
@@ -0,0 +1,144 @@
1
+ import { randomBytes } from "crypto";
2
+ import { existsSync, statSync } from "fs";
3
+ import net from "net";
4
+ import os from "os";
5
+ import path from "path";
6
+ import { loadPackage } from "../engines/index.js";
7
+ import { lineReader, tabName } from "./client.js";
8
+ /** The pseudo-terminal package workspaces run in, installed on first use into ~/.nou/engines like an engine. */
9
+ export const PTY_PACKAGE = "@lydell/node-pty";
10
+ /** A folder for a new workspace: `~` and relative paths (from `from`) resolved; null when it isn't a folder. */
11
+ export function workspaceFolder(folder, from) {
12
+ const raw = folder.trim().replace(/^["']|["']$/g, "") || from;
13
+ const full = path.resolve(from, raw.startsWith("~") ? path.join(os.homedir(), raw.slice(1)) : raw);
14
+ try {
15
+ return existsSync(full) && statSync(full).isDirectory() ? full : null;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ /**
22
+ * Runs the workspaces window: each folder becomes a workspace, a whole Nou in its own pseudo-terminal. Only
23
+ * the active one is drawn; the others keep working (their agents too) and draw everything again when you
24
+ * switch to them. `firstArgs` go to the first workspace (e.g. --continue, to pick its conversation back up).
25
+ * Resolves with the exit code once the last workspace is closed.
26
+ */
27
+ export async function runHost(folders, firstArgs, command = [process.execPath, process.argv[1]]) {
28
+ const mod = await loadPackage(PTY_PACKAGE);
29
+ const spawnPty = mod.spawn ?? mod.default?.spawn;
30
+ const token = randomBytes(16).toString("hex");
31
+ const workspaces = [];
32
+ let active = null;
33
+ let nextId = 1;
34
+ const size = () => ({ cols: process.stdout.columns || 120, rows: process.stdout.rows || 40 });
35
+ const send = (w, msg) => w.socket?.write(`${JSON.stringify(msg)}\n`);
36
+ const tabs = () => workspaces.map((w) => ({ id: w.id, name: tabName(w.folder), folder: w.folder, busy: w.busy, active: w === active }));
37
+ const broadcast = () => {
38
+ const list = tabs();
39
+ for (const w of workspaces)
40
+ send(w, { type: "tabs", tabs: list });
41
+ };
42
+ const activate = (w) => {
43
+ active = w;
44
+ broadcast();
45
+ send(w, { type: "redraw" });
46
+ };
47
+ const server = net.createServer((socket) => {
48
+ socket.on("data", lineReader((msg) => {
49
+ const from = workspaces.find((w) => w.socket === socket);
50
+ if (msg.type === "hello") {
51
+ const w = workspaces.find((x) => x.id === msg.id);
52
+ if (!w || msg.token !== token)
53
+ return socket.destroy(); // only our own workspaces
54
+ w.socket = socket;
55
+ send(w, { type: "tabs", tabs: tabs() });
56
+ if (w === active)
57
+ send(w, { type: "redraw" });
58
+ return;
59
+ }
60
+ if (!from)
61
+ return;
62
+ if (msg.type === "state") {
63
+ from.busy = msg.busy;
64
+ broadcast();
65
+ }
66
+ else if (msg.type === "switch") {
67
+ const to = workspaces[msg.index];
68
+ if (to && to !== active)
69
+ activate(to);
70
+ }
71
+ else if (msg.type === "new") {
72
+ const folder = workspaceFolder(msg.folder, from.folder);
73
+ if (!folder)
74
+ return send(from, { type: "error", text: `No such folder: ${msg.folder}` });
75
+ activate(open(folder, []));
76
+ }
77
+ }));
78
+ socket.on("error", () => { });
79
+ });
80
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
81
+ const port = server.address().port;
82
+ let finish = () => { };
83
+ const done = new Promise((resolve) => (finish = resolve));
84
+ function open(folder, args) {
85
+ const id = String(nextId++);
86
+ const { cols, rows } = size();
87
+ const pty = spawnPty(command[0], [...command.slice(1), ...args], {
88
+ name: "xterm-256color",
89
+ cols,
90
+ rows,
91
+ cwd: folder,
92
+ env: { ...process.env, NOU_HOST_PORT: String(port), NOU_HOST_TOKEN: token, NOU_WORKSPACE_ID: id },
93
+ });
94
+ const w = { id, folder, pty, busy: false, socket: null };
95
+ workspaces.push(w);
96
+ pty.onData((data) => {
97
+ if (w === active)
98
+ process.stdout.write(data);
99
+ });
100
+ pty.onExit(({ exitCode }) => {
101
+ const at = workspaces.indexOf(w);
102
+ if (at >= 0)
103
+ workspaces.splice(at, 1);
104
+ w.socket?.destroy();
105
+ if (!workspaces.length)
106
+ return finish(exitCode);
107
+ if (w === active)
108
+ activate(workspaces[Math.min(at, workspaces.length - 1)]);
109
+ else
110
+ broadcast();
111
+ });
112
+ return w;
113
+ }
114
+ // Keys and mouse go to the workspace on screen; a resize goes to all of them.
115
+ const stdin = process.stdin;
116
+ if (stdin.isTTY)
117
+ stdin.setRawMode(true);
118
+ stdin.resume();
119
+ const onInput = (data) => active?.pty.write(data.toString("utf-8"));
120
+ stdin.on("data", onInput);
121
+ const onResize = () => {
122
+ const { cols, rows } = size();
123
+ for (const w of workspaces)
124
+ w.pty.resize(cols, rows);
125
+ };
126
+ process.stdout.on("resize", onResize);
127
+ const killAll = () => {
128
+ for (const w of workspaces)
129
+ w.pty.kill();
130
+ };
131
+ process.on("exit", killAll);
132
+ folders.forEach((folder, i) => open(folder, i === 0 ? firstArgs : []));
133
+ active = workspaces[workspaces.length - 1] ?? null; // the one just opened is the one you asked for
134
+ broadcast();
135
+ const code = await done;
136
+ stdin.off("data", onInput);
137
+ if (stdin.isTTY)
138
+ stdin.setRawMode(false);
139
+ stdin.pause();
140
+ process.stdout.off("resize", onResize);
141
+ process.off("exit", killAll);
142
+ server.close();
143
+ return code;
144
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nou-cli",
3
- "version": "0.5.4",
3
+ "version": "0.5.10",
4
4
  "description": "Nou: an IDE-style AI coding assistant for your terminal. Local (Ollama, LM Studio) and cloud (Claude, OpenAI, Gemini...) models, live diffs, git-aware, built-in editor.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,6 +61,7 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "@eslint/js": "^10.0.1",
64
+ "@lydell/node-pty": "1.2.0-beta.18",
64
65
  "@types/node": "^22.20.5",
65
66
  "@types/react": "^19.3.0",
66
67
  "eslint": "^10.2.0",