@zswarm/core 0.1.6 → 0.1.8

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.
@@ -0,0 +1,1171 @@
1
+ import { execFile } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { existsSync } from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import { basename, dirname, isAbsolute, join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { ZellijError } from "../errors.js";
8
+ import { encodePowerShellCommand } from "../zellij/ipc.js";
9
+ import { coverHostReport, hostDoctorRequest, isRequiredFailure, mergeHostReply, } from "./doctor.js";
10
+ import { callServe, isLoopbackHost, parseListenAddress, probeServe, redactServeSecret, SERVE_CORE_VERSION, SERVE_HELLO_TIMEOUT_MS, SERVE_LAUNCH_ID_ENV, SERVE_PROTOCOL, SERVE_TASK_NAME, serveLogonCommand, } from "./serve.js";
11
+ import { authorizeServeListen, SERVE_BIND_REMEDY, SERVE_TAILSCALE_BIN_ENV, } from "./serve-bind.js";
12
+ import { throwIfAborted } from "./util.js";
13
+ /** Overall install/readiness deadline when `timeoutMs` is omitted. */
14
+ export const DEFAULT_SERVE_INSTALL_TIMEOUT_MS = 30_000;
15
+ export const SERVE_NOT_READY_CODE = "serve_not_ready";
16
+ export const SERVE_TASK_OWNED_CODE = "serve_task_owned";
17
+ export const SERVE_TASK_VERSION_ENV = "ZSWARM_CORE_VERSION";
18
+ const HOST_ENV_KEYS = [
19
+ "ZSWARM_BIN",
20
+ "ZSWARM_PATH",
21
+ "ZSWARM_TMP",
22
+ "ZSWARM_STATE_DIR",
23
+ "ZSWARM_BUS",
24
+ "ZSWARM_BUS_PLUGIN",
25
+ "ZSWARM_CACHE_TTL_MS",
26
+ "ZSWARM_SESSION",
27
+ "ZELLIJ_SOCKET_DIR",
28
+ "ZELLIJ_TMP_DIR",
29
+ "ZELLIJ_CONFIG_DIR",
30
+ "TEMP",
31
+ "TMP",
32
+ "PATH",
33
+ "ZSWARM_READONLY",
34
+ "ZSWARM_ALLOW_PANES",
35
+ "ZSWARM_DENY_PANES",
36
+ "ZSWARM_ALLOW_SPAWN",
37
+ "ZSWARM_ALLOW_CLOSE",
38
+ "ZSWARM_ALLOW_WORKTREE_REMOVE",
39
+ /** Optional Tailscale CLI path; re-checked on every task startup, never a token. */
40
+ SERVE_TAILSCALE_BIN_ENV,
41
+ ];
42
+ const CONTROLLER_ENV_KEYS = [
43
+ "ZSWARM_SERVE",
44
+ "ZSWARM_SSH",
45
+ "ZSWARM_SSH_OPTS",
46
+ "ZSWARM_SSH_MODE",
47
+ "ZSWARM_SSH_BIN",
48
+ "ZSWARM_REMOTE_BIN",
49
+ "ZSWARM_REMOTE_SHELL",
50
+ ];
51
+ const WELL_KNOWN_SIDS = new Set(["S-1-5-18", "S-1-5-19", "S-1-5-20"]);
52
+ const PS_PREAMBLE = [
53
+ "$ErrorActionPreference = 'Stop'",
54
+ "$ProgressPreference = 'SilentlyContinue'",
55
+ "[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false",
56
+ "function Get-ZswarmIdentity {",
57
+ " $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()",
58
+ " if (-not $id -or -not $id.User -or [string]::IsNullOrWhiteSpace($id.User.Value)) {",
59
+ " throw 'cannot resolve the current Windows SID for the Interactive logon task'",
60
+ " }",
61
+ " $name = [string]$id.Name",
62
+ " if ([string]::IsNullOrWhiteSpace($name)) {",
63
+ " throw 'cannot resolve the current Windows account name for the Interactive logon task'",
64
+ " }",
65
+ " return @{ name = $name; sid = [string]$id.User.Value }",
66
+ "}",
67
+ "function Resolve-ZswarmSid($account) {",
68
+ " if ($null -eq $account) { return $null }",
69
+ " $text = [string]$account",
70
+ " if ([string]::IsNullOrWhiteSpace($text)) { return $null }",
71
+ " if ($text -match '^S-\\d-') { return $text }",
72
+ " try {",
73
+ " $nt = New-Object System.Security.Principal.NTAccount($text)",
74
+ " $sid = $nt.Translate([System.Security.Principal.SecurityIdentifier])",
75
+ " if (-not $sid -or [string]::IsNullOrWhiteSpace($sid.Value)) { return $null }",
76
+ " return [string]$sid.Value",
77
+ " } catch {",
78
+ " return $null",
79
+ " }",
80
+ "}",
81
+ "function Assert-ZswarmCurrentOwner($task) {",
82
+ " $identity = Get-ZswarmIdentity",
83
+ " $existingSid = Resolve-ZswarmSid ([string]$task.Principal.UserId)",
84
+ " if ([string]::IsNullOrWhiteSpace($existingSid) -or $existingSid -ne $identity.sid) {",
85
+ " throw 'zswarm-serve is owned by a different or unverified Windows account'",
86
+ " }",
87
+ " return $identity",
88
+ "}",
89
+ "function Write-ZswarmJson($obj) {",
90
+ " $json = $obj | ConvertTo-Json -Compress -Depth 6",
91
+ " [Console]::Out.Write($json)",
92
+ "}",
93
+ `function Get-ZswarmTask { Get-ScheduledTask -TaskName '${SERVE_TASK_NAME}' -ErrorAction Stop }`,
94
+ "function Test-ZswarmMissing($err) {",
95
+ " return $err.CategoryInfo.Category -eq 'ObjectNotFound' -or ([string]$err.FullyQualifiedErrorId -match 'ObjectNotFound')",
96
+ "}",
97
+ ].join("\n");
98
+ const REMEDY = {
99
+ notReady: "Registration/start was accepted, but authenticated hello and host inspection did not prove this installation is ready. Retry serve --install or inspect with zswarm doctor; serve --clear removes the owned task. Install does not kill unrelated processes.",
100
+ stale: "A listener answered on this port but did not present this installation's launch identity. Install does not kill arbitrary node processes or port owners. Stop the owned zswarm-serve task (serve --clear) or free the port, then retry.",
101
+ session: "Start the requested Zellij session on the desktop account that owns the crew, then retry serve --install --session <name> or zswarm doctor --session <name>.",
102
+ ipc: "The installed server could not see the desktop Zellij IPC. Confirm the logon task runs as the already-logged-in desktop account (Interactive/Limited) and that ZSWARM_TMP points at that TEMP.",
103
+ cli: "Cannot resolve a zswarm CLI launcher for the logon task. process.argv[1] in MCP is the MCP entrypoint, not the CLI. Set ZSWARM_SERVE_CLI to the CLI script (cli.js / zswarm.mjs), or run `zswarm serve --install` from the CLI.",
104
+ owner: "An existing zswarm-serve task belongs to a different or unverified Windows account. Install does not overwrite another user's task. Run --install as the desktop account that owns the crew, or --clear from that account.",
105
+ interactive: "This task requires an already logged-in interactive desktop session (LogonType Interactive, RunLevel Limited). It is not a headless boot/SYSTEM service.",
106
+ token: "Set the same ZSWARM_SERVE_TOKEN on the server and this caller. The token is stored in the task action (readable by this user and administrators), not encrypted; reinstall to rotate.",
107
+ bind: SERVE_BIND_REMEDY.literal,
108
+ uncertain: "Inspect the zswarm-serve task. The last mutation was interrupted before a confirmed reply; the task may or may not exist. Install/clear does not claim rollback.",
109
+ };
110
+ export function looksLikeMcpEntrypoint(scriptPath) {
111
+ const base = basename(scriptPath).toLowerCase();
112
+ return (base === "mcp-server.js" ||
113
+ base === "mcp-server.mjs" ||
114
+ base === "mcp-server.cjs" ||
115
+ base === "zswarm-mcp" ||
116
+ base === "zswarm-mcp.js" ||
117
+ base === "zswarm-mcp.mjs" ||
118
+ base === "zswarm-mcp.cjs" ||
119
+ base === "zswarm-mcp.cmd");
120
+ }
121
+ export function looksLikeCliEntrypoint(scriptPath) {
122
+ const base = basename(scriptPath).toLowerCase();
123
+ if (base.endsWith(".cmd") || base.endsWith(".bat"))
124
+ return false;
125
+ return (base === "cli.js" ||
126
+ base === "cli.mjs" ||
127
+ base === "cli.cjs" ||
128
+ base === "zswarm" ||
129
+ base === "zswarm.js" ||
130
+ base === "zswarm.mjs" ||
131
+ base === "zswarm.cjs");
132
+ }
133
+ function fileExists(path) {
134
+ try {
135
+ return Boolean(path) && existsSync(path);
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ }
141
+ function resolveFromRequire(fromFile, spec) {
142
+ try {
143
+ const href = isAbsolute(fromFile) ? pathToFileURL(fromFile).href : fromFile;
144
+ return createRequire(href).resolve(spec);
145
+ }
146
+ catch {
147
+ return undefined;
148
+ }
149
+ }
150
+ function firstExisting(paths) {
151
+ for (const path of paths) {
152
+ if (path && looksLikeCliEntrypoint(path) && fileExists(path))
153
+ return path;
154
+ }
155
+ return undefined;
156
+ }
157
+ /**
158
+ * Resolve node + CLI script for the logon task. MCP argv[1] is never installed.
159
+ */
160
+ export function resolveServeCliLaunch(input) {
161
+ const execPath = input.execPath ?? process.execPath;
162
+ if (!execPath) {
163
+ throw new ZellijError("usage", "cannot resolve node executable for the logon task");
164
+ }
165
+ const argv = input.argv ?? process.argv;
166
+ const envCli = input.env?.ZSWARM_SERVE_CLI?.trim();
167
+ const explicit = input.scriptPath?.trim();
168
+ if (explicit) {
169
+ if (looksLikeMcpEntrypoint(explicit)) {
170
+ throw new ZellijError("usage", `${REMEDY.cli} (${explicit} looks like zswarm-mcp)`);
171
+ }
172
+ if (!looksLikeCliEntrypoint(explicit) || !fileExists(explicit)) {
173
+ throw new ZellijError("usage", `cannot use ${explicit} as the zswarm CLI launcher for serve --install`);
174
+ }
175
+ return { execPath, scriptPath: explicit };
176
+ }
177
+ if (envCli) {
178
+ if (looksLikeMcpEntrypoint(envCli)) {
179
+ throw new ZellijError("usage", `${REMEDY.cli} (ZSWARM_SERVE_CLI points at MCP)`);
180
+ }
181
+ if (!looksLikeCliEntrypoint(envCli) || !fileExists(envCli)) {
182
+ throw new ZellijError("usage", `ZSWARM_SERVE_CLI is not a usable zswarm CLI script (${envCli})`);
183
+ }
184
+ return { execPath, scriptPath: envCli };
185
+ }
186
+ const argvScript = typeof argv[1] === "string" ? argv[1] : "";
187
+ if (argvScript && looksLikeMcpEntrypoint(argvScript)) {
188
+ const sibling = firstExisting([
189
+ join(dirname(argvScript), "zswarm.mjs"),
190
+ join(dirname(argvScript), "zswarm.js"),
191
+ join(dirname(argvScript), "zswarm"),
192
+ join(dirname(argvScript), "..", "..", "cli", "dist", "cli.js"),
193
+ join(dirname(argvScript), "..", "..", "cli", "dist", "cli.mjs"),
194
+ resolveFromRequire(argvScript, "@zswarm/cli"),
195
+ ]);
196
+ if (sibling)
197
+ return { execPath, scriptPath: sibling };
198
+ throw new ZellijError("usage", REMEDY.cli);
199
+ }
200
+ if (argvScript && looksLikeCliEntrypoint(argvScript) && fileExists(argvScript)) {
201
+ return { execPath, scriptPath: argvScript };
202
+ }
203
+ throw new ZellijError("usage", "cannot resolve a zswarm CLI launcher for the logon task; set ZSWARM_SERVE_CLI or run serve --install from the CLI");
204
+ }
205
+ /** Host Zellij/IPC + configured server policy; controller routing is stripped. */
206
+ export function serveTaskChildEnv(env) {
207
+ const out = {};
208
+ for (const key of HOST_ENV_KEYS) {
209
+ const raw = key === "PATH" ? env.PATH ?? env.Path : env[key];
210
+ const value = typeof raw === "string" ? raw.trim() : "";
211
+ if (value)
212
+ out[key] = value;
213
+ }
214
+ for (const key of CONTROLLER_ENV_KEYS)
215
+ delete out[key];
216
+ return out;
217
+ }
218
+ /** Child env persisted in the task, including package version and optional launch id. */
219
+ export function serveTaskLaunchEnv(env, launchId) {
220
+ const out = {
221
+ ...serveTaskChildEnv(env),
222
+ [SERVE_TASK_VERSION_ENV]: SERVE_CORE_VERSION,
223
+ };
224
+ const id = launchId?.trim();
225
+ if (id)
226
+ out[SERVE_LAUNCH_ID_ENV] = id;
227
+ return out;
228
+ }
229
+ export function isWindowsSid(value) {
230
+ return /^S-\d-\d+(-\d+)+$/i.test(value.trim());
231
+ }
232
+ /** Verified SID equality. Account-name / UPN resemblance is not identity. */
233
+ export function sameWindowsSid(a, b) {
234
+ const na = a.trim().toUpperCase();
235
+ const nb = b.trim().toUpperCase();
236
+ if (!na || !nb || !isWindowsSid(na) || !isWindowsSid(nb))
237
+ return false;
238
+ return na === nb;
239
+ }
240
+ /** @deprecated Use sameWindowsSid; names are not verified identity. */
241
+ export function sameWindowsAccount(a, b) {
242
+ return sameWindowsSid(a, b);
243
+ }
244
+ function privilegedAccount(userId) {
245
+ return /(?:^|\\)(system|local service|network service)$/i.test(userId.trim()) ||
246
+ /^nt authority\\/i.test(userId.trim());
247
+ }
248
+ function privilegedSid(sid) {
249
+ return WELL_KNOWN_SIDS.has(sid.trim().toUpperCase());
250
+ }
251
+ export function buildServeTaskScript(action, params = {}) {
252
+ if (action === "inspect") {
253
+ return [
254
+ PS_PREAMBLE,
255
+ "try {",
256
+ " $task = Get-ZswarmTask",
257
+ "} catch {",
258
+ " if (Test-ZswarmMissing $_) {",
259
+ " $identity = Get-ZswarmIdentity",
260
+ " Write-ZswarmJson @{ exists = $false; currentUser = $identity.name; currentSid = $identity.sid }",
261
+ " exit 0",
262
+ " }",
263
+ " throw",
264
+ "}",
265
+ "$identity = Get-ZswarmIdentity",
266
+ "$action = @($task.Actions)[0]",
267
+ "$principal = $task.Principal",
268
+ "Write-ZswarmJson @{",
269
+ " exists = $true",
270
+ " currentUser = $identity.name",
271
+ " currentSid = $identity.sid",
272
+ " taskName = [string]$task.TaskName",
273
+ " state = [string]$task.State",
274
+ " userId = [string]$principal.UserId",
275
+ " userSid = (Resolve-ZswarmSid ([string]$principal.UserId))",
276
+ " logonType = [string]$principal.LogonType",
277
+ " runLevel = [string]$principal.RunLevel",
278
+ " execute = [string]$action.Execute",
279
+ " arguments = [string]$action.Arguments",
280
+ "}",
281
+ ].join("\n");
282
+ }
283
+ if (action === "register") {
284
+ const command = params.command ?? "";
285
+ const commandB64 = Buffer.from(command, "utf8").toString("base64");
286
+ return [
287
+ PS_PREAMBLE,
288
+ `$cmd = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${commandB64}'))`,
289
+ "try {",
290
+ " $existing = Get-ZswarmTask",
291
+ " $identity = Assert-ZswarmCurrentOwner $existing",
292
+ "} catch {",
293
+ " if (-not (Test-ZswarmMissing $_)) { throw }",
294
+ " $identity = Get-ZswarmIdentity",
295
+ "}",
296
+ "$userId = $identity.name",
297
+ "$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument ('/c ' + $cmd)",
298
+ "$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited",
299
+ "$trigger = New-ScheduledTaskTrigger -AtLogOn -User $userId",
300
+ `Register-ScheduledTask -TaskName '${SERVE_TASK_NAME}' -Action $action -Principal $principal -Trigger $trigger -Force | Out-Null`,
301
+ "Write-ZswarmJson @{ registered = $true; userId = $userId; userSid = $identity.sid; logonType = 'Interactive'; runLevel = 'Limited'; taskName = '" +
302
+ SERVE_TASK_NAME +
303
+ "' }",
304
+ ].join("\n");
305
+ }
306
+ if (action === "start") {
307
+ return [
308
+ PS_PREAMBLE,
309
+ "try {",
310
+ " $task = Get-ZswarmTask",
311
+ " [void](Assert-ZswarmCurrentOwner $task)",
312
+ "} catch {",
313
+ " throw",
314
+ "}",
315
+ `Start-ScheduledTask -TaskName '${SERVE_TASK_NAME}'`,
316
+ "Write-ZswarmJson @{ started = $true; taskName = '" + SERVE_TASK_NAME + "' }",
317
+ ].join("\n");
318
+ }
319
+ if (action === "stop") {
320
+ return [
321
+ PS_PREAMBLE,
322
+ "try {",
323
+ " $task = Get-ZswarmTask",
324
+ " [void](Assert-ZswarmCurrentOwner $task)",
325
+ ` Stop-ScheduledTask -TaskName '${SERVE_TASK_NAME}'`,
326
+ "} catch {",
327
+ " if (-not (Test-ZswarmMissing $_)) { throw }",
328
+ "}",
329
+ "Write-ZswarmJson @{ stopped = $true; taskName = '" + SERVE_TASK_NAME + "' }",
330
+ ].join("\n");
331
+ }
332
+ return [
333
+ PS_PREAMBLE,
334
+ "$identity = Get-ZswarmIdentity",
335
+ "$stopped = $false",
336
+ "$stopFailed = $false",
337
+ "$stopError = $null",
338
+ "try {",
339
+ " $task = Get-ZswarmTask",
340
+ "} catch {",
341
+ " if (Test-ZswarmMissing $_) {",
342
+ " Write-ZswarmJson @{ cleared = $true; missing = $true; stopped = $false; taskName = '" +
343
+ SERVE_TASK_NAME +
344
+ "' }",
345
+ " exit 0",
346
+ " }",
347
+ " throw",
348
+ "}",
349
+ "[void](Assert-ZswarmCurrentOwner $task)",
350
+ "try {",
351
+ ` Stop-ScheduledTask -TaskName '${SERVE_TASK_NAME}'`,
352
+ " $stopped = $true",
353
+ "} catch {",
354
+ " if (Test-ZswarmMissing $_) {",
355
+ " Write-ZswarmJson @{ cleared = $true; missing = $true; stopped = $false; taskName = '" +
356
+ SERVE_TASK_NAME +
357
+ "' }",
358
+ " exit 0",
359
+ " }",
360
+ " $stopFailed = $true",
361
+ " $stopError = [string]$_.Exception.Message",
362
+ "}",
363
+ "try {",
364
+ ` Unregister-ScheduledTask -TaskName '${SERVE_TASK_NAME}' -Confirm:$false`,
365
+ "} catch {",
366
+ " if (Test-ZswarmMissing $_) {",
367
+ " Write-ZswarmJson @{ cleared = $true; missing = $true; stopped = $stopped; stopFailed = $stopFailed; stopError = $stopError; taskName = '" +
368
+ SERVE_TASK_NAME +
369
+ "' }",
370
+ " exit 0",
371
+ " }",
372
+ " throw",
373
+ "}",
374
+ "Write-ZswarmJson @{ cleared = $true; stopped = $stopped; stopFailed = $stopFailed; stopError = $stopError; taskName = '" +
375
+ SERVE_TASK_NAME +
376
+ "' }",
377
+ ].join("\n");
378
+ }
379
+ function runPowerShellDefault(script, options) {
380
+ const timeout = Math.floor(options.timeoutMs);
381
+ if (timeout <= 0) {
382
+ return Promise.resolve({
383
+ code: 1,
384
+ stdout: "",
385
+ stderr: "no remaining budget",
386
+ timedOut: true,
387
+ });
388
+ }
389
+ return new Promise((resolve) => {
390
+ execFile("powershell.exe", [
391
+ "-NoProfile",
392
+ "-ExecutionPolicy",
393
+ "Bypass",
394
+ "-EncodedCommand",
395
+ encodePowerShellCommand(script),
396
+ ], {
397
+ windowsHide: true,
398
+ timeout,
399
+ signal: options.signal,
400
+ maxBuffer: 1024 * 1024,
401
+ }, (error, stdout, stderr) => {
402
+ const err = error;
403
+ const aborted = Boolean(options.signal?.aborted) ||
404
+ err?.name === "AbortError" ||
405
+ err?.code === "ABORT_ERR";
406
+ const timedOut = Boolean(err?.killed) && !aborted;
407
+ const code = aborted || timedOut
408
+ ? 1
409
+ : error && typeof error.code === "number"
410
+ ? error.code
411
+ : error
412
+ ? 1
413
+ : 0;
414
+ resolve({
415
+ code,
416
+ stdout: String(stdout ?? ""),
417
+ stderr: String(stderr ?? ""),
418
+ ...(aborted ? { aborted: true } : {}),
419
+ ...(timedOut ? { timedOut: true } : {}),
420
+ });
421
+ });
422
+ });
423
+ }
424
+ function parseTaskJson(stdout, secrets) {
425
+ const text = stdout.trim();
426
+ const start = text.indexOf("{");
427
+ const end = text.lastIndexOf("}");
428
+ if (start === -1 || end < start) {
429
+ throw new ZellijError("zellij_failed", `serve task helper returned no JSON (${scrubDiagnosticText(text || "no output", secrets).slice(0, 200)})`);
430
+ }
431
+ try {
432
+ const parsed = JSON.parse(text.slice(start, end + 1));
433
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
434
+ throw new Error("not object");
435
+ }
436
+ return parsed;
437
+ }
438
+ catch {
439
+ throw new ZellijError("zellij_failed", "serve task helper returned invalid JSON");
440
+ }
441
+ }
442
+ function serveInstallToken(input) {
443
+ const raw = input.token ??
444
+ (input.env ? input.env.ZSWARM_SERVE_TOKEN : process.env.ZSWARM_SERVE_TOKEN);
445
+ const secret = raw?.trim() ?? "";
446
+ if (!secret) {
447
+ throw new ZellijError("serve_auth", "zswarm serve requires ZSWARM_SERVE_TOKEN; another local OS user can connect to 127.0.0.1");
448
+ }
449
+ return secret;
450
+ }
451
+ function tokenFromCommand(command) {
452
+ const found = [];
453
+ const matches = command.matchAll(/ZSWARM_SERVE_TOKEN=([^"&\r\n]+)/g);
454
+ for (const match of matches) {
455
+ const value = match[1]?.trim();
456
+ if (value)
457
+ found.push(value);
458
+ }
459
+ return found;
460
+ }
461
+ function scrubDiagnosticText(text, secrets) {
462
+ let out = text
463
+ .replace(/set\s+"ZSWARM_SERVE_TOKEN=[^"]*"/gi, 'set "ZSWARM_SERVE_TOKEN=***"')
464
+ .replace(/ZSWARM_SERVE_TOKEN=([^"&\r\n]*)/gi, "ZSWARM_SERVE_TOKEN=***");
465
+ for (const secret of secrets) {
466
+ if (!secret)
467
+ continue;
468
+ out = redactServeSecret(out, secret);
469
+ const utf8 = Buffer.from(secret, "utf8").toString("base64");
470
+ if (utf8.length >= 4)
471
+ out = out.split(utf8).join("***");
472
+ const utf16 = Buffer.from(secret, "utf16le").toString("base64");
473
+ if (utf16.length >= 4)
474
+ out = out.split(utf16).join("***");
475
+ }
476
+ out = out.replace(/FromBase64String\('([^']*)'\)/g, "FromBase64String('***')");
477
+ out = out.replace(/-EncodedCommand\s+[A-Za-z0-9+/]+=*/g, "-EncodedCommand ***");
478
+ out = out.replace(/[A-Za-z0-9+/]{40,}={0,2}/g, "***");
479
+ return out;
480
+ }
481
+ function scrubSecrets(value, secrets) {
482
+ if (typeof value === "string")
483
+ return scrubDiagnosticText(value, secrets);
484
+ if (Array.isArray(value))
485
+ return value.map((item) => scrubSecrets(item, secrets));
486
+ if (value && typeof value === "object") {
487
+ const out = {};
488
+ for (const [key, nested] of Object.entries(value)) {
489
+ out[key] = scrubSecrets(nested, secrets);
490
+ }
491
+ return out;
492
+ }
493
+ return value;
494
+ }
495
+ function taskRunning(state) {
496
+ return (state ?? "").toLowerCase() === "running";
497
+ }
498
+ function commandFromArguments(raw) {
499
+ const text = raw?.trim() ?? "";
500
+ if (text.toLowerCase().startsWith("/c "))
501
+ return text.slice(3).trim();
502
+ return text;
503
+ }
504
+ export function serveCommandFingerprint(command) {
505
+ return command
506
+ .replace(/set "ZSWARM_SERVE_LAUNCH_ID=[^"]*"&& /g, "")
507
+ .replace(/&& set "ZSWARM_SERVE_LAUNCH_ID=[^"]*"/g, "");
508
+ }
509
+ function launchIdFromCommand(command) {
510
+ const match = command.match(/set "ZSWARM_SERVE_LAUNCH_ID=([^"]*)"/);
511
+ return match?.[1]?.trim() ?? "";
512
+ }
513
+ function asString(value) {
514
+ return typeof value === "string" ? value : "";
515
+ }
516
+ function liveSessionsFrom(checks) {
517
+ const row = checks.find((item) => item.id === "zellij_sessions");
518
+ const live = row?.detail?.live;
519
+ if (!Array.isArray(live))
520
+ return [];
521
+ return live.filter((name) => typeof name === "string" && name.length > 0);
522
+ }
523
+ function checkById(checks, id) {
524
+ return checks.find((item) => item.id === id);
525
+ }
526
+ function hostReportFromMerged(merged, checks, explicitSession) {
527
+ return {
528
+ route: {
529
+ transport: "local",
530
+ endpoint: "host",
531
+ session: merged.session ?? explicitSession,
532
+ sessionOrigin: merged.sessionOrigin ?? (explicitSession ? "explicit" : "unresolved"),
533
+ },
534
+ checks,
535
+ };
536
+ }
537
+ function evaluateHostReadiness(report, explicitSession) {
538
+ const checks = report.checks;
539
+ const sessions = liveSessionsFrom(checks);
540
+ const sessionRow = checkById(checks, "session");
541
+ const sessionName = typeof sessionRow?.detail.session === "string" ? sessionRow.detail.session : explicitSession;
542
+ const failed = checks.filter((item) => isRequiredFailure(report, item));
543
+ if (failed.length > 0) {
544
+ const row = failed[0];
545
+ return {
546
+ ready: false,
547
+ phase: row.id === "session" ? "session" : "host",
548
+ cause: row.code,
549
+ remedy: row.remedy ?? (row.id === "zellij_ipc" ? REMEDY.ipc : row.id === "session" ? REMEDY.session : REMEDY.notReady),
550
+ session: sessionName,
551
+ sessions,
552
+ };
553
+ }
554
+ if (explicitSession) {
555
+ return { ready: true, session: explicitSession, sessions };
556
+ }
557
+ const listed = checkById(checks, "zellij_sessions");
558
+ const empty = sessions.length === 0 || listed?.code === "sessions_none";
559
+ if (empty) {
560
+ return {
561
+ ready: true,
562
+ session: null,
563
+ sessions,
564
+ warning: "Serve is authenticated and can list sessions, but no live Zellij session is visible. This is a healthy server, not a ready crew. Pass --session once the desktop crew is running.",
565
+ };
566
+ }
567
+ return { ready: true, session: sessionName, sessions };
568
+ }
569
+ function notReadyError(message, details, secrets, code = SERVE_NOT_READY_CODE) {
570
+ return new ZellijError(code, scrubDiagnosticText(message, secrets), scrubSecrets(details, secrets));
571
+ }
572
+ async function runTask(action, input) {
573
+ const script = buildServeTaskScript(action, { command: input.command });
574
+ const result = await input.run(script, {
575
+ timeoutMs: input.timeoutMs,
576
+ signal: input.signal,
577
+ });
578
+ if (result.code === 0) {
579
+ try {
580
+ return parseTaskJson(result.stdout, input.secrets);
581
+ }
582
+ catch (err) {
583
+ if (result.aborted || input.signal?.aborted) {
584
+ throw new ZellijError("cancelled", "operation cancelled");
585
+ }
586
+ if (result.timedOut) {
587
+ throw new ZellijError("timeout", `serve task ${action} timed out`);
588
+ }
589
+ throw err;
590
+ }
591
+ }
592
+ if (result.aborted || input.signal?.aborted) {
593
+ throw new ZellijError("cancelled", "operation cancelled");
594
+ }
595
+ if (result.timedOut) {
596
+ throw new ZellijError("timeout", `serve task ${action} timed out`);
597
+ }
598
+ throw new ZellijError("zellij_failed", `serve --${action === "unregister" ? "clear" : "install"} ${action} failed`, scrubSecrets({
599
+ phase: action,
600
+ cause: "helper_failed",
601
+ exitCode: result.code,
602
+ stderr: scrubDiagnosticText((result.stderr || result.stdout).trim(), input.secrets).slice(0, 400),
603
+ }, input.secrets));
604
+ }
605
+ function remainingOf(deadline, now) {
606
+ return Math.max(0, deadline - now());
607
+ }
608
+ async function defaultSleep(ms, signal) {
609
+ if (ms <= 0)
610
+ return;
611
+ await new Promise((resolve, reject) => {
612
+ if (signal?.aborted) {
613
+ reject(new ZellijError("cancelled", "operation cancelled"));
614
+ return;
615
+ }
616
+ const timer = setTimeout(() => {
617
+ signal?.removeEventListener("abort", onAbort);
618
+ resolve();
619
+ }, ms);
620
+ const onAbort = () => {
621
+ clearTimeout(timer);
622
+ signal?.removeEventListener("abort", onAbort);
623
+ reject(new ZellijError("cancelled", "operation cancelled"));
624
+ };
625
+ signal?.addEventListener("abort", onAbort, { once: true });
626
+ });
627
+ }
628
+ /**
629
+ * Register/update the current-user Interactive logon task and wait until the
630
+ * requested server instance is authenticated and the host crew is visible.
631
+ */
632
+ export async function installServeLogon(input = {}) {
633
+ if ((input.platform ?? process.platform) !== "win32") {
634
+ throw new ZellijError("usage", "serve --install registers a Windows logon task; on Unix start `zswarm serve --listen` in the session that owns Zellij");
635
+ }
636
+ const timeoutMs = Math.max(1, Math.floor(input.timeoutMs ?? DEFAULT_SERVE_INSTALL_TIMEOUT_MS));
637
+ const now = input.now ?? Date.now;
638
+ const sleep = input.sleep ?? ((ms) => defaultSleep(ms, input.signal));
639
+ const deadline = now() + timeoutMs;
640
+ const remaining = () => remainingOf(deadline, now);
641
+ const env = input.env ?? process.env;
642
+ const { host, label } = parseListenAddress(input.listen);
643
+ const token = serveInstallToken(input);
644
+ const secrets = [token];
645
+ throwIfAborted(input.signal);
646
+ if (remaining() <= 0) {
647
+ throw notReadyError("serve --install timed out before task mutation", {
648
+ installed: false,
649
+ ready: false,
650
+ running: false,
651
+ phase: "inspect",
652
+ cause: "timeout",
653
+ listen: label,
654
+ task: SERVE_TASK_NAME,
655
+ }, secrets, "timeout");
656
+ }
657
+ // Non-loopback: verify Tailscale + OS ownership before any task mutation.
658
+ // Installer-time proof is not a permanent permit; every task startup revalidates via startServe.
659
+ if (!isLoopbackHost(host)) {
660
+ await authorizeServeListen(host, {
661
+ env,
662
+ signal: input.signal,
663
+ deadline,
664
+ now,
665
+ tailscaleStatus: input.tailscaleStatus,
666
+ networkInterfaces: input.networkInterfaces,
667
+ });
668
+ }
669
+ throwIfAborted(input.signal);
670
+ if (remaining() <= 0) {
671
+ throw notReadyError("serve --install timed out before task mutation", {
672
+ installed: false,
673
+ ready: false,
674
+ running: false,
675
+ phase: "verify",
676
+ cause: "timeout",
677
+ listen: label,
678
+ task: SERVE_TASK_NAME,
679
+ }, secrets, "timeout");
680
+ }
681
+ const launch = resolveServeCliLaunch({
682
+ execPath: input.execPath,
683
+ scriptPath: input.scriptPath,
684
+ argv: input.argv,
685
+ env,
686
+ });
687
+ const run = input.runPowerShell ?? runPowerShellDefault;
688
+ const probe = input.probeServe ?? probeServe;
689
+ const call = input.callServe ??
690
+ ((target, args, options) => callServe(target, args, options));
691
+ const explicitSession = input.session?.trim() || null;
692
+ const requestedLaunchId = input.launchId?.trim() || "";
693
+ const partial = (extra) => {
694
+ const details = scrubSecrets({
695
+ installed: false,
696
+ ready: false,
697
+ running: false,
698
+ task: SERVE_TASK_NAME,
699
+ listen: label,
700
+ ...extra,
701
+ }, secrets);
702
+ if (details.mutation === "uncertain")
703
+ delete details.installed;
704
+ return details;
705
+ };
706
+ const assertRunnable = (phase, extra = {}) => {
707
+ if (input.signal?.aborted) {
708
+ throw notReadyError("serve --install cancelled", {
709
+ ...partial({ ...extra, phase, cause: "cancelled" }),
710
+ }, secrets, "cancelled");
711
+ }
712
+ const left = remaining();
713
+ if (left <= 0) {
714
+ throw notReadyError(`serve --install timed out during ${phase}`, {
715
+ ...partial({ ...extra, phase, cause: "timeout" }),
716
+ }, secrets, "timeout");
717
+ }
718
+ return Math.min(20_000, left);
719
+ };
720
+ const rethrowKnown = (err, extra) => {
721
+ if (err instanceof ZellijError && (err.code === "cancelled" || err.code === "timeout")) {
722
+ throw notReadyError(err.message, {
723
+ ...partial(extra),
724
+ ...(err.details ?? {}),
725
+ cause: err.code,
726
+ phase: typeof extra.phase === "string" ? extra.phase : err.code,
727
+ }, secrets, err.code);
728
+ }
729
+ throw err;
730
+ };
731
+ const inspectBudget = assertRunnable("inspect");
732
+ const inspected = await runTask("inspect", {
733
+ run,
734
+ timeoutMs: inspectBudget,
735
+ signal: input.signal,
736
+ secrets,
737
+ });
738
+ const exists = inspected.exists === true;
739
+ const existingCommand = commandFromArguments(asString(inspected.arguments));
740
+ secrets.push(...tokenFromCommand(existingCommand));
741
+ const ownership = (() => {
742
+ const currentUser = asString(inspected.currentUser);
743
+ const currentSid = asString(inspected.currentSid);
744
+ const userId = asString(inspected.userId);
745
+ const userSid = asString(inspected.userSid);
746
+ if (!isWindowsSid(currentSid)) {
747
+ throw notReadyError("cannot verify the current Windows account SID for the Interactive logon task", {
748
+ ...partial({
749
+ phase: "inspect",
750
+ cause: SERVE_TASK_OWNED_CODE,
751
+ remedy: REMEDY.owner,
752
+ currentUser,
753
+ }),
754
+ }, secrets, SERVE_TASK_OWNED_CODE);
755
+ }
756
+ if (!exists) {
757
+ return { currentUser, currentSid, userId, userSid };
758
+ }
759
+ const verified = isWindowsSid(userSid) && sameWindowsSid(currentSid, userSid);
760
+ if (!verified || privilegedSid(userSid) || privilegedAccount(userId)) {
761
+ throw notReadyError(`serve --install will not overwrite ${SERVE_TASK_NAME} owned by ${userId || "an unverified account"}`, {
762
+ ...partial({
763
+ phase: "inspect",
764
+ cause: SERVE_TASK_OWNED_CODE,
765
+ remedy: REMEDY.owner,
766
+ principal: { userId, userSid },
767
+ currentUser,
768
+ currentSid,
769
+ }),
770
+ }, secrets, SERVE_TASK_OWNED_CODE);
771
+ }
772
+ return { currentUser, currentSid, userId, userSid };
773
+ })();
774
+ const execute = asString(inspected.execute);
775
+ const alreadyRunning = exists && taskRunning(asString(inspected.state));
776
+ const existingLaunchId = launchIdFromCommand(existingCommand);
777
+ const desiredBase = serveLogonCommand(launch.execPath, launch.scriptPath, label, token, serveTaskLaunchEnv(env));
778
+ const configMatches = exists &&
779
+ execute.toLowerCase() === "cmd.exe" &&
780
+ serveCommandFingerprint(existingCommand) === desiredBase &&
781
+ asString(inspected.logonType).toLowerCase() === "interactive" &&
782
+ asString(inspected.runLevel).toLowerCase() === "limited";
783
+ const reuseExistingLaunch = configMatches &&
784
+ Boolean(existingLaunchId) &&
785
+ (!requestedLaunchId || requestedLaunchId === existingLaunchId);
786
+ const launchId = reuseExistingLaunch
787
+ ? existingLaunchId
788
+ : (requestedLaunchId || randomUUID());
789
+ const childEnv = serveTaskLaunchEnv(env, launchId);
790
+ const command = serveLogonCommand(launch.execPath, launch.scriptPath, label, token, childEnv);
791
+ const redactedCommand = redactServeSecret(command, token);
792
+ const needsUpdate = !reuseExistingLaunch;
793
+ let installed = exists && reuseExistingLaunch;
794
+ let principal = {
795
+ userId: ownership.userId || ownership.currentUser,
796
+ userSid: ownership.userSid || ownership.currentSid,
797
+ logonType: asString(inspected.logonType) || "Interactive",
798
+ runLevel: asString(inspected.runLevel) || "Limited",
799
+ };
800
+ const mutate = async (action, extra, commandArg) => {
801
+ const budget = assertRunnable(action, extra);
802
+ try {
803
+ return await runTask(action, {
804
+ run,
805
+ timeoutMs: budget,
806
+ signal: input.signal,
807
+ secrets,
808
+ command: commandArg,
809
+ });
810
+ }
811
+ catch (err) {
812
+ return rethrowKnown(err, extra);
813
+ }
814
+ };
815
+ if (needsUpdate) {
816
+ assertRunnable("register", { installed: false });
817
+ if (alreadyRunning) {
818
+ await mutate("stop", { phase: "stop", installed: false });
819
+ }
820
+ let registered;
821
+ try {
822
+ registered = await mutate("register", {
823
+ phase: "register",
824
+ mutation: "uncertain",
825
+ observed: { existed: exists },
826
+ remedy: REMEDY.uncertain,
827
+ }, command);
828
+ installed = true;
829
+ principal = {
830
+ userId: asString(registered.userId) || principal.userId,
831
+ userSid: asString(registered.userSid) || principal.userSid,
832
+ logonType: asString(registered.logonType) || "Interactive",
833
+ runLevel: asString(registered.runLevel) || "Limited",
834
+ };
835
+ }
836
+ catch (err) {
837
+ if (err instanceof ZellijError && (err.code === "cancelled" || err.code === "timeout")) {
838
+ throw err;
839
+ }
840
+ const message = err instanceof Error ? err.message : String(err);
841
+ throw notReadyError(message, {
842
+ ...partial({
843
+ phase: "register",
844
+ cause: "register_failed",
845
+ stopped: alreadyRunning,
846
+ command: redactedCommand,
847
+ remedy: "Task mutation was interrupted; inspect the zswarm-serve task. Install does not claim rollback.",
848
+ }),
849
+ }, secrets, err instanceof ZellijError ? err.code : "zellij_failed");
850
+ }
851
+ assertRunnable("start", { installed: true, principal, command: redactedCommand });
852
+ try {
853
+ await mutate("start", { phase: "start", installed: true, principal, command: redactedCommand });
854
+ }
855
+ catch (err) {
856
+ if (err instanceof ZellijError && (err.code === "cancelled" || err.code === "timeout")) {
857
+ throw err;
858
+ }
859
+ const message = err instanceof Error ? err.message : String(err);
860
+ throw notReadyError(message, {
861
+ ...partial({
862
+ installed: true,
863
+ phase: "start",
864
+ cause: "start_failed",
865
+ principal,
866
+ command: redactedCommand,
867
+ remedy: REMEDY.notReady,
868
+ }),
869
+ }, secrets, err instanceof ZellijError ? err.code : "zellij_failed");
870
+ }
871
+ }
872
+ else if (!alreadyRunning) {
873
+ assertRunnable("start", { installed: true, principal, command: redactedCommand });
874
+ await mutate("start", { phase: "start", installed: true, principal, command: redactedCommand });
875
+ installed = true;
876
+ }
877
+ let lastCause = "serve_unreachable";
878
+ let lastPhase = "hello";
879
+ let lastRemedy = REMEDY.notReady;
880
+ let lastServer;
881
+ let lastInspection;
882
+ let lastSessions = [];
883
+ let probes = 0;
884
+ const known = () => partial({
885
+ installed: true,
886
+ principal,
887
+ command: redactedCommand,
888
+ phase: lastPhase,
889
+ cause: lastCause,
890
+ remedy: lastRemedy,
891
+ ...(lastServer ? { server: lastServer } : {}),
892
+ ...(lastInspection ? { inspection: { checks: lastInspection } } : {}),
893
+ sessions: lastSessions,
894
+ session: explicitSession,
895
+ });
896
+ const waitRetry = async () => {
897
+ try {
898
+ await sleep(Math.min(100, remaining()));
899
+ }
900
+ catch (err) {
901
+ if (err instanceof ZellijError && (err.code === "cancelled" || err.code === "timeout")) {
902
+ throw notReadyError(err.message, known(), secrets, err.code);
903
+ }
904
+ throw err;
905
+ }
906
+ };
907
+ while (remaining() > 0 && probes < 64) {
908
+ assertRunnable("hello", known());
909
+ probes += 1;
910
+ const helloBudget = Math.max(1, Math.min(SERVE_HELLO_TIMEOUT_MS, remaining()));
911
+ const hello = await probe(label, {
912
+ token,
913
+ timeoutMs: helloBudget,
914
+ signal: input.signal,
915
+ });
916
+ if (!hello.ok) {
917
+ if (hello.error.code === "cancelled") {
918
+ throw notReadyError("serve --install cancelled during hello", known(), secrets, "cancelled");
919
+ }
920
+ lastPhase = "hello";
921
+ lastCause = hello.error.code;
922
+ lastRemedy =
923
+ hello.error.code === "serve_unauthorized"
924
+ ? REMEDY.token
925
+ : hello.error.code === "serve_unreachable" || hello.error.code === "timeout"
926
+ ? REMEDY.notReady
927
+ : hello.error.message;
928
+ if (remaining() <= 0)
929
+ break;
930
+ await waitRetry();
931
+ continue;
932
+ }
933
+ const server = hello.data;
934
+ lastServer = {
935
+ protocol: server.protocol,
936
+ serverId: server.serverId,
937
+ hostname: server.hostname,
938
+ platform: server.platform,
939
+ version: server.version,
940
+ capabilities: [...server.capabilities],
941
+ ...(server.launchId ? { launchId: server.launchId } : {}),
942
+ };
943
+ if (server.protocol !== SERVE_PROTOCOL) {
944
+ lastPhase = "hello";
945
+ lastCause = "serve_incompatible";
946
+ lastRemedy = `This serve protocol is not supported by this client (got ${server.protocol}).`;
947
+ await waitRetry();
948
+ continue;
949
+ }
950
+ if (server.launchId !== launchId) {
951
+ lastPhase = "hello";
952
+ lastCause = "stale_listener";
953
+ lastRemedy = REMEDY.stale;
954
+ if (remaining() <= 0)
955
+ break;
956
+ await waitRetry();
957
+ continue;
958
+ }
959
+ assertRunnable("host", known());
960
+ const hostResult = await call(label, hostDoctorRequest(remaining(), explicitSession), {
961
+ timeoutMs: Math.max(1, remaining()),
962
+ token,
963
+ signal: input.signal,
964
+ });
965
+ const absorbHostRows = (result) => {
966
+ const merged = mergeHostReply(result);
967
+ if (merged.checks.length) {
968
+ lastInspection = coverHostReport(merged.checks, explicitSession);
969
+ lastSessions = liveSessionsFrom(lastInspection);
970
+ }
971
+ return merged;
972
+ };
973
+ if (!hostResult.ok && hostResult.error.code === "cancelled") {
974
+ absorbHostRows(hostResult);
975
+ throw notReadyError("serve --install cancelled during host inspection", known(), secrets, "cancelled");
976
+ }
977
+ if (!hostResult.ok && hostResult.error.code === "timeout") {
978
+ lastPhase = "host";
979
+ lastCause = "timeout";
980
+ lastRemedy = REMEDY.notReady;
981
+ absorbHostRows(hostResult);
982
+ if (remaining() <= 0)
983
+ break;
984
+ await waitRetry();
985
+ continue;
986
+ }
987
+ if (!hostResult.ok) {
988
+ const merged = absorbHostRows(hostResult);
989
+ const checks = lastInspection ?? coverHostReport(merged.checks, explicitSession);
990
+ const report = hostReportFromMerged(merged, checks, explicitSession);
991
+ const verdict = evaluateHostReadiness(report, explicitSession);
992
+ lastInspection = checks;
993
+ lastPhase = verdict.phase ?? "host";
994
+ lastCause = verdict.cause ?? hostResult.error.code;
995
+ lastRemedy = verdict.remedy ?? hostResult.error.message;
996
+ throw notReadyError(hostResult.error.message, {
997
+ ...known(),
998
+ envelope: { code: hostResult.error.code, message: hostResult.error.message },
999
+ inspection: { checks },
1000
+ }, secrets, hostResult.error.code || SERVE_NOT_READY_CODE);
1001
+ }
1002
+ const merged = absorbHostRows(hostResult);
1003
+ if (merged.unsupported) {
1004
+ lastPhase = "host";
1005
+ lastCause = "doctor_unsupported";
1006
+ lastRemedy =
1007
+ "This serve peer does not implement doctor host inspection. Upgrade zswarm serve on the crew host, then retry.";
1008
+ await waitRetry();
1009
+ continue;
1010
+ }
1011
+ if (merged.invalid) {
1012
+ lastPhase = "host";
1013
+ lastCause = "host_report_invalid";
1014
+ lastRemedy =
1015
+ "Authenticated hello succeeded, but the host doctor reply was missing or malformed. Upgrade zswarm serve and retry.";
1016
+ if (remaining() <= 0)
1017
+ break;
1018
+ await waitRetry();
1019
+ continue;
1020
+ }
1021
+ const checks = coverHostReport(merged.checks, explicitSession);
1022
+ lastInspection = checks;
1023
+ lastSessions = liveSessionsFrom(checks);
1024
+ const verdict = evaluateHostReadiness(hostReportFromMerged(merged, checks, explicitSession), explicitSession);
1025
+ if (verdict.ready) {
1026
+ assertRunnable("ready", {
1027
+ installed: true,
1028
+ principal,
1029
+ server: lastServer,
1030
+ inspection: { checks },
1031
+ });
1032
+ const data = {
1033
+ installed: true,
1034
+ ready: true,
1035
+ running: true,
1036
+ task: SERVE_TASK_NAME,
1037
+ listen: label,
1038
+ command: redactedCommand,
1039
+ principal,
1040
+ server: lastServer,
1041
+ session: verdict.session,
1042
+ sessions: verdict.sessions,
1043
+ inspection: { checks },
1044
+ };
1045
+ if (verdict.warning)
1046
+ data.warning = verdict.warning;
1047
+ return scrubSecrets(data, secrets);
1048
+ }
1049
+ lastPhase = verdict.phase ?? "host";
1050
+ lastCause = verdict.cause ?? SERVE_NOT_READY_CODE;
1051
+ lastRemedy = verdict.remedy ?? REMEDY.notReady;
1052
+ const conclusive = lastCause === "session_missing" ||
1053
+ lastCause === "ipc_failed" ||
1054
+ lastCause === "zellij_missing" ||
1055
+ lastCause === "zellij_wrong_bin" ||
1056
+ lastCause === "zellij_incompatible";
1057
+ if (conclusive) {
1058
+ throw notReadyError(`serve --install registered ${SERVE_TASK_NAME} but it is not ready (${lastCause})`, known(), secrets);
1059
+ }
1060
+ if (remaining() <= 0)
1061
+ break;
1062
+ await waitRetry();
1063
+ }
1064
+ if (input.signal?.aborted) {
1065
+ throw notReadyError("serve --install cancelled", known(), secrets, "cancelled");
1066
+ }
1067
+ const code = lastCause === "cancelled" ? "cancelled" : remaining() <= 0 || lastCause === "timeout" ? "timeout" : SERVE_NOT_READY_CODE;
1068
+ throw notReadyError(code === "timeout"
1069
+ ? "serve --install timed out waiting for authenticated readiness"
1070
+ : `serve --install registered ${SERVE_TASK_NAME} but it is not ready (${lastCause})`, known(), secrets, code === SERVE_NOT_READY_CODE ? SERVE_NOT_READY_CODE : code);
1071
+ }
1072
+ export async function uninstallServeLogon(input = {}) {
1073
+ if ((input.platform ?? process.platform) !== "win32") {
1074
+ throw new ZellijError("usage", "serve --clear is Windows-only");
1075
+ }
1076
+ const timeoutMs = Math.max(1, Math.floor(input.timeoutMs ?? DEFAULT_SERVE_INSTALL_TIMEOUT_MS));
1077
+ const now = input.now ?? Date.now;
1078
+ const deadline = now() + timeoutMs;
1079
+ const remaining = () => remainingOf(deadline, now);
1080
+ const token = input.token?.trim() || input.env?.ZSWARM_SERVE_TOKEN?.trim() || "none";
1081
+ const secrets = token === "none" ? [] : [token];
1082
+ const run = input.runPowerShell ?? runPowerShellDefault;
1083
+ const assertClear = (phase, extra = {}) => {
1084
+ if (input.signal?.aborted) {
1085
+ throw notReadyError("serve --clear cancelled", {
1086
+ installed: extra.installed ?? false,
1087
+ ready: false,
1088
+ task: SERVE_TASK_NAME,
1089
+ phase,
1090
+ cause: "cancelled",
1091
+ ...extra,
1092
+ }, secrets, "cancelled");
1093
+ }
1094
+ const left = remaining();
1095
+ if (left <= 0) {
1096
+ throw notReadyError("serve --clear timed out", {
1097
+ installed: extra.installed ?? false,
1098
+ ready: false,
1099
+ task: SERVE_TASK_NAME,
1100
+ phase,
1101
+ cause: "timeout",
1102
+ ...extra,
1103
+ }, secrets, "timeout");
1104
+ }
1105
+ return Math.min(20_000, left);
1106
+ };
1107
+ const inspectBudget = assertClear("inspect");
1108
+ const inspected = await runTask("inspect", {
1109
+ run,
1110
+ timeoutMs: inspectBudget,
1111
+ signal: input.signal,
1112
+ secrets,
1113
+ });
1114
+ secrets.push(...tokenFromCommand(commandFromArguments(asString(inspected.arguments))));
1115
+ assertClear("unregister", { installed: inspected.exists === true });
1116
+ if (inspected.exists === true) {
1117
+ const currentSid = asString(inspected.currentSid);
1118
+ const userSid = asString(inspected.userSid);
1119
+ const currentUser = asString(inspected.currentUser);
1120
+ const existingUser = asString(inspected.userId);
1121
+ const verified = isWindowsSid(currentSid) && isWindowsSid(userSid) && sameWindowsSid(currentSid, userSid);
1122
+ if (!verified || privilegedSid(userSid) || privilegedAccount(existingUser)) {
1123
+ throw notReadyError(`serve --clear will not remove ${SERVE_TASK_NAME} owned by ${existingUser || "an unverified account"}`, {
1124
+ installed: true,
1125
+ ready: false,
1126
+ task: SERVE_TASK_NAME,
1127
+ phase: "inspect",
1128
+ cause: SERVE_TASK_OWNED_CODE,
1129
+ remedy: REMEDY.owner,
1130
+ principal: { userId: existingUser, userSid },
1131
+ currentUser,
1132
+ currentSid,
1133
+ }, secrets, SERVE_TASK_OWNED_CODE);
1134
+ }
1135
+ }
1136
+ const unregisterBudget = assertClear("unregister", { installed: inspected.exists === true });
1137
+ let cleared;
1138
+ try {
1139
+ cleared = await runTask("unregister", {
1140
+ run,
1141
+ timeoutMs: unregisterBudget,
1142
+ signal: input.signal,
1143
+ secrets,
1144
+ });
1145
+ }
1146
+ catch (err) {
1147
+ if (err instanceof ZellijError && (err.code === "cancelled" || err.code === "timeout")) {
1148
+ throw notReadyError(err.message, {
1149
+ ready: false,
1150
+ task: SERVE_TASK_NAME,
1151
+ phase: "unregister",
1152
+ cause: err.code,
1153
+ mutation: "uncertain",
1154
+ observed: { existed: inspected.exists === true },
1155
+ remedy: REMEDY.uncertain,
1156
+ }, secrets, err.code);
1157
+ }
1158
+ throw err;
1159
+ }
1160
+ const confirmed = {
1161
+ cleared: true,
1162
+ stopped: cleared.stopped === true,
1163
+ ...(cleared.missing === true ? { missing: true } : {}),
1164
+ ...(cleared.stopFailed === true ? { stopFailed: true } : {}),
1165
+ };
1166
+ assertClear("unregister", confirmed);
1167
+ return {
1168
+ task: SERVE_TASK_NAME,
1169
+ ...confirmed,
1170
+ };
1171
+ }