@volter/teams 0.1.36 → 0.1.38

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/machine-core.mjs CHANGED
@@ -8,7 +8,8 @@
8
8
  // It decides nothing on anyone's behalf. Every act is logged.
9
9
  import { createServer, connect as netConnect } from 'node:net';
10
10
  import { EventEmitter } from 'node:events';
11
- import { existsSync, mkdirSync, rmSync, chmodSync } from 'node:fs';
11
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, chmodSync, statSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
12
13
  import { randomBytes } from 'node:crypto';
13
14
  import { WebSocket, createWebSocketStream } from 'ws';
14
15
  import { Channel, NODE_PROTOCOL } from './channel.mjs';
@@ -89,13 +90,22 @@ export class MachineCore extends EventEmitter {
89
90
  await clearStaleSocket(this.socketPath);
90
91
  await this.beforeListen();
91
92
  this.server = createServer((socket) => this.#accept(socket));
93
+ // A Unix socket is bound under a name of this daemon's own and then renamed into place: Node unlinks the name it
94
+ // bound when the server closes, and that name must never be the shared path, which a later daemon may serve from a
95
+ // file of its own (a daemon stopping later deleted the serving daemon's socket and cut it off).
96
+ const bindPath = isNamedPipe(this.socketPath) ? this.socketPath : join(dirname(this.socketPath), `.m${process.pid.toString(36)}`);
97
+ if (bindPath !== this.socketPath) rmSync(bindPath, { force: true });
92
98
  await new Promise((resolveListen, reject) => {
93
99
  this.server.once('error', reject);
94
- this.server.listen(this.socketPath, () => { this.server.off('error', reject); resolveListen(); });
100
+ this.server.listen(bindPath, () => { this.server.off('error', reject); resolveListen(); });
95
101
  });
96
102
  // Owner-only: the socket is the local door, and whoever opens it is this OS user. A named pipe's default
97
103
  // access already admits only its owner (and SYSTEM and Administrators) to connect.
98
- if (!isNamedPipe(this.socketPath)) chmodSync(this.socketPath, 0o600);
104
+ if (!isNamedPipe(this.socketPath)) {
105
+ chmodSync(bindPath, 0o600);
106
+ this.socketInode = statSync(bindPath).ino;
107
+ renameSync(bindPath, this.socketPath);
108
+ }
99
109
  this.address = this.socketPath;
100
110
  writeJson(this.p.machine, { pid: process.pid, id: this.id, socket: this.socketPath, startedAt: Date.now(), version: MACHINE_VERSION, protocol: NODE_PROTOCOL, floor: this.floor, server: this.link?.server ?? null });
101
111
  this.emit('listening', this.socketPath);
@@ -116,8 +126,11 @@ export class MachineCore extends EventEmitter {
116
126
  for (const { channel } of this.connections.values()) channel.close();
117
127
  this.server?.close();
118
128
  await this.afterStop();
119
- if (!isNamedPipe(this.socketPath)) rmSync(this.socketPath, { force: true });
120
- if (existsSync(this.p.machine)) rmSync(this.p.machine);
129
+ // Only this daemon's own socket and record: a daemon that has since taken the path over keeps them.
130
+ if (!isNamedPipe(this.socketPath) && this.socketInode !== undefined) {
131
+ try { if (statSync(this.socketPath).ino === this.socketInode) rmSync(this.socketPath, { force: true }); } catch { /* already gone */ }
132
+ }
133
+ try { if (JSON.parse(readFileSync(this.p.machine, 'utf8')).pid === process.pid) rmSync(this.p.machine); } catch { /* absent or unreadable */ }
121
134
  }
122
135
 
123
136
  /** Before the connections close: a product finishes or abandons what is in flight. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/teams",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "type": "module",
5
5
  "description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
6
6
  "license": "MIT",
package/viewer/index.html CHANGED
@@ -18,7 +18,7 @@
18
18
  #status { padding: 4px 10px; color: var(--muted); font-size: 12px; border-bottom: 1px solid var(--line); }
19
19
  #status:empty { display: none; }
20
20
  main { flex: 1; min-height: 0; display: flex; }
21
- #terminal { flex: 1; min-width: 0; padding: 4px; }
21
+ #terminal { flex: 1; min-width: 0; padding: 4px; color-scheme: light dark; background: var(--volter-terminal-background); }
22
22
  #folder { flex: 1; min-width: 0; display: none; }
23
23
  #folder.open { display: flex; }
24
24
  #entries { width: 280px; flex: none; border-right: 1px solid var(--line); overflow: auto; }
package/viewer/viewer.mjs CHANGED
@@ -61,12 +61,37 @@ function useLayout(layout) {
61
61
  const rows = layout === 'rows';
62
62
  $('session').classList.toggle('layout-rows', rows);
63
63
  if (rows && !document.getElementById('brand-tokens')) {
64
- const link = Object.assign(document.createElement('link'), { id: 'brand-tokens', rel: 'stylesheet', href: 'https://brand.volter.ai/tokens.css' });
65
- document.head.append(link);
64
+ brandTokens();
66
65
  // A chat's rows are light unless the host's theme names a scheme.
67
66
  if (!themed) document.documentElement.style.setProperty('--scui-color-scheme', 'light');
68
67
  }
69
68
  }
69
+ // The brand's tokens stylesheet, loaded once; resolves when it has loaded (or failed to).
70
+ let brandLoaded = null;
71
+ function brandTokens() {
72
+ if (brandLoaded) return brandLoaded;
73
+ const link = Object.assign(document.createElement('link'), { id: 'brand-tokens', rel: 'stylesheet', href: 'https://brand.volter.ai/tokens.css' });
74
+ brandLoaded = new Promise((resolve) => { link.onload = link.onerror = resolve; });
75
+ document.head.append(link);
76
+ return brandLoaded;
77
+ }
78
+ // A pane looks like Terminal.app's Basic profile: the brand's terminal roles, resolved on the terminal element, which
79
+ // (like Terminal.app) follows the system's appearance. xterm takes concrete values, so each is read through a probe.
80
+ const TERMINAL_SLOTS = { background: 'background', foreground: 'foreground', cursor: 'cursor', selectionBackground: 'selection', black: 'black', red: 'red', green: 'green', yellow: 'yellow', blue: 'blue', magenta: 'magenta', cyan: 'cyan', white: 'white', brightBlack: 'bright-black', brightRed: 'bright-red', brightGreen: 'bright-green', brightYellow: 'bright-yellow', brightBlue: 'bright-blue', brightMagenta: 'bright-magenta', brightCyan: 'bright-cyan', brightWhite: 'bright-white' };
81
+ function terminalLook(element) {
82
+ const probe = document.createElement('span');
83
+ probe.hidden = true;
84
+ element.append(probe);
85
+ const theme = {};
86
+ for (const [slot, role] of Object.entries(TERMINAL_SLOTS)) {
87
+ probe.style.color = `var(--volter-terminal-${role})`;
88
+ theme[slot] = getComputedStyle(probe).color;
89
+ }
90
+ probe.style.font = 'var(--volter-font-size-terminal, 11px) var(--volter-font-terminal, ui-monospace, Menlo, monospace)';
91
+ const { fontFamily, fontSize } = getComputedStyle(probe);
92
+ probe.remove();
93
+ return { fontFamily, fontSize: Number.parseFloat(fontSize), theme };
94
+ }
70
95
  // A fragment value that is base64url JSON (UTF-8), or null.
71
96
  function fragmentJson(name) {
72
97
  try {
@@ -189,9 +214,10 @@ async function showPane(connect, key, control) {
189
214
  import('https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/+esm'),
190
215
  import('https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/+esm'),
191
216
  ]);
217
+ await brandTokens();
192
218
  const element = $('terminal');
193
219
  element.hidden = false;
194
- const terminal = new Terminal({ cursorBlink: control, disableStdin: !control, fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 13, scrollback: 5000, theme: { background: '#0b0d10' } });
220
+ const terminal = new Terminal({ disableStdin: !control, scrollback: 5000, ...terminalLook(element) });
195
221
  const fit = new FitAddon();
196
222
  terminal.loadAddon(fit);
197
223
  terminal.open(element);