@volter/supercode-teams 0.3.185 → 0.3.187
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-cli.mjs +608 -0
- package/bin/teams-supervised.mjs +5 -0
- package/bin/teams.mjs +49 -582
- package/bin/workspace/published-install.mjs +46 -0
- package/bin/workspace/release-state.mjs +82 -0
- package/bin/workspace/retain-release.mjs +186 -0
- package/bin/workspace/upgrade-supervisor.mjs +234 -0
- package/bin/workspace/upgrade-watch.mjs +5 -2
- package/bin/workspace.mjs +19 -6
- package/machine.mjs +41 -22
- package/mail/import-maildir.mjs +8 -7
- package/mail/recovery-view.mjs +36 -0
- package/mail/retire-maildir.mjs +57 -68
- package/mail/sqlite-store.mjs +15 -5
- package/mail/upgrade-admission.mjs +44 -0
- package/mail/upgrade-readiness.mjs +59 -0
- package/mail/upgrade-state.mjs +78 -0
- package/package.json +1 -1
package/bin/teams.mjs
CHANGED
|
@@ -1,585 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
import
|
|
35
|
-
|
|
36
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
37
|
-
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
38
|
-
import { noteStep, rotatedLogs, slowLogPath, timeCommand } from '@volter/supercode-harness-sdk/slow-log';
|
|
39
|
-
|
|
40
|
-
import { TeamsMachine } from '../machine.mjs';
|
|
41
|
-
import { harnessTransport, nodeRpc, forwardPort, watchMachine } from '../client.mjs';
|
|
42
|
-
import { attachBrowserProfile, listBrowserProfiles } from '../browser-attach.mjs';
|
|
43
|
-
import { openMachine } from '../machine-door.mjs';
|
|
44
|
-
import { ownClaim } from '../live-sessions.mjs';
|
|
45
|
-
import { moveWorkspace } from '../workspace-move.mjs';
|
|
46
|
-
import { supercodeHome, teamsHome, paths, readJson, AccountLog, parseHostPort } from '@volter/teams/home';
|
|
47
|
-
import { NODE_PROTOCOL } from '@volter/teams/channel';
|
|
48
|
-
import { APP_DOOR_PATH, appDoorFile, appDoorLink, normalOrigin, readAppDoor, readAppDoorState, writeAppDoor } from '../app-door.mjs';
|
|
49
|
-
|
|
50
|
-
const MACHINE_VERBS = new Set(['machine', 'slow', 'describe', 'records', 'panes', 'input', 'suspend', 'resume', 'files', 'ports', 'port', 'browser', 'stream', 'jobs', 'workspace', 'title', 'attention', 'watch', 'rpc', 'launches', 'log', 'mail', 'board', 'health', 'app-door']);
|
|
51
|
-
// Every flag a machine verb reads; any other is refused rather than silently ignored.
|
|
52
|
-
const KNOWN_FLAGS = new Set(['home', 'since', 'machine', 'context', 'floor', 'supercode', 'tmux-socket', 'verbose', 'lines', 'input', 'name', 'for', 'urgency', 'reason', 'interrupt', 'query', 'out', 'in', 'interval', 'no-enter', 'when-composer-empty', 'key', 'listen', 'list', 'from', 'to', 'path', 'to-path', 'repo', 'branch', 'solo', 'card', 'create', 'presence-only', 'processes', 'panes', 'clear-draft', 'message-ids', 'harness', 'observe', 'attach-context', 'json', 'all', 'origin', 'port', 'remove-origin', 'open', 'foreign']);
|
|
53
|
-
|
|
54
|
-
const argv = process.argv.slice(2);
|
|
55
|
-
const flags = {};
|
|
56
|
-
const positional = [];
|
|
57
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
58
|
-
const a = argv[i];
|
|
59
|
-
if (a === '--') { positional.push(...argv.slice(i + 1)); break; }
|
|
60
|
-
if (a.startsWith('--')) {
|
|
61
|
-
const eq = a.indexOf('=');
|
|
62
|
-
if (eq > 0) { flags[a.slice(2, eq)] = a.slice(eq + 1); continue; } // --args=--norc
|
|
63
|
-
const key = a.slice(2);
|
|
64
|
-
const next = argv[i + 1];
|
|
65
|
-
if (next !== undefined && !next.startsWith('--')) { flags[key] = next; i += 1; } else flags[key] = true;
|
|
66
|
-
} else positional.push(a);
|
|
67
|
-
}
|
|
68
|
-
const [verb, sub, ...rest] = positional;
|
|
69
|
-
const migrationRead = verb === 'mail' && sub === 'migration-check';
|
|
70
|
-
// A Windows scheduled task keeps no output (launchd and systemd name log files), so its unit names one here.
|
|
71
|
-
// The local migration preflight does not open the service log or timing log.
|
|
72
|
-
if (!migrationRead && process.env.SUPERCODE_TEAMS_LOG) {
|
|
73
|
-
const log = createWriteStream(process.env.SUPERCODE_TEAMS_LOG, { flags: 'a' });
|
|
74
|
-
for (const stream of [process.stdout, process.stderr]) stream.write = log.write.bind(log);
|
|
75
|
-
// The log is this process's own; a pane or harness it starts writes to its own terminal.
|
|
76
|
-
delete process.env.SUPERCODE_TEAMS_LOG;
|
|
77
|
-
}
|
|
78
|
-
// Each command is timed (the slow log, @volter/supercode-harness-sdk/slow-log); a service, a watch or a forward runs
|
|
79
|
-
// until stopped and is timed one door at a time instead.
|
|
80
|
-
const LONG_RUNNING = new Set(['connect', 'watch', 'port', 'stream', 'attach', 'serve']);
|
|
81
|
-
// A service runs this entry as `teams.mjs teams connect`: the leading `teams` is the entry's own name, not a verb.
|
|
82
|
-
const [timedVerb, timedSub] = verb === 'teams' ? [sub, rest[0]] : [verb, sub];
|
|
83
|
-
if (!migrationRead && !LONG_RUNNING.has(timedVerb) && !(timedVerb === 'machine' && timedSub === 'start') && !(timedVerb === 'message' && timedSub === 'watch')) timeCommand(`teams ${[timedVerb, timedSub].filter(Boolean).join(' ')}`, argv);
|
|
84
|
-
|
|
85
|
-
const MACHINE_HELP = 'machine verbs: machine start|status|restart, slow [--since 1h], describe, panes, input, suspend, resume, files, ports, port, stream, jobs, workspace, title, attention, watch, rpc, launches, log, health, browser, app-door on|off|rotate|status|link (add --machine NAME for an enrolled machine); local read: mail migration-check [--home dir] [--json]';
|
|
86
|
-
|
|
87
|
-
if (verb === undefined || verb === 'help') {
|
|
88
|
-
process.stdout.write(`supercode teams: ${MACHINE_HELP}\n`);
|
|
89
|
-
const { workspaceMain } = await import('./workspace.mjs');
|
|
90
|
-
await workspaceMain(['--help']);
|
|
91
|
-
process.exit(0);
|
|
92
|
-
}
|
|
93
|
-
// A workspace verb is done when its promise settles. On Windows the process then ends on its own: Node there aborts
|
|
94
|
-
// (libuv's `UV_HANDLE_CLOSING` assertion) when process.exit() follows a fetch, which every workspace verb makes.
|
|
95
|
-
const workspaceVerb = !MACHINE_VERBS.has(verb);
|
|
96
|
-
if (workspaceVerb) {
|
|
97
|
-
const { workspaceMain } = await import('./workspace.mjs');
|
|
98
|
-
await workspaceMain(argv).then(() => { process.exitCode = 0; }, (error) => {
|
|
99
|
-
process.stderr.write(JSON.stringify({ version: 1, error: { code: error.code ?? 'invalid_request', message: error.message } }) + '\n');
|
|
100
|
-
process.exitCode = (error.exitCode ?? ({ invalid_request: 2, ambiguous_reference: 2, authentication_required: 3, not_found: 4, forbidden: 4, unsupported: 5, machine_offline: 5, controller_busy: 6, conflict: 6, resync_required: 6, revision_mismatch: 6, idempotency_conflict: 6, batch_gap: 6, capture_conflict: 6, source_paused: 6 }[error.code] ?? 1));
|
|
101
|
-
});
|
|
102
|
-
// Exit only once stdout has drained: a pipe takes 64 KB at a time, and exiting sooner cut a large --json answer off.
|
|
103
|
-
if (process.platform !== 'win32') { await new Promise((resolve) => process.stdout.write('', resolve)); process.exit(); }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// teamsHome() creates the default directory; the preflight only resolves it.
|
|
107
|
-
const home = flags.home ? String(flags.home) : migrationRead
|
|
108
|
-
? process.env.SUPERCODE_TEAMS_HOME ?? join(supercodeHome(), 'teams') : teamsHome();
|
|
109
|
-
const p = paths(home);
|
|
110
|
-
const out = (value) => process.stdout.write((typeof value === 'string' ? value : JSON.stringify(value, null, 2)) + '\n');
|
|
111
|
-
const die = (message, code = 2) => { process.stderr.write(`supercode teams: ${message}\n`); process.exit(code); };
|
|
112
|
-
if (!workspaceVerb) for (const flag of Object.keys(flags)) if (!KNOWN_FLAGS.has(flag)) die(`unknown flag: --${flag}`);
|
|
113
|
-
|
|
114
|
-
/** Named keys (tmux's names) as the bytes a terminal sends for them: `panes.input` delivers
|
|
115
|
-
* them (`keys`) as a keystream, which every terminal host turns back into keys. */
|
|
116
|
-
const KEY_BYTES = { Escape: '\u001b', Enter: '\r', Tab: '\t', BTab: '\u001b[Z', BSpace: '\u007f', Space: ' ', Up: '\u001b[A', Down: '\u001b[B', Right: '\u001b[C', Left: '\u001b[D', Home: '\u001b[H', End: '\u001b[F', PPage: '\u001b[5~', NPage: '\u001b[6~', DC: '\u001b[3~' };
|
|
117
|
-
function keyBytes(names) {
|
|
118
|
-
return names.split(/[\s,]+/u).filter(Boolean).map((name) => {
|
|
119
|
-
const control = /^C-([a-z])$/iu.exec(name);
|
|
120
|
-
if (control) return String.fromCharCode(control[1].toLowerCase().charCodeAt(0) - 96);
|
|
121
|
-
if (!Object.hasOwn(KEY_BYTES, name)) die(`unknown key ${name}: one of ${Object.keys(KEY_BYTES).join(', ')}, C-a … C-z`);
|
|
122
|
-
return KEY_BYTES[name];
|
|
123
|
-
}).join('');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async function machineChannel() {
|
|
127
|
-
const started = performance.now();
|
|
128
|
-
try { return await openMachine({ machine: flags.machine ? String(flags.machine) : null, context: flags.context ? String(flags.context) : null, home }); }
|
|
129
|
-
finally { noteStep(flags.machine ? `open machine ${flags.machine}` : 'open the local machine', performance.now() - started); }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Who this command acts for, so the pane doors can refuse a session acting on a pane it did not start. To this machine's
|
|
133
|
-
// daemon, its claim (its pid, its parent, the session in its environment, a name it acts for), which the daemon holds to
|
|
134
|
-
// its live-session map; to another machine, the caller this machine's daemon resolves from that claim, once per command.
|
|
135
|
-
let actingAnswer;
|
|
136
|
-
async function acting() {
|
|
137
|
-
if (!flags.machine) return { claim: ownClaim() };
|
|
138
|
-
if (actingAnswer === undefined) {
|
|
139
|
-
const local = await openMachine({ machine: null, home });
|
|
140
|
-
try {
|
|
141
|
-
const answer = await (await nodeRpc(local)).call('harness.v1.caller', { claim: ownClaim() });
|
|
142
|
-
actingAnswer = answer?.caller ?? null;
|
|
143
|
-
} catch (error) {
|
|
144
|
-
die(`cannot tell which session runs this, so the pane doors cannot hold it to the panes it started; nothing was done (${String(error.message).slice(0, 200)})`, 1);
|
|
145
|
-
} finally { local.close(); }
|
|
146
|
-
}
|
|
147
|
-
return actingAnswer ? { caller: actingAnswer } : {};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function withNode(fn) {
|
|
151
|
-
const channel = await machineChannel();
|
|
152
|
-
try {
|
|
153
|
-
const rpc = await nodeRpc(channel);
|
|
154
|
-
return await fn(rpc, channel);
|
|
155
|
-
} finally {
|
|
156
|
-
channel.close();
|
|
2
|
+
// Keep the service bootstrap independent of candidate daemon modules. An npm
|
|
3
|
+
// replacement that deferred activation must still reach its retained release
|
|
4
|
+
// when the service manager next starts this canonical entry.
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { delimiter, dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2), verbs = args[0] === 'teams' ? args.slice(1) : args;
|
|
12
|
+
const starts = (verbs[0] === 'connect' && !verbs.includes('--install') && !verbs.includes('--add-root'))
|
|
13
|
+
|| (verbs[0] === 'machine' && verbs[1] === 'start');
|
|
14
|
+
const json = async path => { try { return JSON.parse(await readFile(path, 'utf8')); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } };
|
|
15
|
+
let installed = null;
|
|
16
|
+
for (let parent = dirname(fileURLToPath(import.meta.url)); dirname(parent) !== parent; parent = dirname(parent)) {
|
|
17
|
+
if ((await json(join(parent, 'package.json')))?.name === '@volter/supercode') { installed = await json(join(parent, 'package.json')); break; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (starts && installed && !process.env.SUPERCODE_UPGRADE_SUPERVISOR) {
|
|
21
|
+
const at = args.indexOf('--home'), inline = args.find(arg => arg.startsWith('--home='));
|
|
22
|
+
const home = resolve(at >= 0 ? args[at + 1] : inline ? inline.slice(7) : process.env.SUPERCODE_TEAMS_HOME
|
|
23
|
+
|| join(process.env.SUPERCODE_HOME || join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'supercode'), 'teams'));
|
|
24
|
+
const history = await json(join(home, 'service', 'release-history.json'));
|
|
25
|
+
let release = history?.candidate;
|
|
26
|
+
const fallbackController = history?.refused?.supports_ready ? history.refused : release;
|
|
27
|
+
const controller = release?.supports_ready && release.native !== history?.previous?.native ? release : fallbackController;
|
|
28
|
+
if (!release?.supervised) {
|
|
29
|
+
// Retaining packages and fetching a predecessor belong to installation,
|
|
30
|
+
// never first serving. With no history, use this installed release and
|
|
31
|
+
// keep original mail readable if migration is refused.
|
|
32
|
+
process.env.SUPERCODE_MAIL_STARTUP_RECOVERY = '1';
|
|
33
|
+
process.env.SUPERCODE_RELEASE_VERSION = installed.version;
|
|
34
|
+
await import('./teams-cli.mjs');
|
|
35
|
+
release = null;
|
|
157
36
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (sub === 'start') {
|
|
173
|
-
const machine = new TeamsMachine({
|
|
174
|
-
home,
|
|
175
|
-
floor: flags.floor ? Number(flags.floor) : null,
|
|
176
|
-
supercode: flags.supercode ?? 'supercode',
|
|
177
|
-
// A named tmux server keeps a daemon's panes off the default socket.
|
|
178
|
-
...(flags['tmux-socket'] ? { tmuxSocket: String(flags['tmux-socket']) } : {}),
|
|
179
|
-
});
|
|
180
|
-
// --verbose narrates every door with its latency, so a slow machine is diagnosed from its own log.
|
|
181
|
-
machine.on('door', (d) => { if (flags.verbose) process.stderr.write(`door ${d.method} ${d.outcome} ${d.ms}ms${d.detail ? ` ${d.detail}` : ''}\n`); });
|
|
182
|
-
machine.on('serve-stderr', (t) => process.stderr.write(t));
|
|
183
|
-
machine.on('mail-log', (line) => process.stderr.write(`mail: ${line}\n`));
|
|
184
|
-
machine.on('records-log', (line) => process.stderr.write(`records: ${line}\n`));
|
|
185
|
-
machine.on('serve-exit', (e) => process.stderr.write(`harness serve exited: ${JSON.stringify(e)}\n`));
|
|
186
|
-
machine.on('unrouted-notification', (m) => { if (flags.verbose) process.stderr.write(`unrouted: ${m.method}\n`); });
|
|
187
|
-
const socket = await machine.start();
|
|
188
|
-
out({ id: machine.id, socket, home, protocol: NODE_PROTOCOL, floor: machine.floor });
|
|
189
|
-
const stop = async () => { await machine.stop(); process.exit(0); };
|
|
190
|
-
// Another daemon took the socket path: this one serves nobody, and its children would run beside that one's.
|
|
191
|
-
machine.once('superseded', (path) => { process.stderr.write(`supercode teams: another machine daemon now serves ${path}; this one stops\n`); void stop(); });
|
|
192
|
-
process.on('SIGINT', stop);
|
|
193
|
-
process.on('SIGTERM', stop);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (sub === 'status') {
|
|
197
|
-
// and every supercode process here still running a release an install replaced (t_8e109edc)
|
|
198
|
-
let stale_releases = null;
|
|
199
|
-
try { stale_releases = await (await import('@volter/supercode-health/drift')).staleSupercode(); } catch { /* not read */ }
|
|
200
|
-
out({ ...readJson(p.machine, { running: false }), stale_releases }); return;
|
|
201
|
-
}
|
|
202
|
-
// `machine restart [--machine M] [--reason text]`: the daemon (this one, or M's through the server) restarts under its service.
|
|
203
|
-
if (sub === 'restart') return withNode(async (rpc) => out(await rpc.call('machine.restart', { reason: flags.reason ? String(flags.reason) : null })));
|
|
204
|
-
// `machine boards [list]` and `machine boards unregister <root>`: the board homes this machine's connector follows
|
|
205
|
-
// and publishes, and one no longer followed (a proof's scratch home: t_f7b219b8)
|
|
206
|
-
if (sub === 'boards') {
|
|
207
|
-
const [action = 'list', root] = rest;
|
|
208
|
-
if (action === 'list') return withNode(async (rpc) => out(await rpc.call('harness.v1.boards.list', {})));
|
|
209
|
-
if (action === 'unregister' && root) return withNode(async (rpc) => out(await rpc.call('harness.v1.boards.unregister', { root: resolve(root) })));
|
|
210
|
-
die('machine boards [list] | machine boards unregister <root>');
|
|
211
|
-
}
|
|
212
|
-
// `machine pane-of <pid>`: the pane this daemon launched that the process runs in, and who launched it, from the
|
|
213
|
-
// daemon's own record (the native's acting caller asks it for its own pid; never what a command sets for itself)
|
|
214
|
-
if (sub === 'pane-of') return withNode(async (rpc) => out(await rpc.call('harness.v1.pane_of', { pid: Number(rest[0]) })));
|
|
215
|
-
die('machine start|status|restart|boards|pane-of');
|
|
216
|
-
}
|
|
217
|
-
case 'slow': {
|
|
218
|
-
// `slow [--since 1h|30m|<iso>]`: the slow log on this machine, grouped by what was slow (kind and name), the
|
|
219
|
-
// slowest first, each with its count, median and longest time and its usual slowest step. The audit reads this.
|
|
220
|
-
const since = (() => {
|
|
221
|
-
const value = String(flags.since ?? '24h');
|
|
222
|
-
const relative = /^(\d+)([smhd])$/.exec(value);
|
|
223
|
-
if (relative) return Date.now() - Number(relative[1]) * { s: 1e3, m: 6e4, h: 36e5, d: 864e5 }[relative[2]];
|
|
224
|
-
const at = Date.parse(value);
|
|
225
|
-
if (Number.isNaN(at)) die('slow --since takes 30m, 2h, 1d or a date');
|
|
226
|
-
return at;
|
|
227
|
-
})();
|
|
228
|
-
let text = '';
|
|
229
|
-
for (const file of [...rotatedLogs(), slowLogPath()]) {
|
|
230
|
-
try { text += readFileSync(file, 'utf8'); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
231
|
-
}
|
|
232
|
-
const groups = new Map();
|
|
233
|
-
for (const line of text.split('\n')) {
|
|
234
|
-
let row; try { row = JSON.parse(line); } catch { continue; }
|
|
235
|
-
if (!(Date.parse(row.at) >= since)) continue;
|
|
236
|
-
const key = `${row.kind} ${row.name}`;
|
|
237
|
-
const group = groups.get(key) ?? { kind: row.kind, name: row.name, count: 0, ms: [], steps: new Map(), last: null };
|
|
238
|
-
group.count += 1; group.ms.push(row.ms); group.last = row.at;
|
|
239
|
-
if (row.slowest?.step) group.steps.set(row.slowest.step, (group.steps.get(row.slowest.step) ?? 0) + 1);
|
|
240
|
-
groups.set(key, group);
|
|
241
|
-
}
|
|
242
|
-
out([...groups.values()].map((group) => {
|
|
243
|
-
const sorted = group.ms.sort((a, b) => a - b);
|
|
244
|
-
const step = [...group.steps].sort((a, b) => b[1] - a[1])[0];
|
|
245
|
-
return { kind: group.kind, name: group.name, count: group.count, median_ms: sorted[Math.floor(sorted.length / 2)], max_ms: sorted.at(-1), usual_slowest_step: step ? step[0] : null, last: group.last };
|
|
246
|
-
}).sort((a, b) => b.max_ms * b.count - a.max_ms * a.count));
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
case 'describe':
|
|
250
|
-
return withNode(async (rpc) => out(await rpc.call('describe')));
|
|
251
|
-
case 'panes': {
|
|
252
|
-
// `--processes`: each pane and its root pid (no screens read); `--presence-only [--panes a,b]`: whether each pane's
|
|
253
|
-
// composer is idle, for the named panes only when named.
|
|
254
|
-
if (sub === 'ls') return withNode(async (rpc) => out(await rpc.call(
|
|
255
|
-
flags.processes ? 'panes.processes' : flags['presence-only'] ? 'panes.present' : 'panes.list',
|
|
256
|
-
flags['presence-only'] && typeof flags.panes === 'string' ? { panes: flags.panes.split(',').filter(Boolean) } : {})));
|
|
257
|
-
if (sub === 'capture') return withNode(async (rpc) => out((await rpc.call('panes.capture', { pane: rest[0], lines: Number(flags.lines ?? 200) })).text));
|
|
258
|
-
// One pane, named: `--foreign` closes a pane another session started, and is said for that pane alone.
|
|
259
|
-
if (sub === 'close') {
|
|
260
|
-
// the flag parser takes `--foreign p_…` as the flag's value: that pane is the one named
|
|
261
|
-
const named = typeof flags.foreign === 'string' ? [flags.foreign, ...rest] : rest;
|
|
262
|
-
if (named.length !== 1) die('panes close <pane> [--foreign]: one pane per close');
|
|
263
|
-
return withNode(async (rpc) => out(await rpc.call('panes.close', { pane: named[0], foreign: flags.foreign !== undefined, ...(await acting()), ...(flags.foreign ? { foreign: true } : {}) })));
|
|
264
|
-
}
|
|
265
|
-
// `window <pane> [machine]`: a terminal window on this machine's screen showing the pane, this machine's or, named,
|
|
266
|
-
// another machine's (attached through Teams, in that machine's Teams context `--attach-context`); `--observe` shows it
|
|
267
|
-
// read-only.
|
|
268
|
-
if (sub === 'window') return withNode(async (rpc) => out(await rpc.call('panes.window', { pane: rest[0], machine: rest[1] ?? null, mode: flags.observe ? 'observe' : 'control', context: typeof flags['attach-context'] === 'string' ? flags['attach-context'] : null })));
|
|
269
|
-
die('panes ls|capture|close|window');
|
|
270
|
-
}
|
|
271
|
-
case 'title': {
|
|
272
|
-
if (sub === 'set') return withNode(async (rpc) => out(await rpc.call('title.set', { target: `pane:${rest[0]}`, title: rest.slice(1).join(' ') || null })));
|
|
273
|
-
if (sub === 'ls' || sub === undefined) return withNode(async (rpc) => out(await rpc.call('title.list')));
|
|
274
|
-
die('title set <pane> <text> | title ls');
|
|
275
|
-
}
|
|
276
|
-
case 'attention': {
|
|
277
|
-
// The machine computes the label; this prints it. Sorted by rank so the most
|
|
278
|
-
// urgent pane is the first line, which is the only ordering a person
|
|
279
|
-
// reading a fleet list ever wants.
|
|
280
|
-
if (sub === 'ls' || sub === undefined) {
|
|
281
|
-
return withNode(async (rpc) => {
|
|
282
|
-
const rows = await rpc.call('attention.snapshot');
|
|
283
|
-
out([...rows].sort((a, b) => a.rank - b.rank));
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
if (sub === 'rank') return withNode(async (rpc) => out(await rpc.call('attention.rank')));
|
|
287
|
-
if (sub === 'request') return withNode(async (rpc) => out(await rpc.call('attention.request', { pane: rest[0], urgency: flags.urgency ? String(flags.urgency) : undefined, reason: flags.reason ? String(flags.reason) : null })));
|
|
288
|
-
if (sub === 'clear') return withNode(async (rpc) => out(await rpc.call('attention.clear', { pane: rest[0] })));
|
|
289
|
-
die('attention ls | rank | request <pane> [--urgency now|soon|whenever] [--reason text] | clear <pane>');
|
|
290
|
-
}
|
|
291
|
-
case 'suspend':
|
|
292
|
-
return withNode(async (rpc) => out(await rpc.call('panes.suspend', { pane: sub, interrupt: Boolean(flags.interrupt), ...(await acting()), ...(flags.foreign ? { foreign: true } : {}) })));
|
|
293
|
-
case 'resume':
|
|
294
|
-
return withNode(async (rpc) => out(await rpc.call('panes.resume', { pane: sub, input: flags.input ? String(flags.input) : null, ...(await acting()), ...(flags.foreign ? { foreign: true } : {}) })));
|
|
295
|
-
case 'files': {
|
|
296
|
-
const path = rest[0];
|
|
297
|
-
if (sub === 'ls') return withNode(async (rpc) => out(await rpc.call('files.list', { path })));
|
|
298
|
-
if (sub === 'stat') return withNode(async (rpc) => out(await rpc.call('files.stat', { path })));
|
|
299
|
-
if (sub === 'search') return withNode(async (rpc) => out(await rpc.call('files.search', { path, query: flags.query ?? '' })));
|
|
300
|
-
if (sub === 'get') return withNode(async (rpc) => {
|
|
301
|
-
// In ranges: one answer is one frame, so a file of any size is read 4 MiB at a time until it ends.
|
|
302
|
-
const { size } = await rpc.call('files.stat', { path });
|
|
303
|
-
const CHUNK = 4 * 1024 * 1024;
|
|
304
|
-
if (flags.out) writeFileSync(flags.out, Buffer.alloc(0));
|
|
305
|
-
for (let offset = 0; offset < size;) {
|
|
306
|
-
const result = await rpc.call('files.read', { path, offset, length: Math.min(CHUNK, size - offset) });
|
|
307
|
-
const bytes = Buffer.from(result.bytes, 'base64');
|
|
308
|
-
if (!bytes.length) break;
|
|
309
|
-
if (flags.out) appendFileSync(flags.out, bytes); else await new Promise((done) => process.stdout.write(bytes, done));
|
|
310
|
-
offset += bytes.length;
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
if (sub === 'put') return withNode(async (rpc) => {
|
|
314
|
-
const bytes = flags.in ? readFileSync(flags.in) : readFileSync(0);
|
|
315
|
-
out(await rpc.call('files.write', { path, bytes: bytes.toString('base64'), truncate: true }));
|
|
316
|
-
});
|
|
317
|
-
die('files ls|stat|search|get|put <path>');
|
|
318
|
-
}
|
|
319
|
-
case 'ports': {
|
|
320
|
-
if (sub === 'ls' || sub === undefined) return withNode(async (rpc) => out(await rpc.call('ports.list')));
|
|
321
|
-
if (sub === 'resolve') return withNode(async (rpc) => out(await rpc.call('ports.resolve', { pane: rest[0] })));
|
|
322
|
-
die('ports ls|resolve <pane>');
|
|
323
|
-
}
|
|
324
|
-
case 'watch': {
|
|
325
|
-
// `watch [--interval ms]`: the machine's live state as JSON lines — one snapshot, then deltas — until ctrl-c.
|
|
326
|
-
const channel = await machineChannel();
|
|
327
|
-
const w = await watchMachine(channel, { intervalMs: Number(flags.interval ?? 1000) });
|
|
328
|
-
w.on('snapshot', (s) => out(JSON.stringify(s)));
|
|
329
|
-
w.on('delta', (d) => out(JSON.stringify(d)));
|
|
330
|
-
w.on('error-message', (m) => process.stderr.write(`[watch: ${m}]\n`));
|
|
331
|
-
w.on('close', (reason) => { process.stderr.write(`[watch ended${reason ? `: ${reason}` : ''}]\n`); channel.close(); process.exit(0); });
|
|
332
|
-
if (typeof flags.for === 'string') setTimeout(() => { w.close(); }, Number(flags.for));
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
case 'input':
|
|
336
|
-
// `input <pane> <text>`: one delivery through the host's own sendInput; gated like control.
|
|
337
|
-
// `--when-composer-empty`: a user's own turn, typed only into an empty composer
|
|
338
|
-
// (answers `delivered: false` with the reason when it holds a draft).
|
|
339
|
-
// `--harness NAME` (with it): the addressed session's harness; the daemon refuses a pane running another program.
|
|
340
|
-
if (flags['clear-draft']) {
|
|
341
|
-
if (rest.length || flags.key || flags['no-enter'] || flags['when-composer-empty']) die('input <pane> --clear-draft only clears the current draft; send a new message separately');
|
|
342
|
-
return withNode(async rpc => out(await rpc.call('panes.clearDraft', { pane: sub, ...(await acting()), ...(flags.foreign ? { foreign: true } : {}) })));
|
|
343
|
-
}
|
|
344
|
-
if (flags.key !== undefined) {
|
|
345
|
-
if (flags.key === true || rest.length) die('input <pane> --key NAMES: keys by tmux name, space- or comma-separated (Escape, Enter, Tab, BTab, BSpace, Space, Up, Down, Left, Right, Home, End, PPage, NPage, DC, C-a … C-z)');
|
|
346
|
-
return withNode(async (rpc) => out(await rpc.call('panes.input', { pane: sub, text: keyBytes(String(flags.key)), submit: false, keys: true, ...(await acting()), ...(flags.foreign ? { foreign: true } : {}) })));
|
|
347
|
-
}
|
|
348
|
-
if (flags['when-composer-empty']) return withNode(async (rpc) => out(await rpc.call('panes.turn', { pane: sub, text: rest.join(' '), message_ids: flags['message-ids'] ? String(flags['message-ids']).split(',') : undefined, harness: typeof flags.harness === 'string' ? flags.harness : undefined, ...(await acting()), ...(flags.foreign ? { foreign: true } : {}) })));
|
|
349
|
-
return withNode(async (rpc) => out(await rpc.call('panes.input', { pane: sub, text: rest.join(' '), submit: !flags['no-enter'], message_ids: flags['message-ids'] ? String(flags['message-ids']).split(',') : undefined, ...(await acting()), ...(flags.foreign ? { foreign: true } : {}) })));
|
|
350
|
-
case 'port': {
|
|
351
|
-
// `port <pane>/<n> [--listen host:port]`: a machine port forwarded here. Stays up until ctrl-c.
|
|
352
|
-
const spec = String(sub ?? '');
|
|
353
|
-
const slash = spec.lastIndexOf('/');
|
|
354
|
-
const pane = slash > 0 ? spec.slice(0, slash) : null;
|
|
355
|
-
const portNumber = Number(slash >= 0 ? spec.slice(slash + 1) : spec);
|
|
356
|
-
if (!Number.isInteger(portNumber)) die('port <pane>/<n> [--listen host:port]');
|
|
357
|
-
const channel = await machineChannel();
|
|
358
|
-
const fwd = await forwardPort(channel, { pane, port: portNumber, listen: parseHostPort(flags.listen) });
|
|
359
|
-
out({ local: fwd.address, remote: `${pane ?? '*'}/${portNumber}` });
|
|
360
|
-
const stop = async () => { await fwd.close(); channel.close(); process.exit(0); };
|
|
361
|
-
process.on('SIGINT', stop);
|
|
362
|
-
process.on('SIGTERM', stop);
|
|
363
|
-
// The machine's channel gone, no connection can ride it: the forward ends, saying so, for whatever keeps it to start
|
|
364
|
-
// it again (it stayed listening and refused every connection until one refusal crashed it).
|
|
365
|
-
channel.once('close', async () => { process.stderr.write("supercode teams port: the machine's channel closed\n"); await fwd.close(); process.exit(1); });
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
case 'browser': {
|
|
369
|
-
// `browser <profile> [--listen host:port]`: a machine's browser profile as a local CDP
|
|
370
|
-
// endpoint (connectOverCDP to the printed address). Stays up until ctrl-c.
|
|
371
|
-
if (flags.list) {
|
|
372
|
-
const channel = await machineChannel();
|
|
373
|
-
try { out(await listBrowserProfiles(channel)); } finally { channel.close(); }
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
if (!sub) die('browser <profile> [--listen host:port] [--machine NAME] | browser --list [--machine NAME]');
|
|
377
|
-
const attached = await attachBrowserProfile(() => machineChannel(), { profile: String(sub), listen: parseHostPort(flags.listen) });
|
|
378
|
-
out({ cdp: attached.address, target: `browser-profile:${sub}` });
|
|
379
|
-
const stop = async () => { await attached.close(); process.exit(0); };
|
|
380
|
-
process.on('SIGINT', stop);
|
|
381
|
-
process.on('SIGTERM', stop);
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
case 'stream': {
|
|
385
|
-
// `stream exec|sftp [--machine NAME]`: this process's stdin and stdout carried to one of the
|
|
386
|
-
// machine's execution streams, the transport a pilot's exec client and SSHFS (passive) use.
|
|
387
|
-
if (sub !== 'exec' && sub !== 'sftp') die('stream exec|sftp [--machine NAME]');
|
|
388
|
-
const channel = await machineChannel();
|
|
389
|
-
let stream;
|
|
390
|
-
try { stream = await channel.open(sub, {}); } catch (error) { die(error.message, 1); }
|
|
391
|
-
stream.on('data', (chunk) => process.stdout.write(Buffer.from(chunk, 'base64')));
|
|
392
|
-
stream.on('close', () => { channel.close(); process.stdout.end(() => process.exit(0)); });
|
|
393
|
-
process.stdin.on('data', (bytes) => { if (!stream.closed) stream.write(bytes.toString('base64')); });
|
|
394
|
-
process.stdin.on('end', () => stream.close('input ended'));
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
case 'mail': {
|
|
398
|
-
if (sub === 'migration-check') {
|
|
399
|
-
if (Object.keys(flags).some((flag) => !['home', 'json'].includes(flag)) || rest.length || flags.home === true) die('mail migration-check is a local read and takes only --home and --json');
|
|
400
|
-
const { inspectLegacyMaildir } = await import('../mail/migration-readiness.mjs');
|
|
401
|
-
const result = await inspectLegacyMaildir(join(dirname(resolve(home)), 'mail'));
|
|
402
|
-
out(result); process.exitCode = result.admitted ? 0 : 1; return;
|
|
403
|
-
}
|
|
404
|
-
// `mail --machine NAME`: one `supercode message` request (JSON on stdin) to that
|
|
405
|
-
// machine's mail door, and its answer (one JSON line) on stdout.
|
|
406
|
-
const chunks = [];
|
|
407
|
-
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
408
|
-
let request;
|
|
409
|
-
try { request = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { die('mail reads one JSON request on stdin'); }
|
|
410
|
-
const channel = await machineChannel();
|
|
411
|
-
let stream;
|
|
412
|
-
// A typed line waits as long as the server retries it on a loaded machine (2 minutes), and a little longer.
|
|
413
|
-
const timeoutMs = request?.op === 'turn' ? 150_000 : undefined;
|
|
414
|
-
try { stream = await channel.open('mail', { request }, { timeoutMs }); } catch (error) { die(error.message, 1); }
|
|
415
|
-
let answered = false;
|
|
416
|
-
stream.on('data', (chunk) => { answered = true; process.stdout.write(`${chunk}\n`); });
|
|
417
|
-
stream.on('close', (reason) => {
|
|
418
|
-
channel.close();
|
|
419
|
-
if (!answered) process.stdout.write(`${JSON.stringify({ outcome: 'failed', detail: String(reason ?? 'the mail door closed without answering') })}\n`);
|
|
420
|
-
process.exit(0);
|
|
421
|
-
});
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
case 'board': {
|
|
425
|
-
// `board`: one `supercode workflow` verb for a board, `{board, argv}` as JSON on stdin, to this machine's board door,
|
|
426
|
-
// which names the calling session and reaches the machine the board's address names. Never another machine's door
|
|
427
|
-
// directly: that machine cannot name the caller, so it refuses every verb. The answer, `{code, stdout, stderr}`, is
|
|
428
|
-
// one JSON line.
|
|
429
|
-
if (flags.machine) die('board takes no --machine: the board address names its machine, and this machine\'s door names you and reaches it');
|
|
430
|
-
const chunks = [];
|
|
431
|
-
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
432
|
-
let request;
|
|
433
|
-
try { request = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { die('board reads one JSON request on stdin'); }
|
|
434
|
-
// the caller is this process's own claim, never one the request carries
|
|
435
|
-
request = { ...request, claim: ownClaim() };
|
|
436
|
-
const channel = await machineChannel();
|
|
437
|
-
let stream;
|
|
438
|
-
try { stream = await channel.open('board', { request }, { timeoutMs: 600_000 }); } catch (error) { die(error.message, 1); }
|
|
439
|
-
let answer = '';
|
|
440
|
-
stream.on('data', (chunk) => { answer += chunk; });
|
|
441
|
-
stream.on('close', (reason) => {
|
|
442
|
-
channel.close();
|
|
443
|
-
// Exit once the answer is written: a pipe takes 64 KiB at a time, and an exit before then drops the rest.
|
|
444
|
-
// the door accepted the verb, then closed with no answer: whether it ran is unknown (`unanswered`), never a plain failure
|
|
445
|
-
process.stdout.write(`${answer.trim() || JSON.stringify({ code: 1, stdout: '', stderr: String(reason ?? 'the board door closed without answering'), unanswered: true })}\n`, () => process.exit(0));
|
|
446
|
-
});
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
case 'workspace': {
|
|
450
|
-
if (sub === 'list') return withNode(async (rpc) => out(await rpc.call('harness.v1.workspaces.list', {})));
|
|
451
|
-
if (sub === 'register' && rest[0]) return withNode(async (rpc) => out(await rpc.call('harness.v1.workspaces.register', { path: rest[0], repository: flags.repo ?? null })));
|
|
452
|
-
if (sub === 'unregister' && rest[0]) return withNode(async (rpc) => out(await rpc.call('harness.v1.workspaces.unregister', { path: rest[0] })));
|
|
453
|
-
if (sub === 'resolve' && rest[0]) return withNode(async (rpc) => out(await rpc.call('harness.v1.workspaces.resolve', { repository: rest[0], branch: flags.branch ?? null, solo: Boolean(flags.solo), card: flags.card ?? null, create: Boolean(flags.create) })));
|
|
454
|
-
if (sub === 'inspect' && rest[0]) return withNode(async (rpc) => out(await rpc.call('harness.v1.workspaces.inspect', { path: rest[0] })));
|
|
455
|
-
// `workspace move --from A --to B --path P [--to-path Q]`: the project leaves A and arrives
|
|
456
|
-
// at B, published there only if every byte arrived (sha256 on commit) and neither machine
|
|
457
|
-
// runs an exec job. A is left as it was; B's path must not exist.
|
|
458
|
-
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]');
|
|
459
|
-
try {
|
|
460
|
-
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 }));
|
|
461
|
-
} catch (error) { die(error.message, error.exitCode ?? 1); }
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
case 'jobs':
|
|
465
|
-
// `jobs [--machine NAME]`: the execution jobs the machine's daemon owns now.
|
|
466
|
-
return withNode(async (rpc) => out(await rpc.call('jobs.list', {})));
|
|
467
|
-
case 'launches': {
|
|
468
|
-
if (sub === 'get' && rest[0]) return withNode(async (rpc) => out(await rpc.call('launches.get', { key: rest[0] })));
|
|
469
|
-
if (sub === 'cancel' && rest[0]) return withNode(async (rpc) => out(await rpc.call('launches.cancel', { key: rest[0], ...(await acting()), ...(flags.foreign ? { foreign: true } : {}) })));
|
|
470
|
-
if (sub === 'list') return withNode(async (rpc) => out(await rpc.call('launches.list', {})));
|
|
471
|
-
die('launches get <key> | cancel <key> | list [--machine NAME]');
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
case 'health': {
|
|
475
|
-
// This machine's vitals are read from the pack's own files, not through the daemon: the reading has to
|
|
476
|
-
// work when the daemon is what is sick. `--machine NAME` reads an enrolled machine through its doors.
|
|
477
|
-
const local = !flags.machine;
|
|
478
|
-
if (local || ['run', 'once', 'config'].includes(sub)) {
|
|
479
|
-
if (!local) die(`health ${sub} runs on the machine itself; read another machine with health status --machine NAME`);
|
|
480
|
-
const entry = await healthPackEntry();
|
|
481
|
-
if (!entry) die('the machine-health pack (@volter/supercode-health) is not installed; set SUPERCODE_HEALTH_ENTRY to its bin/health.mjs', 1);
|
|
482
|
-
const r = spawnSync(process.execPath, [entry, ...(sub ? [sub] : []), ...rest, ...(flags.json ? ['--json'] : []), ...(flags.all ? ['--all'] : [])], { stdio: 'inherit' });
|
|
483
|
-
process.exit(r.status ?? 1);
|
|
484
|
-
}
|
|
485
|
-
if (sub === 'subscribe' || sub === 'unsubscribe') {
|
|
486
|
-
if (!rest[0]) die(`health ${sub} <sc:machine:harness:id> --machine NAME`);
|
|
487
|
-
return withNode(async (rpc) => out(await rpc.call(`health.${sub}`, { address: rest[0] })));
|
|
488
|
-
}
|
|
489
|
-
if (sub && !['status', 'features', 'alarms'].includes(sub)) die('health [status|features|alarms] [--machine NAME] | health subscribe|unsubscribe <address> | health run|once|config');
|
|
490
|
-
return withNode(async (rpc) => {
|
|
491
|
-
const answer = await rpc.call('health.read', {});
|
|
492
|
-
const s = answer.snapshot;
|
|
493
|
-
const entry = await healthPackEntry();
|
|
494
|
-
const fmt = entry ? await import(new URL('../lib/format.mjs', pathToFileURL(entry)).href).catch(() => null) : null;
|
|
495
|
-
if (flags.json || !fmt) { out(sub === 'features' ? s?.features ?? null : sub === 'alarms' ? s?.alarms ?? null : answer); return; }
|
|
496
|
-
if (sub === 'features') { out(s ? fmt.formatFeatures(s.features) : 'no reading yet'); return; }
|
|
497
|
-
if (sub === 'alarms') { out(s?.alarms?.length ? s.alarms.map((a) => `${a.level.toUpperCase()} ${a.key}: ${a.summary} (since ${a.since})`).join('\n') : 'no alarms raised'); return; }
|
|
498
|
-
out(fmt.formatStatus(s, { all: Boolean(flags.all) }));
|
|
499
|
-
if (!answer.probe?.running) out(`probe: not run by that machine's daemon (${answer.probe?.reason ?? 'unknown'})`);
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
case 'records':
|
|
503
|
-
// this machine's record and the records its daemon holds of the machines it follows, each with how current it is
|
|
504
|
-
// (order 3 PR 3): what `message list`, `show` and the board read instead of asking those machines
|
|
505
|
-
return withNode(async (rpc) => out(await rpc.call('machine.records', {})));
|
|
506
|
-
case 'rpc': {
|
|
507
|
-
if (!sub) die('rpc <method> [JSON params] [--machine NAME]');
|
|
508
|
-
const method = sub.startsWith('harness.v1.') ? sub : `harness.v1.${sub}`;
|
|
509
|
-
const params = rest[0] ? JSON.parse(rest[0]) : {};
|
|
510
|
-
const channel = await machineChannel();
|
|
511
|
-
// Mail operations were ported to the node door. Keep their operator CLI
|
|
512
|
-
// on that door; native sessions.* requests retain the harness transport.
|
|
513
|
-
if (method.startsWith('harness.v1.mail.')) {
|
|
514
|
-
try { out(await (await nodeRpc(channel)).call(method, params)); }
|
|
515
|
-
finally { channel.close(); }
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
const client = new SupercodeHarnessClient({ transport: harnessTransport(channel) });
|
|
519
|
-
try {
|
|
520
|
-
await client.start?.();
|
|
521
|
-
out(await client.request(method, params));
|
|
522
|
-
} finally {
|
|
523
|
-
await client.close?.();
|
|
524
|
-
channel.close();
|
|
525
|
-
}
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
case 'app-door': {
|
|
529
|
-
// `app-door on [--origin O[,O…]] [--port N]`, `off`, `rotate`, `status`, `link <app url> [--open]`: the door
|
|
530
|
-
// web apps on this machine use, off until turned on (../app-door.mjs). This machine's own setting: no --machine.
|
|
531
|
-
if (flags.machine) die('app-door is set on the machine itself');
|
|
532
|
-
const origins = (value) => (value === undefined || value === true ? [] : String(value).split(',').map((item) => item.trim()).filter(Boolean));
|
|
533
|
-
const show = (setting) => {
|
|
534
|
-
const served = readAppDoorState(home);
|
|
535
|
-
out({
|
|
536
|
-
enabled: setting.enabled,
|
|
537
|
-
door: `ws://127.0.0.1:${setting.port}${APP_DOOR_PATH}`,
|
|
538
|
-
origins: setting.origins,
|
|
539
|
-
// What the daemon itself last said: listening on the port, or why not.
|
|
540
|
-
listening: served?.listening ?? null,
|
|
541
|
-
...(served?.failure ? { failure: served.failure } : {}),
|
|
542
|
-
file: appDoorFile(home),
|
|
543
|
-
});
|
|
544
|
-
};
|
|
545
|
-
if (sub === 'on' || sub === 'off' || sub === 'rotate') {
|
|
546
|
-
const port = flags.port === undefined ? undefined : Number(flags.port);
|
|
547
|
-
if (port !== undefined && !(Number.isInteger(port) && port > 0 && port < 65536)) die('--port takes a port number');
|
|
548
|
-
const add = origins(flags.origin);
|
|
549
|
-
const remove = origins(flags['remove-origin']);
|
|
550
|
-
if (sub === 'on') {
|
|
551
|
-
const dropped = new Set(remove.map(normalOrigin));
|
|
552
|
-
const kept = readAppDoor(home).origins.filter((item) => !dropped.has(item));
|
|
553
|
-
if (!kept.length && !add.length) die('the app door admits only the origins it names: add one with --origin http://127.0.0.1:PORT');
|
|
554
|
-
}
|
|
555
|
-
const setting = writeAppDoor(home, { enabled: sub === 'rotate' ? undefined : sub === 'on', port, add, remove, rotate: sub === 'rotate' });
|
|
556
|
-
show(setting);
|
|
557
|
-
if (sub === 'rotate') out('a new token: every link handed out before no longer opens the door; open the app again with app-door link');
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
if (sub === 'status' || sub === undefined) { show(readAppDoor(home)); return; }
|
|
561
|
-
if (sub === 'link') {
|
|
562
|
-
// `--open` takes no value; `link --open URL` reads the URL as one.
|
|
563
|
-
const target = rest[0] ?? (typeof flags.open === 'string' ? flags.open : null);
|
|
564
|
-
if (!target) die('app-door link <app url> [--open]');
|
|
565
|
-
const setting = readAppDoor(home);
|
|
566
|
-
if (!setting.enabled) die('the app door is off: app-door on --origin <the app\'s origin>');
|
|
567
|
-
const link = appDoorLink(setting, target);
|
|
568
|
-
if (flags.open) spawnSync(process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open', [link], { stdio: 'ignore', windowsHide: true });
|
|
569
|
-
out(link);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
die('app-door on [--origin ORIGIN[,ORIGIN]] [--remove-origin ORIGIN] [--port N] | app-door off | app-door rotate | app-door status | app-door link <app url> [--open]');
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
case 'log': {
|
|
576
|
-
if (sub !== 'verify') die('log verify');
|
|
577
|
-
out(AccountLog.verify(p.log));
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
default:
|
|
581
|
-
die(MACHINE_HELP);
|
|
37
|
+
if (release) {
|
|
38
|
+
const selected = [...args], nativeAt = selected.indexOf('--supercode');
|
|
39
|
+
if (nativeAt >= 0) selected[nativeAt + 1] = release.native;
|
|
40
|
+
const inlineNative = selected.findIndex(arg => arg.startsWith('--supercode='));
|
|
41
|
+
if (inlineNative >= 0) selected[inlineNative] = `--supercode=${release.native}`;
|
|
42
|
+
const child = spawn(process.execPath, [controller.supervised, ...selected], { stdio: 'inherit', windowsHide: true,
|
|
43
|
+
env: { ...process.env, SUPERCODE_BIN: release.native,
|
|
44
|
+
PATH: [join(release.root, 'bin'), process.env.PATH].filter(Boolean).join(delimiter) } });
|
|
45
|
+
for (const signal of ['SIGINT', 'SIGTERM', ...(process.platform === 'win32' ? ['SIGBREAK'] : [])]) process.once(signal, () => child.kill(signal));
|
|
46
|
+
const outcome = await new Promise(done => {
|
|
47
|
+
child.once('error', error => { process.stderr.write(`supercode: retained startup failed: ${error.message}\n`); done(1); });
|
|
48
|
+
child.once('exit', code => done(code ?? 1));
|
|
49
|
+
});
|
|
50
|
+
process.exitCode = outcome;
|
|
582
51
|
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
if (!workspaceVerb) main().catch((error) => die(error.message ?? String(error), error.code === 'machine_offline' ? 5 : 1));
|
|
52
|
+
} else await import('./teams-cli.mjs');
|