@volter/supercode-teams 0.2.35 → 0.2.36
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.mjs +8 -23
- package/package.json +1 -1
- package/popup-terminals.mjs +81 -0
package/machine.mjs
CHANGED
|
@@ -45,6 +45,7 @@ import { fingerprint, signBytes, canonical } from '@volter/teams/identity';
|
|
|
45
45
|
import { teamsHome, paths, readJson, writeJson, Launches } from '@volter/teams/home';
|
|
46
46
|
import { request as httpRequest } from 'node:http';
|
|
47
47
|
import { DEFAULT_RANK, normalizeRank, judge } from './attention.mjs';
|
|
48
|
+
import { popupTerminal, openPopup } from './popup-terminals.mjs';
|
|
48
49
|
import { JobOwner, executionCapabilities, streamProgram, serveWorkspace } from './execution.mjs';
|
|
49
50
|
|
|
50
51
|
/** The preload every launched pane carries: it announces the ports a pane's node processes listen on. */
|
|
@@ -67,16 +68,6 @@ const COMPOSER_RECHECK_MS = 400;
|
|
|
67
68
|
* must have stopped changing for this long before the pane counts as ready. */
|
|
68
69
|
const INITIAL_INPUT_SETTLE_MS = 1_000;
|
|
69
70
|
|
|
70
|
-
/** The WezTerm this machine has, on PATH or where its installer puts it, or null. */
|
|
71
|
-
function weztermProgram() {
|
|
72
|
-
const candidates = process.platform === 'win32'
|
|
73
|
-
? [join(process.env.ProgramFiles ?? 'C:\\Program Files', 'WezTerm', 'wezterm.exe')]
|
|
74
|
-
: ['/opt/homebrew/bin/wezterm', '/usr/local/bin/wezterm', '/Applications/WezTerm.app/Contents/MacOS/wezterm', '/usr/bin/wezterm'];
|
|
75
|
-
const onPath = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['wezterm'], { encoding: 'utf8', timeout: 3_000 });
|
|
76
|
-
const found = onPath.status === 0 ? onPath.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean) : null;
|
|
77
|
-
return found ?? candidates.find((path) => existsSync(path)) ?? null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
71
|
/** How long a launch watches its new pane before answering, so a program that ends at once is reported. */
|
|
81
72
|
const LAUNCH_SETTLE_MS = 500;
|
|
82
73
|
|
|
@@ -676,23 +667,17 @@ export class TeamsMachine extends MachineCore {
|
|
|
676
667
|
listPorts() { return listAnnouncedPorts(this.portsDir); }
|
|
677
668
|
|
|
678
669
|
/**
|
|
679
|
-
* A terminal window on this machine's screen showing the pane, so the people at the machine see what runs on it
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
* pane lives in tmux, or in this daemon's ConPTY host on Windows). No OS automation permission is involved. Without
|
|
683
|
-
* WezTerm there is no window, as before.
|
|
670
|
+
* A terminal window on this machine's screen showing the pane, so the people at the machine see what runs on it:
|
|
671
|
+
* in whichever capable terminal is installed (popup-terminals.mjs), running an attach client. The pane ending ends
|
|
672
|
+
* the client and the window closes; a person closing the window ends only the client, never the pane.
|
|
684
673
|
*/
|
|
685
674
|
#popupWindow(contextKey, session) {
|
|
686
|
-
const
|
|
687
|
-
if (!
|
|
675
|
+
const terminal = popupTerminal(this.env);
|
|
676
|
+
if (!terminal) return;
|
|
688
677
|
const attach = process.platform === 'win32'
|
|
689
678
|
? [process.execPath, fileURLToPath(new URL('./bin/fleet.mjs', import.meta.url)), 'attach', contextKey, '--mode', 'control']
|
|
690
|
-
: ['tmux', ...(this.tmuxSocket ? ['-L', this.tmuxSocket] : []), 'attach-session', '-t', session.label ?? contextKey];
|
|
691
|
-
|
|
692
|
-
const child = spawn(wezterm, ['start', '--always-new-process', '--', ...attach], { detached: true, stdio: 'ignore', env: { ...this.env, TMUX: '' } });
|
|
693
|
-
child.on('error', () => {});
|
|
694
|
-
child.unref();
|
|
695
|
-
} catch { /* no window; the pane runs regardless */ }
|
|
679
|
+
: [spawnSync('which', ['tmux'], { encoding: 'utf8' }).stdout.trim() || 'tmux', ...(this.tmuxSocket ? ['-L', this.tmuxSocket] : []), 'attach-session', '-t', session.label ?? contextKey];
|
|
680
|
+
openPopup(terminal, attach, { ...this.env, TMUX: '' });
|
|
696
681
|
}
|
|
697
682
|
|
|
698
683
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.36",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// The terminal a pane's window opens in: whichever capable terminal this machine has. Each adapter opens a new window
|
|
2
|
+
// running one command (the pane's attach client) through the terminal's own launch interface: never AppleScript, so
|
|
3
|
+
// no automation permission is asked for. Closing the window ends only the attach client, never the pane (tmux, or the
|
|
4
|
+
// daemon's ConPTY host on Windows, holds it); the pane ending ends the client, and the terminal closes the window as
|
|
5
|
+
// its command exits. Adapters are tried in rank order; SUPERCODE_POPUP_TERMINAL names one (or `none`).
|
|
6
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
7
|
+
import { chmodSync, existsSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
const onPath = (name) => {
|
|
13
|
+
const found = spawnSync(process.platform === 'win32' ? 'where' : 'which', [name], { encoding: 'utf8', timeout: 3_000 });
|
|
14
|
+
return found.status === 0 ? found.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? null : null;
|
|
15
|
+
};
|
|
16
|
+
const firstPath = (...paths) => paths.find((path) => path && existsSync(path)) ?? null;
|
|
17
|
+
const programFiles = process.env.ProgramFiles ?? 'C:\\Program Files';
|
|
18
|
+
const localApps = process.env.LOCALAPPDATA ?? '';
|
|
19
|
+
|
|
20
|
+
/** A shell script that deletes itself and then becomes `argv`: what a terminal opened on a file runs. */
|
|
21
|
+
function selfDeletingScript(argv, extension) {
|
|
22
|
+
const path = join(tmpdir(), `supercode-pane-${randomBytes(6).toString('hex')}${extension}`);
|
|
23
|
+
const quoted = argv.map((arg) => `'${String(arg).replaceAll("'", `'"'"'`)}'`).join(' ');
|
|
24
|
+
writeFileSync(path, `#!/bin/sh\nrm -f -- "$0"\nexec ${quoted}\n`);
|
|
25
|
+
chmodSync(path, 0o700);
|
|
26
|
+
return path;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Adapters by platform, in rank order: { name, find() → program or null, launch(program, argv) → [command, args] }. */
|
|
30
|
+
const ADAPTERS = {
|
|
31
|
+
darwin: [
|
|
32
|
+
{ name: 'wezterm', find: () => onPath('wezterm') ?? firstPath('/Applications/WezTerm.app/Contents/MacOS/wezterm'), launch: (p, argv) => [p, ['start', '--always-new-process', '--', ...argv]] },
|
|
33
|
+
{ name: 'kitty', find: () => onPath('kitty') ?? firstPath('/Applications/kitty.app/Contents/MacOS/kitty'), launch: (p, argv) => [p, argv] },
|
|
34
|
+
{ name: 'ghostty', find: () => firstPath('/Applications/Ghostty.app'), launch: (p, argv) => ['open', ['-na', p, '--args', '-e', ...argv]] },
|
|
35
|
+
{ name: 'alacritty', find: () => onPath('alacritty') ?? firstPath('/Applications/Alacritty.app/Contents/MacOS/alacritty'), launch: (p, argv) => [p, ['-e', ...argv]] },
|
|
36
|
+
// iTerm2 and Terminal run a script file they are asked to open: a plain `open`, not a scripting bridge.
|
|
37
|
+
{ name: 'iterm2', find: () => firstPath('/Applications/iTerm.app'), launch: (p, argv) => ['open', ['-a', p, selfDeletingScript(argv, '.command')]] },
|
|
38
|
+
{ name: 'terminal', find: () => firstPath('/System/Applications/Utilities/Terminal.app', '/Applications/Utilities/Terminal.app'), launch: (p, argv) => ['open', ['-a', p, selfDeletingScript(argv, '.command')]] },
|
|
39
|
+
],
|
|
40
|
+
win32: [
|
|
41
|
+
{ name: 'wezterm', find: () => onPath('wezterm') ?? firstPath(join(programFiles, 'WezTerm', 'wezterm.exe')), launch: (p, argv) => [p, ['start', '--always-new-process', '--', ...argv]] },
|
|
42
|
+
{ name: 'alacritty', find: () => onPath('alacritty') ?? firstPath(join(programFiles, 'Alacritty', 'alacritty.exe')), launch: (p, argv) => [p, ['-e', ...argv]] },
|
|
43
|
+
{ name: 'windows-terminal', find: () => onPath('wt') ?? firstPath(join(localApps, 'Microsoft', 'WindowsApps', 'wt.exe')), launch: (p, argv) => [p, ['-w', 'new', 'new-tab', '--', ...argv]] },
|
|
44
|
+
// A console window of its own, on every Windows: it lives exactly as long as its command.
|
|
45
|
+
{ name: 'conhost', find: () => firstPath(join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'conhost.exe')), launch: (p, argv) => [p, argv] },
|
|
46
|
+
],
|
|
47
|
+
linux: [
|
|
48
|
+
{ name: 'wezterm', find: () => onPath('wezterm'), launch: (p, argv) => [p, ['start', '--always-new-process', '--', ...argv]] },
|
|
49
|
+
{ name: 'kitty', find: () => onPath('kitty'), launch: (p, argv) => [p, argv] },
|
|
50
|
+
{ name: 'ghostty', find: () => onPath('ghostty'), launch: (p, argv) => [p, ['-e', ...argv]] },
|
|
51
|
+
{ name: 'alacritty', find: () => onPath('alacritty'), launch: (p, argv) => [p, ['-e', ...argv]] },
|
|
52
|
+
{ name: 'xterm', find: () => onPath('xterm'), launch: (p, argv) => [p, ['-e', ...argv]] },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** The terminal a window opens in here: the one SUPERCODE_POPUP_TERMINAL names, else the best installed; or null. */
|
|
57
|
+
export function popupTerminal(env = process.env, platform = process.platform) {
|
|
58
|
+
const adapters = ADAPTERS[platform] ?? [];
|
|
59
|
+
const wanted = env.SUPERCODE_POPUP_TERMINAL?.trim().toLowerCase();
|
|
60
|
+
if (wanted === 'none') return null;
|
|
61
|
+
// A display to show a window on: macOS and Windows always have one for a signed-in user; Linux needs X or Wayland.
|
|
62
|
+
if (platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY) return null;
|
|
63
|
+
for (const adapter of wanted ? adapters.filter((item) => item.name === wanted) : adapters) {
|
|
64
|
+
const program = adapter.find();
|
|
65
|
+
if (program) return { name: adapter.name, program, launch: (argv) => adapter.launch(program, argv) };
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Opens a window running `argv` in `terminal` (from popupTerminal). Best effort: the pane runs whatever happens here. */
|
|
71
|
+
export function openPopup(terminal, argv, env) {
|
|
72
|
+
try {
|
|
73
|
+
const [command, args] = terminal.launch(argv);
|
|
74
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore', env, windowsHide: false });
|
|
75
|
+
child.on('error', () => {});
|
|
76
|
+
child.unref();
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|