sh3-server 0.15.0 → 0.15.2

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/app/index.html CHANGED
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>SH3</title>
8
- <script type="module" crossorigin src="/assets/index-D37_vuLS.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-vrVBfm18.css">
8
+ <script type="module" crossorigin src="/assets/index-dcWTOW90.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-CuexSuRN.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="app"></div>
@@ -3,9 +3,11 @@ export declare class HistoryStore {
3
3
  private readonly max;
4
4
  private buffer;
5
5
  constructor(dataDir: string, userId: string, historyMaxLines: number);
6
- /** Read the capped in-memory view most recent `historyMaxLines` lines. */
7
- read(): string[];
8
- /** Append a new line to both the on-disk file and the in-memory buffer. */
9
- append(line: string): void;
6
+ /** Read lines for a specific mode, in append order, up to the cap. */
7
+ read(mode: string): string[];
8
+ /** Read all-modes bundle for the history-bundle WS reply. */
9
+ readBundle(): Record<string, string[]>;
10
+ /** Append a line tagged with the active mode. */
11
+ append(line: string, mode: string): void;
10
12
  private loadBuffer;
11
13
  }
@@ -2,12 +2,17 @@
2
2
  * Per-user append-only history store for the shell-shard server.
3
3
  *
4
4
  * Storage format: JSONL at <dataDir>/history-<userId>.jsonl.
5
- * Each line: { "ts": <ms>, "line": "<command>" }.
5
+ * Each line: { "ts": <ms>, "line": "<command>", "mode": "<modeId>" }.
6
+ *
7
+ * Records carry a mode tag so different modes (sh3, bash, custom) maintain
8
+ * independent histories on read. Legacy untagged records (pre-tag rollout)
9
+ * load as 'bash' for back-compat with existing JSONL files.
6
10
  *
7
11
  * The store holds a capped in-memory view (most recent `historyMaxLines`)
8
- * that is served to clients on attach. The on-disk file is not rotated
9
- * in v1 — rotation is in the deferred list. If the file exceeds 2× the
10
- * cap, a warning is logged on load but the store still functions.
12
+ * served to clients on attach via the history-bundle WS message. The on-
13
+ * disk file is not rotated in v1 — rotation is in the deferred list. If the
14
+ * file exceeds 2× the cap, a warning is logged on load but the store still
15
+ * functions.
11
16
  *
12
17
  * A corrupt tail line (partial write from a crash mid-append) is skipped
13
18
  * on load with a warning.
@@ -23,15 +28,23 @@ export class HistoryStore {
23
28
  this.max = historyMaxLines;
24
29
  this.buffer = this.loadBuffer();
25
30
  }
