@volter/supercode-teams 0.2.177 → 0.2.179
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/bin/teams.mjs +2 -1
- package/machine.mjs +22 -6
- package/package.json +1 -1
- package/workspaces.mjs +27 -8
package/bin/teams.mjs
CHANGED
|
@@ -307,12 +307,13 @@ async function main() {
|
|
|
307
307
|
case 'workspace': {
|
|
308
308
|
if (sub === 'list') return withNode(async (rpc) => out(await rpc.call('workspace.list', {})));
|
|
309
309
|
if (sub === 'register' && rest[0]) return withNode(async (rpc) => out(await rpc.call('workspace.register', { path: rest[0], repository: flags.repo ?? null })));
|
|
310
|
+
if (sub === 'unregister' && rest[0]) return withNode(async (rpc) => out(await rpc.call('workspace.unregister', { path: rest[0] })));
|
|
310
311
|
if (sub === 'resolve' && rest[0]) return withNode(async (rpc) => out(await rpc.call('workspace.resolve', { repository: rest[0], branch: flags.branch ?? null, solo: Boolean(flags.solo), card: flags.card ?? null, create: Boolean(flags.create) })));
|
|
311
312
|
if (sub === 'inspect' && rest[0]) return withNode(async (rpc) => out(await rpc.call('workspace.inspect', { path: rest[0] })));
|
|
312
313
|
// `workspace move --from A --to B --path P [--to-path Q]`: the project leaves A and arrives
|
|
313
314
|
// at B, published there only if every byte arrived (sha256 on commit) and neither machine
|
|
314
315
|
// runs an exec job. A is left as it was; B's path must not exist.
|
|
315
|
-
if (sub !== 'move' || !flags.from || !flags.to || !flags.path) die('workspace move --from MACHINE --to MACHINE --path P [--to-path Q]');
|
|
316
|
+
if (sub !== 'move' || !flags.from || !flags.to || !flags.path) die('workspace list | register PATH [--repo R] | unregister PATH | resolve REPO | inspect PATH | move --from MACHINE --to MACHINE --path P [--to-path Q]');
|
|
316
317
|
try {
|
|
317
318
|
out(await moveWorkspace({ from: String(flags.from), to: String(flags.to), path: String(flags.path), toPath: String(flags['to-path'] ?? flags.path), context: flags.context ? String(flags.context) : null, home }));
|
|
318
319
|
} catch (error) { die(error.message, error.exitCode ?? 1); }
|
package/machine.mjs
CHANGED
|
@@ -1080,6 +1080,16 @@ export class TeamsMachine extends MachineCore {
|
|
|
1080
1080
|
|
|
1081
1081
|
listPorts() { return listAnnouncedPorts(this.portsDir); }
|
|
1082
1082
|
|
|
1083
|
+
/** The attach clients on each pane's tmux session, by its label; null where panes are not tmux's (a ConPTY host). */
|
|
1084
|
+
#viewerCounts() {
|
|
1085
|
+
if (process.platform === 'win32') return null;
|
|
1086
|
+
const listed = spawnSync('tmux', [...(this.tmuxSocket ? ['-L', this.tmuxSocket] : []), 'list-clients', '-F', '#{session_name}'], { timeout: 3_000, encoding: 'utf8' });
|
|
1087
|
+
const counts = new Map();
|
|
1088
|
+
if (listed.status !== 0) return counts; // no server, or no clients: nothing shows any pane
|
|
1089
|
+
for (const name of listed.stdout.split('\n').filter(Boolean)) counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
1090
|
+
return counts;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1083
1093
|
/**
|
|
1084
1094
|
* A terminal window on this machine's screen showing the pane, so the people at the machine see what runs on it:
|
|
1085
1095
|
* in whichever capable terminal is installed (popup-terminals.mjs), running an attach client. The pane ending ends
|
|
@@ -1087,7 +1097,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
1087
1097
|
*/
|
|
1088
1098
|
#popupWindow(contextKey, session) {
|
|
1089
1099
|
const terminal = popupTerminal(this.env);
|
|
1090
|
-
if (!terminal) return;
|
|
1100
|
+
if (!terminal) return false;
|
|
1091
1101
|
const attach = process.platform === 'win32'
|
|
1092
1102
|
? [process.execPath, fileURLToPath(new URL('./bin/fleet.mjs', import.meta.url)), 'attach', contextKey, '--mode', 'control']
|
|
1093
1103
|
// Presented as `supercode open`'s own client is (crates/cli/src/open.rs `attach`): the terminal sets its own
|
|
@@ -1095,7 +1105,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
1095
1105
|
// screen override, so the window's own scrollback, wheel and selection work instead of the wheel arriving
|
|
1096
1106
|
// in the pane as up/down arrows.
|
|
1097
1107
|
: ['/usr/bin/env', 'TERM=screen-256color', spawnSync('which', ['tmux'], { encoding: 'utf8' }).stdout.trim() || 'tmux', ...(this.tmuxSocket ? ['-L', this.tmuxSocket] : []), '-T', 'RGB', 'attach-session', '-t', session.label ?? contextKey];
|
|
1098
|
-
openPopup(terminal, attach, { ...this.env, TMUX: '' });
|
|
1108
|
+
return openPopup(terminal, attach, { ...this.env, TMUX: '' });
|
|
1099
1109
|
}
|
|
1100
1110
|
|
|
1101
1111
|
/**
|
|
@@ -1445,9 +1455,12 @@ export class TeamsMachine extends MachineCore {
|
|
|
1445
1455
|
// hand here: a list that made a caller ask a second door "and is anything
|
|
1446
1456
|
// wrong with these?" would be a list that knows and will not say.
|
|
1447
1457
|
const rows = await this.attentionEvidence();
|
|
1458
|
+
// how many clients each pane has now (a popped window, a `supercode open`, a relay streaming it elsewhere): what a caller must
|
|
1459
|
+
// know before it opens another view of a pane on the same screen
|
|
1460
|
+
const viewers = this.#viewerCounts();
|
|
1448
1461
|
// `title` is what a viewer should show: the override if one is set, else the tmux label.
|
|
1449
1462
|
return rows.filter((row) => row.session.id && shown(row.session.contextKey)).map(({ session: s, attention, rank, notify, evidence }) => ({
|
|
1450
|
-
pane: s.contextKey, dead: Boolean(s.dead), label: s.label, title: titles[`pane:${s.contextKey}`] ?? s.label, cwd: s.cwd, owned: s.owned, activeCommand: s.activeCommand, pid: s.rootPid ?? null, size: s.size, suspended: this.suspended.has(s.contextKey),
|
|
1463
|
+
pane: s.contextKey, dead: Boolean(s.dead), label: s.label, title: titles[`pane:${s.contextKey}`] ?? s.label, cwd: s.cwd, owned: s.owned, activeCommand: s.activeCommand, pid: s.rootPid ?? null, size: s.size, suspended: this.suspended.has(s.contextKey), viewers: viewers ? (viewers.get(s.label) ?? 0) : null,
|
|
1451
1464
|
attention, rank, notify, evidence, input_cleanup: this.inputRepairs[s.contextKey] ? {pending:true,at:this.inputRepairs[s.contextKey].at,reason:this.inputRepairs[s.contextKey].reason} : null,
|
|
1452
1465
|
}));
|
|
1453
1466
|
}
|
|
@@ -1541,9 +1554,11 @@ export class TeamsMachine extends MachineCore {
|
|
|
1541
1554
|
this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, by, ...executable, status, error: exit?.failed ? exit.how : undefined });
|
|
1542
1555
|
return { key, pane: contextKey, reused: false, status, exit: exit?.how ?? null };
|
|
1543
1556
|
}
|
|
1544
|
-
|
|
1557
|
+
// the window this machine's screen shows the pane in, when one opened: the caller is told, so it does not open a
|
|
1558
|
+
// second view of a pane that is already in front of the person at the machine
|
|
1559
|
+
const window = params.popup !== false && this.#popupWindow(contextKey, session);
|
|
1545
1560
|
if (params.notify) this.#noticeWhenEnded({ key, pane: contextKey, notify: params.notify, label: params.name ?? params.kind ?? params.program });
|
|
1546
|
-
return { key, pane: contextKey, reused: false, status: 'ready' };
|
|
1561
|
+
return { key, pane: contextKey, reused: false, status: 'ready', window };
|
|
1547
1562
|
}
|
|
1548
1563
|
case 'launches.get': {
|
|
1549
1564
|
if (!operator) throw Object.assign(new Error('operators only'), { code: 403 });
|
|
@@ -1799,11 +1814,12 @@ export class TeamsMachine extends MachineCore {
|
|
|
1799
1814
|
act('title', params.target, 'ok', renamed);
|
|
1800
1815
|
return { ok: true, target: params.target, title: params.title ?? null, window: renamed };
|
|
1801
1816
|
}
|
|
1802
|
-
case 'workspace.inspect': case 'workspace.register': case 'workspace.list': case 'workspace.resolve': {
|
|
1817
|
+
case 'workspace.inspect': case 'workspace.register': case 'workspace.unregister': case 'workspace.list': case 'workspace.resolve': {
|
|
1803
1818
|
if (!operator) throw Object.assign(new Error('operators only'), { code: 403 });
|
|
1804
1819
|
const registry = new Workspaces(this.home);
|
|
1805
1820
|
const result = method === 'workspace.inspect' ? inspectCheckout(params.path)
|
|
1806
1821
|
: method === 'workspace.register' ? registry.register(params.path, params.repository)
|
|
1822
|
+
: method === 'workspace.unregister' ? registry.unregister(params.path)
|
|
1807
1823
|
: method === 'workspace.list' ? registry.list()
|
|
1808
1824
|
: registry.resolve(params.repository, params.branch, { solo: params.solo, card: params.card, create: params.create });
|
|
1809
1825
|
act('workspace', '*', 'ok', method);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.179",
|
|
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",
|
package/workspaces.mjs
CHANGED
|
@@ -25,18 +25,27 @@ export function inspectCheckout(path) {
|
|
|
25
25
|
}
|
|
26
26
|
export class Workspaces {
|
|
27
27
|
constructor(home) { this.file = join(home, 'workspaces.jsonl'); }
|
|
28
|
-
|
|
28
|
+
// Every registration stands until its checkout is registered again or unregistered; the newest standing row
|
|
29
|
+
// answers each repository and branch (list). Unregistering a registration made by mistake therefore gives back
|
|
30
|
+
// the one it replaced; the removal row stays in the file as the record of that act.
|
|
31
|
+
standing() {
|
|
29
32
|
let text; try { text = readFileSync(this.file, 'utf8'); } catch (e) { if (e.code === 'ENOENT') return []; throw e; }
|
|
30
|
-
|
|
33
|
+
let standing = [];
|
|
31
34
|
for (const line of text.split('\n').filter(Boolean)) {
|
|
32
35
|
let row; try { row = JSON.parse(line); } catch { throw new Error(`invalid machine workspace map ${this.file}`); }
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
else if (row.primary && old.repository === row.repository) old.primary = false;
|
|
36
|
-
}
|
|
37
|
-
map.set(`${row.repository}\n${row.branch ?? ''}`, row);
|
|
36
|
+
standing = standing.filter((old) => old.path !== row.path);
|
|
37
|
+
if (!row.removed) standing.push(row);
|
|
38
38
|
}
|
|
39
|
-
return
|
|
39
|
+
return standing;
|
|
40
|
+
}
|
|
41
|
+
list() {
|
|
42
|
+
const map = new Map();
|
|
43
|
+
for (const row of this.standing()) { const key = `${row.repository}\n${row.branch ?? ''}`; map.delete(key); map.set(key, { ...row }); }
|
|
44
|
+
// A repository's main checkout is its newest registered main checkout that stands.
|
|
45
|
+
const rows = [...map.values()], main = new Map();
|
|
46
|
+
for (const row of rows) if (row.primary) main.set(row.repository, row);
|
|
47
|
+
for (const row of rows) row.primary = main.get(row.repository) === row;
|
|
48
|
+
return rows;
|
|
40
49
|
}
|
|
41
50
|
register(path, repository = null) {
|
|
42
51
|
const info = inspectCheckout(path), id = repositoryId(repository ?? info.repository);
|
|
@@ -46,6 +55,16 @@ export class Workspaces {
|
|
|
46
55
|
appendFileSync(this.file, JSON.stringify(row) + '\n', { mode: 0o600 });
|
|
47
56
|
return row;
|
|
48
57
|
}
|
|
58
|
+
// A registration made by mistake is removed: the checkout at `path` (its root, as registered) leaves the map. The
|
|
59
|
+
// checkout itself is untouched.
|
|
60
|
+
unregister(path) {
|
|
61
|
+
let at = resolve(path);
|
|
62
|
+
try { at = inspectCheckout(path).root; } catch { try { at = realpathSync(at); } catch { /* a path that no longer exists */ } }
|
|
63
|
+
const row = this.standing().findLast(r => r.path === at || r.path === resolve(path));
|
|
64
|
+
if (!row) throw new Error(`${path} is not registered in this machine's workspace map`);
|
|
65
|
+
appendFileSync(this.file, JSON.stringify({ path: row.path, removed: true, repository: row.repository, branch: row.branch }) + '\n', { mode: 0o600 });
|
|
66
|
+
return { ...row, removed: true };
|
|
67
|
+
}
|
|
49
68
|
resolve(repository, branch = null, { solo = false, card = null, create = false } = {}) {
|
|
50
69
|
const id = repositoryId(repository), rows = this.list().filter(r => r.repository === id);
|
|
51
70
|
if (!rows.length) throw new Error(`repository ${id} is missing from this machine's workspace map; register its checkout with teams workspace register <path>`);
|