typescript-virtual-container 1.5.7 → 1.5.9
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 +39 -29
- package/dist/.tsbuildinfo +1 -1
- package/dist/SSHMimic/executor.js +9 -0
- package/dist/SSHMimic/prompt.d.ts +2 -1
- package/dist/SSHMimic/prompt.js +28 -6
- package/dist/SSHMimic/sftp.d.ts +1 -1
- package/dist/SSHMimic/sftp.js +1 -1
- package/dist/VirtualShell/index.d.ts +2 -3
- package/dist/VirtualShell/index.js +2 -3
- package/dist/VirtualShell/shell.js +108 -134
- package/dist/VirtualShell/shellParser.js +35 -3
- package/dist/commands/coreutils.d.ts +55 -0
- package/dist/commands/coreutils.js +275 -0
- package/dist/commands/id.js +8 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/manuals-bundle.js +237 -1
- package/dist/commands/pacman.d.ts +8 -0
- package/dist/commands/pacman.js +15 -0
- package/dist/commands/registry.js +13 -0
- package/dist/commands/rm.d.ts +1 -1
- package/dist/commands/rm.js +48 -11
- package/dist/commands/runtime.d.ts +5 -0
- package/dist/commands/runtime.js +60 -1
- package/dist/commands/sh.js +5 -3
- package/dist/modules/linuxRootfs.js +7 -3
- package/dist/modules/nanoEditor.d.ts +92 -0
- package/dist/modules/nanoEditor.js +974 -0
- package/dist/modules/pacmanGame.d.ts +59 -0
- package/dist/modules/pacmanGame.js +655 -0
- package/dist/modules/webTermRenderer.d.ts +50 -0
- package/dist/modules/webTermRenderer.js +425 -0
- package/dist/types/commands.d.ts +2 -0
- package/dist/types/pipeline.d.ts +2 -0
- package/dist/utils/shellSession.d.ts +10 -0
- package/dist/utils/shellSession.js +56 -0
- package/package.json +2 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal VT100 screen buffer for browser-side rendering.
|
|
3
|
+
* Handles the subset of escape sequences emitted by NanoEditor:
|
|
4
|
+
* - CSI H / CSI row;colH (cursor position)
|
|
5
|
+
* - CSI K (erase to end of line)
|
|
6
|
+
* - CSI 2J (erase display)
|
|
7
|
+
* - CSI ?25l / ?25h (cursor hide/show)
|
|
8
|
+
* - CSI <n> m (SGR — bold, reverse, fg, bg, reset)
|
|
9
|
+
*/
|
|
10
|
+
export interface Cell {
|
|
11
|
+
ch: string;
|
|
12
|
+
bold: boolean;
|
|
13
|
+
reverse: boolean;
|
|
14
|
+
fg: string | null;
|
|
15
|
+
bg: string | null;
|
|
16
|
+
}
|
|
17
|
+
export declare class WebTermRenderer {
|
|
18
|
+
private rows;
|
|
19
|
+
private cols;
|
|
20
|
+
private screen;
|
|
21
|
+
private scrollback;
|
|
22
|
+
private curRow;
|
|
23
|
+
private curCol;
|
|
24
|
+
private cursorVisible;
|
|
25
|
+
private _cleared;
|
|
26
|
+
private bold;
|
|
27
|
+
private reverse;
|
|
28
|
+
private fg;
|
|
29
|
+
private bg;
|
|
30
|
+
private buf;
|
|
31
|
+
constructor(rows: number, cols: number);
|
|
32
|
+
resize(rows: number, cols: number): void;
|
|
33
|
+
write(data: string): void;
|
|
34
|
+
private flush;
|
|
35
|
+
private handleCsi;
|
|
36
|
+
private handleSgr;
|
|
37
|
+
private scrollUp;
|
|
38
|
+
private putChar;
|
|
39
|
+
private makeScreen;
|
|
40
|
+
/** Render current screen state to an HTML string for a <pre> element. */
|
|
41
|
+
renderHtml(): string;
|
|
42
|
+
get cursorRow(): number;
|
|
43
|
+
get cursorCol(): number;
|
|
44
|
+
get isCursorVisible(): boolean;
|
|
45
|
+
/** Returns true (once) if CSI 2J was received since last call. */
|
|
46
|
+
consumeCleared(): boolean;
|
|
47
|
+
get scrollbackLength(): number;
|
|
48
|
+
clearScrollback(): void;
|
|
49
|
+
renderScrollbackHtml(): string;
|
|
50
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal VT100 screen buffer for browser-side rendering.
|
|
3
|
+
* Handles the subset of escape sequences emitted by NanoEditor:
|
|
4
|
+
* - CSI H / CSI row;colH (cursor position)
|
|
5
|
+
* - CSI K (erase to end of line)
|
|
6
|
+
* - CSI 2J (erase display)
|
|
7
|
+
* - CSI ?25l / ?25h (cursor hide/show)
|
|
8
|
+
* - CSI <n> m (SGR — bold, reverse, fg, bg, reset)
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_CELL = { ch: " ", bold: false, reverse: false, fg: null, bg: null };
|
|
11
|
+
function makeCell(partial) {
|
|
12
|
+
return { ...DEFAULT_CELL, ...partial };
|
|
13
|
+
}
|
|
14
|
+
export class WebTermRenderer {
|
|
15
|
+
rows;
|
|
16
|
+
cols;
|
|
17
|
+
screen;
|
|
18
|
+
scrollback = [];
|
|
19
|
+
curRow = 0;
|
|
20
|
+
curCol = 0;
|
|
21
|
+
cursorVisible = true;
|
|
22
|
+
_cleared = false;
|
|
23
|
+
// Current SGR state
|
|
24
|
+
bold = false;
|
|
25
|
+
reverse = false;
|
|
26
|
+
fg = null;
|
|
27
|
+
bg = null;
|
|
28
|
+
buf = "";
|
|
29
|
+
constructor(rows, cols) {
|
|
30
|
+
this.rows = rows;
|
|
31
|
+
this.cols = cols;
|
|
32
|
+
this.screen = this.makeScreen();
|
|
33
|
+
}
|
|
34
|
+
resize(rows, cols) {
|
|
35
|
+
const newScreen = this.makeScreen(rows, cols);
|
|
36
|
+
for (let r = 0; r < Math.min(rows, this.rows); r++) {
|
|
37
|
+
for (let c = 0; c < Math.min(cols, this.cols); c++) {
|
|
38
|
+
newScreen[r][c] = this.screen[r]?.[c] ?? makeCell();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
this.rows = rows;
|
|
42
|
+
this.cols = cols;
|
|
43
|
+
this.screen = newScreen;
|
|
44
|
+
this.curRow = Math.min(this.curRow, rows - 1);
|
|
45
|
+
this.curCol = Math.min(this.curCol, cols - 1);
|
|
46
|
+
}
|
|
47
|
+
write(data) {
|
|
48
|
+
this.buf += data;
|
|
49
|
+
this.flush();
|
|
50
|
+
}
|
|
51
|
+
flush() {
|
|
52
|
+
let i = 0;
|
|
53
|
+
while (i < this.buf.length) {
|
|
54
|
+
const ch = this.buf[i];
|
|
55
|
+
if (ch === "\x1b") {
|
|
56
|
+
if (i + 1 >= this.buf.length)
|
|
57
|
+
break; // wait for more data
|
|
58
|
+
const next = this.buf[i + 1];
|
|
59
|
+
if (next === "[") {
|
|
60
|
+
// CSI — find terminator
|
|
61
|
+
let j = i + 2;
|
|
62
|
+
while (j < this.buf.length && (this.buf[j] < "@" || this.buf[j] > "~"))
|
|
63
|
+
j++;
|
|
64
|
+
if (j >= this.buf.length)
|
|
65
|
+
break; // incomplete
|
|
66
|
+
const seq = this.buf.slice(i + 2, j);
|
|
67
|
+
const cmd = this.buf[j];
|
|
68
|
+
this.handleCsi(seq, cmd);
|
|
69
|
+
i = j + 1;
|
|
70
|
+
}
|
|
71
|
+
else if (next === "]") {
|
|
72
|
+
// OSC (Operating System Command) — terminator is BEL (\x07) or ST (ESC \)
|
|
73
|
+
// Must consume fully or the payload prints as raw text and corrupts SGR state.
|
|
74
|
+
let j = i + 2;
|
|
75
|
+
while (j < this.buf.length) {
|
|
76
|
+
if (this.buf[j] === "\x07") {
|
|
77
|
+
j++;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (this.buf[j] === "\x1b" && this.buf[j + 1] === "\\") {
|
|
81
|
+
j += 2;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
j++;
|
|
85
|
+
}
|
|
86
|
+
// If terminator not yet received, wait for more data
|
|
87
|
+
if (j >= this.buf.length && this.buf[j - 1] !== "\x07")
|
|
88
|
+
break;
|
|
89
|
+
i = j;
|
|
90
|
+
}
|
|
91
|
+
else if (next === "O") {
|
|
92
|
+
// SS3 — single extra byte (F1-F4, cursor keys in application mode)
|
|
93
|
+
if (i + 2 >= this.buf.length)
|
|
94
|
+
break; // wait for more data
|
|
95
|
+
i += 3; // ESC O <cmd>
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
i += 2; // skip unknown 2-char ESC sequence
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (ch === "\r") {
|
|
102
|
+
this.curCol = 0;
|
|
103
|
+
i++;
|
|
104
|
+
}
|
|
105
|
+
else if (ch === "\n") {
|
|
106
|
+
if (this.curRow < this.rows - 1) {
|
|
107
|
+
this.curRow++;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
this.scrollUp();
|
|
111
|
+
}
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
else if (ch.charCodeAt(0) >= 32) {
|
|
115
|
+
this.putChar(ch);
|
|
116
|
+
i++;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
i++; // skip other control chars
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this.buf = this.buf.slice(i);
|
|
123
|
+
}
|
|
124
|
+
handleCsi(seq, cmd) {
|
|
125
|
+
if (cmd === "H" || cmd === "f") {
|
|
126
|
+
// Cursor position: row;col (1-based)
|
|
127
|
+
const parts = seq.split(";").map((n) => Number.parseInt(n || "1", 10));
|
|
128
|
+
this.curRow = Math.max(0, Math.min((parts[0] ?? 1) - 1, this.rows - 1));
|
|
129
|
+
this.curCol = Math.max(0, Math.min((parts[1] ?? 1) - 1, this.cols - 1));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (cmd === "K") {
|
|
133
|
+
// Erase line from cursor to end
|
|
134
|
+
const mode = seq === "" ? 0 : Number.parseInt(seq, 10);
|
|
135
|
+
if (mode === 0) {
|
|
136
|
+
for (let c = this.curCol; c < this.cols; c++) {
|
|
137
|
+
this.screen[this.curRow][c] = makeCell();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (mode === 1) {
|
|
141
|
+
for (let c = 0; c <= this.curCol; c++) {
|
|
142
|
+
this.screen[this.curRow][c] = makeCell();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (mode === 2) {
|
|
146
|
+
for (let c = 0; c < this.cols; c++) {
|
|
147
|
+
this.screen[this.curRow][c] = makeCell();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (cmd === "m") {
|
|
153
|
+
this.handleSgr(seq);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (cmd === "l" && seq === "?25") {
|
|
157
|
+
this.cursorVisible = false;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (cmd === "h" && seq === "?25") {
|
|
161
|
+
this.cursorVisible = true;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Cursor movement (relative)
|
|
165
|
+
if (cmd === "A") {
|
|
166
|
+
const n = Number.parseInt(seq || "1", 10) || 1;
|
|
167
|
+
this.curRow = Math.max(0, this.curRow - n);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (cmd === "B") {
|
|
171
|
+
const n = Number.parseInt(seq || "1", 10) || 1;
|
|
172
|
+
this.curRow = Math.min(this.rows - 1, this.curRow + n);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (cmd === "C") {
|
|
176
|
+
const n = Number.parseInt(seq || "1", 10) || 1;
|
|
177
|
+
this.curCol = Math.min(this.cols - 1, this.curCol + n);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (cmd === "D") {
|
|
181
|
+
const n = Number.parseInt(seq || "1", 10) || 1;
|
|
182
|
+
this.curCol = Math.max(0, this.curCol - n);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// Cursor column absolute
|
|
186
|
+
if (cmd === "G") {
|
|
187
|
+
const n = Number.parseInt(seq || "1", 10) || 1;
|
|
188
|
+
this.curCol = Math.max(0, Math.min(n - 1, this.cols - 1));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Erase display modes 0/1
|
|
192
|
+
if (cmd === "J") {
|
|
193
|
+
const mode = seq === "" ? 0 : Number.parseInt(seq, 10);
|
|
194
|
+
if (mode === 0) {
|
|
195
|
+
for (let c = this.curCol; c < this.cols; c++)
|
|
196
|
+
this.screen[this.curRow][c] = makeCell();
|
|
197
|
+
for (let r = this.curRow + 1; r < this.rows; r++)
|
|
198
|
+
this.screen[r] = Array.from({ length: this.cols }, () => makeCell());
|
|
199
|
+
}
|
|
200
|
+
else if (mode === 1) {
|
|
201
|
+
for (let r = 0; r < this.curRow; r++)
|
|
202
|
+
this.screen[r] = Array.from({ length: this.cols }, () => makeCell());
|
|
203
|
+
for (let c = 0; c <= this.curCol; c++)
|
|
204
|
+
this.screen[this.curRow][c] = makeCell();
|
|
205
|
+
}
|
|
206
|
+
else if (mode === 2) {
|
|
207
|
+
this.screen = this.makeScreen();
|
|
208
|
+
this.scrollback = [];
|
|
209
|
+
this.curRow = 0;
|
|
210
|
+
this.curCol = 0;
|
|
211
|
+
this._cleared = true;
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
handleSgr(seq) {
|
|
217
|
+
const codes = seq === "" ? [0] : seq.split(";").map((n) => Number.parseInt(n || "0", 10));
|
|
218
|
+
let i = 0;
|
|
219
|
+
while (i < codes.length) {
|
|
220
|
+
const code = codes[i];
|
|
221
|
+
if (code === 0) {
|
|
222
|
+
this.bold = false;
|
|
223
|
+
this.reverse = false;
|
|
224
|
+
this.fg = null;
|
|
225
|
+
this.bg = null;
|
|
226
|
+
}
|
|
227
|
+
else if (code === 1) {
|
|
228
|
+
this.bold = true;
|
|
229
|
+
}
|
|
230
|
+
else if (code === 7) {
|
|
231
|
+
this.reverse = true;
|
|
232
|
+
}
|
|
233
|
+
else if (code === 22) {
|
|
234
|
+
this.bold = false;
|
|
235
|
+
}
|
|
236
|
+
else if (code === 27) {
|
|
237
|
+
this.reverse = false;
|
|
238
|
+
}
|
|
239
|
+
else if (code >= 30 && code <= 37) {
|
|
240
|
+
this.fg = ANSI_COLORS[code - 30];
|
|
241
|
+
}
|
|
242
|
+
else if (code === 38) {
|
|
243
|
+
if (codes[i + 1] === 5 && codes[i + 2] !== undefined) {
|
|
244
|
+
this.fg = xterm256(codes[i + 2]);
|
|
245
|
+
i += 2;
|
|
246
|
+
}
|
|
247
|
+
else if (codes[i + 1] === 2 && codes[i + 4] !== undefined) {
|
|
248
|
+
this.fg = `rgb(${codes[i + 2]},${codes[i + 3]},${codes[i + 4]})`;
|
|
249
|
+
i += 4;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else if (code === 39) {
|
|
253
|
+
this.fg = null;
|
|
254
|
+
}
|
|
255
|
+
else if (code >= 40 && code <= 47) {
|
|
256
|
+
this.bg = ANSI_COLORS[code - 40];
|
|
257
|
+
}
|
|
258
|
+
else if (code === 48) {
|
|
259
|
+
if (codes[i + 1] === 5 && codes[i + 2] !== undefined) {
|
|
260
|
+
this.bg = xterm256(codes[i + 2]);
|
|
261
|
+
i += 2;
|
|
262
|
+
}
|
|
263
|
+
else if (codes[i + 1] === 2 && codes[i + 4] !== undefined) {
|
|
264
|
+
this.bg = `rgb(${codes[i + 2]},${codes[i + 3]},${codes[i + 4]})`;
|
|
265
|
+
i += 4;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else if (code === 49) {
|
|
269
|
+
this.bg = null;
|
|
270
|
+
}
|
|
271
|
+
else if (code >= 90 && code <= 97) {
|
|
272
|
+
this.fg = ANSI_COLORS_BRIGHT[code - 90];
|
|
273
|
+
}
|
|
274
|
+
else if (code >= 100 && code <= 107) {
|
|
275
|
+
this.bg = ANSI_COLORS_BRIGHT[code - 100];
|
|
276
|
+
}
|
|
277
|
+
i++;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
scrollUp() {
|
|
281
|
+
const line = this.screen.shift();
|
|
282
|
+
this.scrollback.push(line);
|
|
283
|
+
if (this.scrollback.length > 1000)
|
|
284
|
+
this.scrollback.shift();
|
|
285
|
+
this.screen.push(Array.from({ length: this.cols }, () => makeCell()));
|
|
286
|
+
// curRow stays at rows-1 (bottom)
|
|
287
|
+
}
|
|
288
|
+
putChar(ch) {
|
|
289
|
+
if (this.curCol >= this.cols) {
|
|
290
|
+
this.curCol = 0;
|
|
291
|
+
if (this.curRow < this.rows - 1) {
|
|
292
|
+
this.curRow++;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
this.scrollUp();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
this.screen[this.curRow][this.curCol] = makeCell({
|
|
299
|
+
ch,
|
|
300
|
+
bold: this.bold,
|
|
301
|
+
reverse: this.reverse,
|
|
302
|
+
fg: this.fg,
|
|
303
|
+
bg: this.bg,
|
|
304
|
+
});
|
|
305
|
+
this.curCol++;
|
|
306
|
+
}
|
|
307
|
+
makeScreen(rows = this.rows, cols = this.cols) {
|
|
308
|
+
return Array.from({ length: rows }, () => Array.from({ length: cols }, () => makeCell()));
|
|
309
|
+
}
|
|
310
|
+
/** Render current screen state to an HTML string for a <pre> element. */
|
|
311
|
+
renderHtml() {
|
|
312
|
+
let html = "";
|
|
313
|
+
for (let r = 0; r < this.rows; r++) {
|
|
314
|
+
const row = this.screen[r];
|
|
315
|
+
let spanOpen = false;
|
|
316
|
+
let lastStyle = "";
|
|
317
|
+
for (let c = 0; c < this.cols; c++) {
|
|
318
|
+
const cell = row[c];
|
|
319
|
+
const isCursor = this.cursorVisible && r === this.curRow && c === this.curCol;
|
|
320
|
+
let fg = cell.fg ?? "#ccc";
|
|
321
|
+
let bg = cell.bg ?? "transparent";
|
|
322
|
+
if (cell.reverse) {
|
|
323
|
+
[fg, bg] = [bg === "transparent" ? "#000" : bg, fg === "transparent" ? "#000" : fg];
|
|
324
|
+
}
|
|
325
|
+
if (isCursor) {
|
|
326
|
+
// Isoler le curseur dans son propre span pour éviter que sa couleur
|
|
327
|
+
// inversée ne déborde sur les cellules vides adjacentes.
|
|
328
|
+
if (spanOpen) {
|
|
329
|
+
html += "</span>";
|
|
330
|
+
spanOpen = false;
|
|
331
|
+
lastStyle = "";
|
|
332
|
+
}
|
|
333
|
+
const curFg = bg === "transparent" ? "#000" : bg;
|
|
334
|
+
const boldPart = cell.bold ? "font-weight:bold;" : "";
|
|
335
|
+
html += `<span style="color:${curFg};background:#ccc;${boldPart}">${escHtml(cell.ch)}</span>`;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const style = `color:${fg};background:${bg};${cell.bold ? "font-weight:bold;" : ""}`;
|
|
339
|
+
if (style !== lastStyle) {
|
|
340
|
+
if (spanOpen)
|
|
341
|
+
html += "</span>";
|
|
342
|
+
html += `<span style="${style}">`;
|
|
343
|
+
spanOpen = true;
|
|
344
|
+
lastStyle = style;
|
|
345
|
+
}
|
|
346
|
+
html += escHtml(cell.ch);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (spanOpen)
|
|
350
|
+
html += "</span>";
|
|
351
|
+
if (r < this.rows - 1)
|
|
352
|
+
html += "\n";
|
|
353
|
+
}
|
|
354
|
+
return html;
|
|
355
|
+
}
|
|
356
|
+
get cursorRow() { return this.curRow; }
|
|
357
|
+
get cursorCol() { return this.curCol; }
|
|
358
|
+
get isCursorVisible() { return this.cursorVisible; }
|
|
359
|
+
/** Returns true (once) if CSI 2J was received since last call. */
|
|
360
|
+
consumeCleared() {
|
|
361
|
+
const v = this._cleared;
|
|
362
|
+
this._cleared = false;
|
|
363
|
+
return v;
|
|
364
|
+
}
|
|
365
|
+
get scrollbackLength() { return this.scrollback.length; }
|
|
366
|
+
clearScrollback() { this.scrollback = []; }
|
|
367
|
+
renderScrollbackHtml() {
|
|
368
|
+
let html = "";
|
|
369
|
+
for (const row of this.scrollback) {
|
|
370
|
+
let spanOpen = false;
|
|
371
|
+
let lastStyle = "";
|
|
372
|
+
for (const cell of row) {
|
|
373
|
+
let fg = cell.fg ?? "#ccc";
|
|
374
|
+
let bg = cell.bg ?? "transparent";
|
|
375
|
+
if (cell.reverse) {
|
|
376
|
+
[fg, bg] = [bg === "transparent" ? "#000" : bg, fg === "transparent" ? "#000" : fg];
|
|
377
|
+
}
|
|
378
|
+
const style = `color:${fg};background:${bg};${cell.bold ? "font-weight:bold;" : ""}`;
|
|
379
|
+
if (style !== lastStyle) {
|
|
380
|
+
if (spanOpen)
|
|
381
|
+
html += "</span>";
|
|
382
|
+
html += `<span style="${style}">`;
|
|
383
|
+
spanOpen = true;
|
|
384
|
+
lastStyle = style;
|
|
385
|
+
}
|
|
386
|
+
html += escHtml(cell.ch);
|
|
387
|
+
}
|
|
388
|
+
if (spanOpen)
|
|
389
|
+
html += "</span>";
|
|
390
|
+
html += "\n";
|
|
391
|
+
}
|
|
392
|
+
return html;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// const ANSI_NORMAL_TO_BRIGHT: Record<string, string> = {
|
|
396
|
+
// "#000": "#555", "#c00": "#f55", "#0c0": "#5f5", "#cc0": "#ff5",
|
|
397
|
+
// "#00c": "#55f", "#c0c": "#f5f", "#0cc": "#5ff", "#ccc": "#fff",
|
|
398
|
+
// };
|
|
399
|
+
// function boldBright(fg: string): string {
|
|
400
|
+
// return ANSI_NORMAL_TO_BRIGHT[fg] ?? fg;
|
|
401
|
+
// }
|
|
402
|
+
function escHtml(ch) {
|
|
403
|
+
if (ch === "&")
|
|
404
|
+
return "&";
|
|
405
|
+
if (ch === "<")
|
|
406
|
+
return "<";
|
|
407
|
+
if (ch === ">")
|
|
408
|
+
return ">";
|
|
409
|
+
return ch;
|
|
410
|
+
}
|
|
411
|
+
const ANSI_COLORS = ["#000", "#c00", "#0c0", "#cc0", "#00c", "#c0c", "#0cc", "#ccc"];
|
|
412
|
+
const ANSI_COLORS_BRIGHT = ["#555", "#f55", "#5f5", "#ff5", "#55f", "#f5f", "#5ff", "#fff"];
|
|
413
|
+
function xterm256(n) {
|
|
414
|
+
if (n < 16)
|
|
415
|
+
return (n < 8 ? ANSI_COLORS : ANSI_COLORS_BRIGHT)[n < 8 ? n : n - 8];
|
|
416
|
+
if (n < 232) {
|
|
417
|
+
const i = n - 16;
|
|
418
|
+
const r = Math.floor(i / 36) * 51;
|
|
419
|
+
const g = Math.floor((i % 36) / 6) * 51;
|
|
420
|
+
const b = (i % 6) * 51;
|
|
421
|
+
return `rgb(${r},${g},${b})`;
|
|
422
|
+
}
|
|
423
|
+
const v = (n - 232) * 10 + 8;
|
|
424
|
+
return `rgb(${v},${v},${v})`;
|
|
425
|
+
}
|
package/dist/types/commands.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface CommandResult {
|
|
|
27
27
|
openEditor?: NanoEditorSession;
|
|
28
28
|
/** Request opening built-in htop-like screen. */
|
|
29
29
|
openHtop?: boolean;
|
|
30
|
+
/** Request opening built-in Pac-Man game. */
|
|
31
|
+
openPacman?: boolean;
|
|
30
32
|
/** Request sudo password challenge flow. */
|
|
31
33
|
sudoChallenge?: SudoChallenge;
|
|
32
34
|
/** Request a generic password challenge (adduser, passwd). */
|
package/dist/types/pipeline.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface Statement {
|
|
|
36
36
|
op?: LogicalOp;
|
|
37
37
|
/** Optional next statement in sequence. */
|
|
38
38
|
next?: Statement;
|
|
39
|
+
/** Run in background (trailing &). */
|
|
40
|
+
background?: boolean;
|
|
39
41
|
}
|
|
40
42
|
/** Top-level parse result for a script. */
|
|
41
43
|
export interface Script {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
2
|
+
export declare function loadHistory(vfs: VirtualFileSystem, authUser: string): string[];
|
|
3
|
+
export declare function saveHistory(vfs: VirtualFileSystem, authUser: string, history: string[]): void;
|
|
4
|
+
export interface LastLogin {
|
|
5
|
+
at: string;
|
|
6
|
+
from: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function readLastLogin(vfs: VirtualFileSystem, authUser: string): LastLogin | null;
|
|
9
|
+
export declare function writeLastLogin(vfs: VirtualFileSystem, authUser: string, from: string): void;
|
|
10
|
+
export declare function listPathCompletions(vfs: VirtualFileSystem, cwd: string, prefix: string): string[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { resolvePath } from "../commands/helpers";
|
|
3
|
+
import { userHome } from "../commands/runtime";
|
|
4
|
+
// ── History ───────────────────────────────────────────────────────────────────
|
|
5
|
+
export function loadHistory(vfs, authUser) {
|
|
6
|
+
const historyPath = `${userHome(authUser)}/.bash_history`;
|
|
7
|
+
if (!vfs.exists(historyPath)) {
|
|
8
|
+
vfs.writeFile(historyPath, "");
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
return vfs.readFile(historyPath)
|
|
12
|
+
.split("\n")
|
|
13
|
+
.map((l) => l.trim())
|
|
14
|
+
.filter((l) => l.length > 0);
|
|
15
|
+
}
|
|
16
|
+
export function saveHistory(vfs, authUser, history) {
|
|
17
|
+
const data = history.length > 0 ? `${history.join("\n")}\n` : "";
|
|
18
|
+
vfs.writeFile(`${userHome(authUser)}/.bash_history`, data);
|
|
19
|
+
}
|
|
20
|
+
export function readLastLogin(vfs, authUser) {
|
|
21
|
+
const p = authUser === "root" ? "/root/.lastlog.json" : `/home/${authUser}/.lastlog`;
|
|
22
|
+
if (!vfs.exists(p))
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(vfs.readFile(p));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function writeLastLogin(vfs, authUser, from) {
|
|
32
|
+
const p = authUser === "root" ? "/root/.lastlog.json" : `/home/${authUser}/.lastlog`;
|
|
33
|
+
vfs.writeFile(p, JSON.stringify({ at: new Date().toISOString(), from }));
|
|
34
|
+
}
|
|
35
|
+
// ── Path completion ───────────────────────────────────────────────────────────
|
|
36
|
+
export function listPathCompletions(vfs, cwd, prefix) {
|
|
37
|
+
const slashIndex = prefix.lastIndexOf("/");
|
|
38
|
+
const dirPart = slashIndex >= 0 ? prefix.slice(0, slashIndex + 1) : "";
|
|
39
|
+
const namePart = slashIndex >= 0 ? prefix.slice(slashIndex + 1) : prefix;
|
|
40
|
+
const basePath = resolvePath(cwd, dirPart || ".");
|
|
41
|
+
try {
|
|
42
|
+
return vfs
|
|
43
|
+
.list(basePath)
|
|
44
|
+
.filter((e) => e.startsWith(namePart))
|
|
45
|
+
.filter((e) => namePart.startsWith(".") || !e.startsWith("."))
|
|
46
|
+
.map((e) => {
|
|
47
|
+
const fullPath = path.posix.join(basePath, e);
|
|
48
|
+
const st = vfs.stat(fullPath);
|
|
49
|
+
return `${dirPart}${e}${st.type === "directory" ? "/" : ""}`;
|
|
50
|
+
})
|
|
51
|
+
.sort();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"version": "1.5.
|
|
7
|
+
"version": "1.5.9",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist/",
|
|
10
10
|
"README.md",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"deploy:npm": "bun publish --access public",
|
|
38
38
|
"bench": "rm -rf .benchmark-shells/ && bun benchmark-virtualshell.ts",
|
|
39
39
|
"benchmark": "bun benchmark-virtualshell.ts > benchmark-results.txt",
|
|
40
|
-
"web-build": "bunx esbuild src/web.ts --bundle --platform=browser --format=esm --target=es2020 --outfile=builds/web.min.js --tree-shaking=true --minify",
|
|
40
|
+
"web-build": "bunx esbuild src/web.ts --bundle --platform=browser --format=esm --target=es2020 --outfile=builds/web.min.js --tree-shaking=true --minify",
|
|
41
41
|
"web-build-iife": "bunx esbuild src/web.ts --bundle --platform=browser --format=iife --target=es2020 --outfile=builds/web-iife.min.js --tree-shaking=true --minify --global-name=WebShellLib",
|
|
42
42
|
"example-build": "bun run web-build && cp builds/web.min.js examples/web.min.js",
|
|
43
43
|
"example-serve": "cd examples && bun server.js",
|