pi-better-background-tasks 0.9.0 → 0.9.1
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/package.json +1 -1
- package/src/process.ts +5 -1
- package/src/shared-navigator.ts +3 -2
- package/src/shared-ssh-core/index.ts +50 -2
- package/src/types.ts +2 -0
package/package.json
CHANGED
package/src/process.ts
CHANGED
|
@@ -95,6 +95,10 @@ export function withWindowsLogRedirect(spec: CommandSpec, logPath: string): Comm
|
|
|
95
95
|
return {
|
|
96
96
|
...spec,
|
|
97
97
|
shell: true,
|
|
98
|
+
// The trampoline only redirects and execs an absolute program; sourcing
|
|
99
|
+
// the login profile (/etc/profile and friends) adds seconds of cold-start
|
|
100
|
+
// cost on a Windows runner and nothing the exec needs.
|
|
101
|
+
loginShell: false,
|
|
98
102
|
// The MSYS2 runtime rewrites POSIX-looking argv (e.g. `/c`, `/opt/x.sh`)
|
|
99
103
|
// when exec'ing native Windows binaries. Node spawn passed argv verbatim,
|
|
100
104
|
// so conversion is disabled to keep raw-argv semantics unchanged. The
|
|
@@ -404,7 +408,7 @@ export function commandExecution(spec: CommandSpec): { execPath: string; execArg
|
|
|
404
408
|
const [command, ...args] = spec.argv!;
|
|
405
409
|
return { execPath: command!, execArgs: args };
|
|
406
410
|
}
|
|
407
|
-
return { execPath: resolveDefaultShell(), execArgs: ["-lc", spec.command!] };
|
|
411
|
+
return { execPath: resolveDefaultShell(), execArgs: [spec.loginShell === false ? "-c" : "-lc", spec.command!] };
|
|
408
412
|
}
|
|
409
413
|
|
|
410
414
|
/** Stdio for a spawned task: stdin is always ignored; stdout/stderr are piped (collected) or ignored (redirected into the log by the child itself). */
|
package/src/shared-navigator.ts
CHANGED
|
@@ -174,8 +174,9 @@ export function isNavigatorUiAvailable(ctx: ExtensionContext | undefined): boole
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
export function navigatorFooterHint(
|
|
178
|
-
|
|
177
|
+
export function navigatorFooterHint(_count: number): string | null {
|
|
178
|
+
// Retain the compatibility helper so refresh/reload clears any old badge.
|
|
179
|
+
return null;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
export function applyNavigatorFooter(ui: { setStatus(key: string, value: string | undefined): void }, count: number): string | null {
|
|
@@ -58,6 +58,8 @@ export interface ResolvedSshIdentity {
|
|
|
58
58
|
export const DEFAULT_SSH_CONNECT_TIMEOUT_SECONDS = 10;
|
|
59
59
|
export const DEFAULT_SSH_CONTROL_PERSIST_SECONDS = 600;
|
|
60
60
|
export const MAX_SSH_CONTROL_PATH_BYTES = 100;
|
|
61
|
+
/** The longest socket name under a ControlPath root: `cm-` plus a 32-char hash. */
|
|
62
|
+
const MUX_CONTROL_PATH_NAME_TEMPLATE = `cm-${"0".repeat(32)}`;
|
|
61
63
|
export const DEFAULT_TMUX_BOOTSTRAP_TIMEOUT_MS = 120_000;
|
|
62
64
|
|
|
63
65
|
export interface RemoteRunner {
|
|
@@ -111,18 +113,33 @@ export interface SshMuxCleanupResult {
|
|
|
111
113
|
commandResult: CommandResult;
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
export interface SshMuxForwardResult {
|
|
117
|
+
target: string;
|
|
118
|
+
localPath: string;
|
|
119
|
+
remotePath: string;
|
|
120
|
+
detail: string;
|
|
121
|
+
commandResult: CommandResult;
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
export interface SshMuxController {
|
|
115
125
|
readonly controlPath: string;
|
|
116
126
|
ensure(): Promise<SshMuxEnsureResult>;
|
|
127
|
+
/** Forward a local Unix socket to a remote Unix socket through the running ControlMaster (`ssh -O forward -L local:remote`). */
|
|
128
|
+
forwardUnixSocket(localPath: string, remotePath: string): Promise<SshMuxForwardResult>;
|
|
129
|
+
/** Cancel a forward made by `forwardUnixSocket` (`ssh -O cancel -L local:remote`) and remove the local socket file. */
|
|
130
|
+
cancelUnixSocketForward(localPath: string, remotePath: string): Promise<SshMuxForwardResult>;
|
|
117
131
|
status(): Promise<SshMuxStatus>;
|
|
118
132
|
cleanup(): Promise<SshMuxCleanupResult>;
|
|
119
133
|
withMux(commandSpec: CommandSpec): CommandSpec;
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
export class SshMuxError extends Error {
|
|
123
|
-
|
|
137
|
+
// Declared, not a parameter property: Node's native type stripping (pi-better-server) cannot run those.
|
|
138
|
+
readonly commandResult: CommandResult;
|
|
139
|
+
constructor(message: string, commandResult: CommandResult) {
|
|
124
140
|
super(message);
|
|
125
141
|
this.name = "SshMuxError";
|
|
142
|
+
this.commandResult = commandResult;
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
|
|
@@ -311,7 +328,10 @@ export function wrapRemoteBashCommand(input: RemoteBashCommandInput): string {
|
|
|
311
328
|
|
|
312
329
|
export function defaultSshControlPathRoot(): string {
|
|
313
330
|
const agentDirectory = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
314
|
-
|
|
331
|
+
const root = join(agentDirectory, "ssh-control");
|
|
332
|
+
// A long agent directory would push the socket past the unix path limit; fall back to a short per-user root.
|
|
333
|
+
if (Buffer.byteLength(join(root, MUX_CONTROL_PATH_NAME_TEMPLATE)) <= MAX_SSH_CONTROL_PATH_BYTES) return root;
|
|
334
|
+
return join("/tmp", `pi-ssh-${process.getuid?.() ?? "user"}`);
|
|
315
335
|
}
|
|
316
336
|
|
|
317
337
|
/**
|
|
@@ -328,6 +348,26 @@ export function createSshMuxController(options: SshMuxControllerOptions): SshMux
|
|
|
328
348
|
const target = options.identity.target;
|
|
329
349
|
|
|
330
350
|
const prepareRoot = (): void => ensureControlPathRoot(controlPathRoot);
|
|
351
|
+
const forwardOperation = async (operation: "forward" | "cancel", localPath: string, remotePath: string): Promise<SshMuxForwardResult> => {
|
|
352
|
+
for (const [name, value] of [["localPath", localPath], ["remotePath", remotePath]] as const) {
|
|
353
|
+
if (!value || /[\s:]/.test(value)) throw new Error(`ssh forward ${name} must be non-empty and contain no whitespace or colon`);
|
|
354
|
+
}
|
|
355
|
+
prepareRoot();
|
|
356
|
+
const commandResult = await options.runner.runOnce(muxCommandSpec(options.commandSpec, target, [
|
|
357
|
+
"-o", "ControlMaster=no",
|
|
358
|
+
"-o", `ControlPath=${controlPath}`,
|
|
359
|
+
"-O", operation,
|
|
360
|
+
"-L", `${localPath}:${remotePath}`,
|
|
361
|
+
], false));
|
|
362
|
+
if (operation === "cancel") rmSync(localPath, { force: true });
|
|
363
|
+
if (!commandSucceeded(commandResult)) {
|
|
364
|
+
throw new SshMuxError(
|
|
365
|
+
`SSH ${operation} of ${localPath} -> ${target}:${remotePath} failed${resultDetail(commandResult) ? `: ${resultDetail(commandResult)}` : "."}`,
|
|
366
|
+
commandResult,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return { target, localPath, remotePath, detail: resultDetail(commandResult), commandResult };
|
|
370
|
+
};
|
|
331
371
|
const buildControlCommand = (operation: "check" | "exit"): CommandSpec => {
|
|
332
372
|
prepareRoot();
|
|
333
373
|
return muxCommandSpec(options.commandSpec, target, [
|
|
@@ -350,6 +390,11 @@ export function createSshMuxController(options: SshMuxControllerOptions): SshMux
|
|
|
350
390
|
return {
|
|
351
391
|
controlPath,
|
|
352
392
|
status,
|
|
393
|
+
forwardUnixSocket: async (localPath, remotePath) => {
|
|
394
|
+
rmSync(localPath, { force: true }); // ssh will not bind over a stale socket file
|
|
395
|
+
return await forwardOperation("forward", localPath, remotePath);
|
|
396
|
+
},
|
|
397
|
+
cancelUnixSocketForward: (localPath, remotePath) => forwardOperation("cancel", localPath, remotePath),
|
|
353
398
|
withMux: (commandSpec) => {
|
|
354
399
|
prepareRoot();
|
|
355
400
|
return muxCommandSpec(commandSpec, target, [
|
|
@@ -452,6 +497,9 @@ function ensureControlPathRoot(controlPathRoot: string): void {
|
|
|
452
497
|
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
453
498
|
throw new Error(`ssh mux ControlPath root must be a real directory: ${controlPathRoot}`);
|
|
454
499
|
}
|
|
500
|
+
if (process.getuid && stats.uid !== process.getuid()) {
|
|
501
|
+
throw new Error(`ssh mux ControlPath root must be owned by the current user: ${controlPathRoot}`);
|
|
502
|
+
}
|
|
455
503
|
chmodSync(controlPathRoot, 0o700);
|
|
456
504
|
}
|
|
457
505
|
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface CommandSpec {
|
|
|
22
22
|
command?: string;
|
|
23
23
|
argv?: string[];
|
|
24
24
|
shell?: boolean;
|
|
25
|
+
/** Run the shell as a login shell (`bash -lc`, the default). False runs `bash -c`, skipping profile sourcing. */
|
|
26
|
+
loginShell?: boolean;
|
|
25
27
|
cwd?: string;
|
|
26
28
|
env?: Record<string, string>;
|
|
27
29
|
}
|