26
- /** Read the capped in-memory view most recent `historyMaxLines` lines. */
27
- read() {
28
- return [...this.buffer];
31
+ /** Read lines for a specific mode, in append order, up to the cap. */
32
+ read(mode) {
33
+ return this.buffer.filter((r) => r.mode === mode).map((r) => r.line);
34
+ }
35
+ /** Read all-modes bundle for the history-bundle WS reply. */
36
+ readBundle() {
37
+ const out = {};
38
+ for (const r of this.buffer) {
39
+ (out[r.mode] ??= []).push(r.line);
40
+ }
41
+ return out;
29
42
  }
30
- /** Append a new line to both the on-disk file and the in-memory buffer. */
31
- append(line) {
32
- const record = { ts: Date.now(), line };
43
+ /** Append a line tagged with the active mode. */
44
+ append(line, mode) {
45
+ const record = { ts: Date.now(), line, mode };
33
46
  appendFileSync(this.path, JSON.stringify(record) + '\n', 'utf-8');
34
- this.buffer.push(line);
47
+ this.buffer.push(record);
35
48
  if (this.buffer.length > this.max) {
36
49
  this.buffer.splice(0, this.buffer.length - this.max);
37
50
  }
@@ -47,9 +60,13 @@ export class HistoryStore {
47
60
  continue;
48
61
  try {
49
62
  const record = JSON.parse(rawLine);
50
- if (typeof record.line === 'string') {
51
- parsed.push(record.line);
52
- }
63
+ if (typeof record.line !== 'string')
64
+ continue;
65
+ parsed.push({
66
+ ts: typeof record.ts === 'number' ? record.ts : 0,
67
+ line: record.line,
68
+ mode: typeof record.mode === 'string' ? record.mode : 'bash',
69
+ });
53
70
  }
54
71
  catch {
55
72
  // Corrupt tail line — skip silently. Log at debug level only.
@@ -31,7 +31,8 @@ export default {
31
31
  app.get('/history', ctx.adminOnly, (c) => {
32
32
  const user = sessionUser(c);
33
33
  const session = manager.getOrCreate(user);
34
- return c.json({ lines: session.readHistory() });
34
+ const mode = c.req.query('mode') ?? 'bash';
35
+ return c.json({ mode, lines: session.readHistory(mode) });
35
36
  });
36
37
  app.get('/session', ctx.adminOnly, ctx.wsRegister((ws, c) => {
37
38
  // Route this connection to the per-user ShellSession. sessionUser
@@ -24,6 +24,12 @@ export interface WsLike {
24
24
  }
25
25
  export declare class ShellSession {
26
26
  readonly userId: string;
27
+ /**
28
+ * Initial cwd at session construction — the per-user shell tenant root.
29
+ * Frozen for the session's lifetime so clients can render relative paths
30
+ * (e.g., `~/foo`) regardless of subsequent `cd` motion.
31
+ */
32
+ readonly tenantRoot: string;
27
33
  cwd: string;
28
34
  env: Record<string, string>;
29
35
  private readonly history;
@@ -40,14 +46,14 @@ export declare class ShellSession {
40
46
  broadcast(event: ServerEvent): void;
41
47
  /**
42
48
  * Submit a command line. Ignored if a process is already running.
43
- * Appends the line to history and starts the runner. Stdout/stderr/
44
- * exit are broadcast as events.
49
+ * Appends the line to history (tagged with the active mode) and starts
50
+ * the runner. Stdout/stderr/exit are broadcast as events.
45
51
  */
46
- submit(line: string, _fromWs: WsLike): Promise<void>;
52
+ submit(line: string, mode: string, _fromWs: WsLike): Promise<void>;
47
53
  /** Record a line in history without running anything (for local SH3 verbs). */
48
- historyLog(line: string): void;
54
+ historyLog(line: string, mode: string): void;
49
55
  /** Read the persisted history buffer — used by the JSON /history endpoint. */
50
- readHistory(): string[];
56
+ readHistory(mode: string): string[];
51
57
  /** Forward a signal to the running process, if any. */
52
58
  signal(sig: 'SIGINT' | 'EOF'): void;
53
59
  /** Change cwd and broadcast a cwd update to every attached client. */
@@ -16,6 +16,12 @@
16
16
  import { HistoryStore } from './history-store.js';
17
17
  export class ShellSession {
18
18
  userId;
19
+ /**
20
+ * Initial cwd at session construction — the per-user shell tenant root.
21
+ * Frozen for the session's lifetime so clients can render relative paths
22
+ * (e.g., `~/foo`) regardless of subsequent `cd` motion.
23
+ */
24
+ tenantRoot;
19
25
  cwd;
20
26
  env;
21
27
  history;
@@ -32,6 +38,7 @@ export class ShellSession {
32
38
  this.history = history;
33
39
  this.ringSize = cfg.ringSize;
34
40
  this.cwd = cfg.defaultCwd || process.cwd();
41
+ this.tenantRoot = this.cwd;
35
42
  this.env = { ...process.env, FORCE_COLOR: '1' };
36
43
  }
37
44
  attach(ws) {
@@ -40,14 +47,15 @@ export class ShellSession {
40
47
  t: 'welcome',
41
48
  userId: this.userId,
42
49
  cwd: this.cwd,
50
+ tenantRoot: this.tenantRoot,
43
51
  env: this.env,
44
52
  seq: this.nextSeq - 1,
45
53
  };
46
54
  ws.send(JSON.stringify(welcome));
47
55
  const replay = { t: 'replay', events: [...this.ring] };
48
56
  ws.send(JSON.stringify(replay));
49
- const history = { t: 'history', lines: this.history.read() };
50
- ws.send(JSON.stringify(history));
57
+ const bundle = { t: 'history-bundle', byMode: this.history.readBundle() };
58
+ ws.send(JSON.stringify(bundle));
51
59
  }
52
60
  detach(ws) {
53
61
  this.clients.delete(ws);
@@ -69,10 +77,10 @@ export class ShellSession {
69
77
  }
70
78
  /**
71
79
  * Submit a command line. Ignored if a process is already running.
72
- * Appends the line to history and starts the runner. Stdout/stderr/
73
- * exit are broadcast as events.
80
+ * Appends the line to history (tagged with the active mode) and starts
81
+ * the runner. Stdout/stderr/exit are broadcast as events.
74
82
  */
75
- async submit(line, _fromWs) {
83
+ async submit(line, mode, _fromWs) {
76
84
  if (this.running || this.starting) {
77
85
  console.warn(`[shell-shard] submit while running — ignored (user=${this.userId})`);
78
86
  return;
@@ -88,7 +96,7 @@ export class ShellSession {
88
96
  // by the running-guard above. See runner.ts for the full race writeup.
89
97
  let exited = false;
90
98
  try {
91
- this.history.append(line);
99
+ this.history.append(line, mode);
92
100
  // Emit a prompt event so every attached client echoes the line
93
101
  this.broadcast({
94
102
  kind: 'prompt',
@@ -134,12 +142,12 @@ export class ShellSession {
134
142
  }
135
143
  }
136
144
  /** Record a line in history without running anything (for local SH3 verbs). */
137
- historyLog(line) {
138
- this.history.append(line);
145
+ historyLog(line, mode) {
146
+ this.history.append(line, mode);
139
147
  }
140
148
  /** Read the persisted history buffer — used by the JSON /history endpoint. */
141
- readHistory() {
142
- return this.history.read();
149
+ readHistory(mode) {
150
+ return this.history.read(mode);
143
151
  }
144
152
  /** Forward a signal to the running process, if any. */
145
153
  signal(sig) {
@@ -27,14 +27,14 @@ export function handleClientMessage(session, ws, raw) {
27
27
  applyCwdChange(session, target, 'cd');
28
28
  return;
29
29
  }
30
- void session.submit(msg.line, ws);
30
+ void session.submit(msg.line, msg.mode, ws);
31
31
  return;
32
32
  }
33
33
  case 'signal':
34
34
  session.signal(msg.sig);
35
35
  return;
36
36
  case 'history-log':
37
- session.historyLog(msg.line);
37
+ session.historyLog(msg.line, msg.mode);
38
38
  return;
39
39
  case 'cwd-query':
40
40
  session.setCwd(session.cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-server",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sh3-server": "dist/cli.js"