@solarisdk/core 0.4.2 → 0.4.4
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/dist/desktop.d.ts +5 -0
- package/dist/desktop.js +41 -5
- package/dist/handle.d.ts +32 -6
- package/dist/handle.js +123 -51
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
package/dist/desktop.d.ts
CHANGED
|
@@ -41,6 +41,11 @@ export declare class Desktop extends SessionHandle {
|
|
|
41
41
|
doubleClick: (x: number, y: number, opts?: ClickOptions) => Promise<void>;
|
|
42
42
|
down: (x: number, y: number, button?: MouseButton) => Promise<void>;
|
|
43
43
|
up: (x: number, y: number, button?: MouseButton) => Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Scroll the wheel with the pointer at (x, y). `direction` defaults to "down", `amount` (wheel clicks, 1-50)
|
|
46
|
+
* to 3. The guest takes the wheel delta in the `y` field and does not move the pointer, so the pointer is moved
|
|
47
|
+
* first and (x, y) is never passed as the delta (it used to be: scroll(x, 500) fired 500 clicks, 20-30 s).
|
|
48
|
+
*/
|
|
44
49
|
scroll: (x: number, y: number, opts?: ScrollOptions) => Promise<void>;
|
|
45
50
|
drag: (from: {
|
|
46
51
|
x: number;
|
package/dist/desktop.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SessionHandle } from "./handle.js";
|
|
2
|
+
import { SolariError } from "./errors.js";
|
|
2
3
|
import { base64ToBytes } from "./transport.js";
|
|
3
4
|
function buttonToCode(button) {
|
|
4
5
|
if (button === undefined)
|
|
@@ -46,11 +47,19 @@ export class Desktop extends SessionHandle {
|
|
|
46
47
|
stream: opts.stream,
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
|
-
execStream(cmd, onChunk, opts = {}) {
|
|
50
|
-
|
|
50
|
+
async execStream(cmd, onChunk, opts = {}) {
|
|
51
|
+
const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() };
|
|
52
|
+
const result = await this.rpcStream("exec", { cmd, args: opts.args ?? [], cwd: opts.cwd, timeoutMs: opts.timeoutMs, stream: true }, (c) => {
|
|
51
53
|
const bytes = c.data ? base64ToBytes(c.data) : new Uint8Array();
|
|
52
|
-
|
|
54
|
+
const dec = decoders[c.stream] ?? decoders.stdout;
|
|
55
|
+
onChunk({ stream: c.stream, text: dec.decode(bytes, { stream: true }), bytes });
|
|
53
56
|
});
|
|
57
|
+
for (const stream of ["stdout", "stderr"]) {
|
|
58
|
+
const rest = decoders[stream].decode();
|
|
59
|
+
if (rest)
|
|
60
|
+
onChunk({ stream, text: rest, bytes: new Uint8Array() });
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
54
63
|
}
|
|
55
64
|
fs = {
|
|
56
65
|
read: (path) => this.files.read(path),
|
|
@@ -71,7 +80,23 @@ export class Desktop extends SessionHandle {
|
|
|
71
80
|
}).then(() => undefined),
|
|
72
81
|
down: (x, y, button = "left") => this.mouseCall(x, y, "down", button),
|
|
73
82
|
up: (x, y, button = "left") => this.mouseCall(x, y, "up", button),
|
|
74
|
-
scroll: (x, y, opts = {}) =>
|
|
83
|
+
scroll: async (x, y, opts = {}) => {
|
|
84
|
+
const amount = opts.amount ?? 3;
|
|
85
|
+
if (!Number.isInteger(amount) || amount < 1 || amount > 50) {
|
|
86
|
+
throw new SolariError("scroll amount must be a whole number of wheel clicks from 1 to 50");
|
|
87
|
+
}
|
|
88
|
+
const direction = opts.direction ?? "down";
|
|
89
|
+
if (direction !== "up" && direction !== "down") {
|
|
90
|
+
throw new SolariError('scroll direction must be "up" or "down"');
|
|
91
|
+
}
|
|
92
|
+
await this.mouseCall(x, y, "move", undefined, opts.humanize);
|
|
93
|
+
await this.guiCall("input.mouse", {
|
|
94
|
+
x,
|
|
95
|
+
y: direction === "up" ? amount : -amount,
|
|
96
|
+
action: "scroll",
|
|
97
|
+
button: buttonToCode(opts.button),
|
|
98
|
+
});
|
|
99
|
+
},
|
|
75
100
|
drag: (from, to, button = "left") => this.guiCall("input.drag", { from, to, button: buttonToCode(button) }).then(() => undefined),
|
|
76
101
|
};
|
|
77
102
|
mouseCall(x, y, action, button, humanize) {
|
|
@@ -126,7 +151,18 @@ export class Desktop extends SessionHandle {
|
|
|
126
151
|
process = {
|
|
127
152
|
list: async () => {
|
|
128
153
|
const r = await this.guiCall("process.list", {});
|
|
129
|
-
return r.processes
|
|
154
|
+
return (r.processes ?? []).map((p) => {
|
|
155
|
+
const info = {
|
|
156
|
+
pid: Number(p.pid),
|
|
157
|
+
name: String(p.name ?? p.comm ?? ""),
|
|
158
|
+
};
|
|
159
|
+
const cmd = p.cmd ?? p.cmdline;
|
|
160
|
+
if (typeof cmd === "string")
|
|
161
|
+
info.cmd = cmd;
|
|
162
|
+
if (typeof p.state === "string")
|
|
163
|
+
info.state = p.state;
|
|
164
|
+
return info;
|
|
165
|
+
});
|
|
130
166
|
},
|
|
131
167
|
kill: (pid) => this.guiCall("process.kill", { pid }).then(() => undefined),
|
|
132
168
|
start: async (cmd, opts = {}) => {
|
package/dist/handle.d.ts
CHANGED
|
@@ -56,14 +56,30 @@ export interface SessionConfig extends ControlChannelConfig {
|
|
|
56
56
|
}
|
|
57
57
|
/** The longest `timeoutMs` the gateway accepts for a command (600 000 ms), and the default for `commands.run`. */
|
|
58
58
|
export declare const DEFAULT_COMMAND_TIMEOUT_MS = 600000;
|
|
59
|
+
/**
|
|
60
|
+
* The session `id` is no longer running (killed or paused, through any handle or by id), so the exit reminder must not
|
|
61
|
+
* name it. Tracking is by id, not by handle: two handles to one session, or a `client.kill(id)` with no handle at all,
|
|
62
|
+
* all clear the same entry.
|
|
63
|
+
*/
|
|
64
|
+
export declare function markSessionReleased(id: string): void;
|
|
65
|
+
/** The lines the exit reminder prints: one per distinct session id, however many handles track it. */
|
|
66
|
+
export declare function exitReminderMessages(): string[];
|
|
67
|
+
/** Ids the exit reminder would name right now (for tests). */
|
|
68
|
+
export declare function unreleasedSessionIds(): string[];
|
|
59
69
|
/** Cap on PTY output held before the first onData() subscriber (bytes, total). */
|
|
60
70
|
export declare const PTY_EARLY_BUFFER_MAX: number;
|
|
71
|
+
export interface SessionHandle {
|
|
72
|
+
/** `await using sbx = await client.create(...)` releases the machine (`kill()`) when the scope ends. */
|
|
73
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
74
|
+
}
|
|
61
75
|
export declare class SessionHandle {
|
|
62
76
|
/** The session id (sandboxId / sessionId). */
|
|
63
77
|
readonly id: string;
|
|
64
78
|
readonly controlUrl: string;
|
|
65
79
|
readonly expiresAt: string;
|
|
66
80
|
protected readonly channel: ControlChannel;
|
|
81
|
+
/** Set once the gateway has refused a command stream because this sandbox is `shared`. */
|
|
82
|
+
private sharedContext;
|
|
67
83
|
protected readonly hooks: SessionHooks;
|
|
68
84
|
/** Set by {@link kill} so {@link close} doesn't warn about a machine we
|
|
69
85
|
* have just released. */
|
|
@@ -166,10 +182,11 @@ export declare class SessionHandle {
|
|
|
166
182
|
}>;
|
|
167
183
|
readonly volumes: {
|
|
168
184
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
185
|
+
* Not available at runtime. A volume is mounted when the sandbox is created (or resumed): the guest cannot fetch the
|
|
186
|
+
* storage credentials itself, and the gateway has no runtime-mount route, so the old pass-through to the guest
|
|
187
|
+
* `volume.mount` RPC always failed with "bucket is required". Pass `volumes: [{ volumeId, path }]` at create.
|
|
171
188
|
*/
|
|
172
|
-
mount: (
|
|
189
|
+
mount: (_volId: string, _path: string) => Promise<void>;
|
|
173
190
|
};
|
|
174
191
|
readonly git: {
|
|
175
192
|
/** Clone `url` into `opts.path` (or git's default dir) under `opts.cwd`. */
|
|
@@ -198,10 +215,19 @@ export declare class SessionHandle {
|
|
|
198
215
|
};
|
|
199
216
|
/** Run `git <args>` in the guest via the command RPC (no shell). */
|
|
200
217
|
private runGit;
|
|
201
|
-
/** Run `git <args>`; throw with the stderr on a non-zero exit. */
|
|
218
|
+
/** Run `git <args>`; throw with the stderr on a non-zero exit. A password passed in `secret` is masked in the message. */
|
|
202
219
|
private mustGit;
|
|
203
|
-
/**
|
|
204
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Credentials for ONE git command, for a private http(s) remote at `remoteUrl`. They go to the guest as environment
|
|
222
|
+
* variables of that command and git reads them back through an inline credential helper, so they are never in the
|
|
223
|
+
* clone URL (and so not in `.git/config`, a snapshot or a template), never in argv (`/proc/<pid>/cmdline`), and never
|
|
224
|
+
* in a config file: the `-c` options last for this command only and the helper script holds no secret.
|
|
225
|
+
*
|
|
226
|
+
* The helper is bound to the remote's origin (`credential.<scheme>://<host>.helper`), so git never hands the password
|
|
227
|
+
* to another host: a redirect to a different host, or a downgrade to http, gets nothing. Returns undefined when
|
|
228
|
+
* there are no credentials, or the remote is not http(s) (ssh remotes do not use them).
|
|
229
|
+
*/
|
|
230
|
+
private gitAuth;
|
|
205
231
|
private gitClone;
|
|
206
232
|
private gitStatus;
|
|
207
233
|
private gitAdd;
|
package/dist/handle.js
CHANGED
|
@@ -6,6 +6,18 @@ export const DEFAULT_COMMAND_TIMEOUT_MS = 600_000;
|
|
|
6
6
|
const unreleased = new Map();
|
|
7
7
|
const MAX_TRACKED_UNRELEASED = 1000;
|
|
8
8
|
let exitHookInstalled = false;
|
|
9
|
+
export function markSessionReleased(id) {
|
|
10
|
+
for (const [token, tracked] of unreleased)
|
|
11
|
+
if (tracked === id)
|
|
12
|
+
unreleased.delete(token);
|
|
13
|
+
}
|
|
14
|
+
export function exitReminderMessages() {
|
|
15
|
+
return [...new Set(unreleased.values())].map((id) => `[solari] this process is exiting but session ${shortId(id)} is still running and will bill until its ` +
|
|
16
|
+
`idle timeout. Call kill() to release it (or declare it with \`await using\`).`);
|
|
17
|
+
}
|
|
18
|
+
export function unreleasedSessionIds() {
|
|
19
|
+
return [...unreleased.values()];
|
|
20
|
+
}
|
|
9
21
|
function trackUnreleased(token, id) {
|
|
10
22
|
unreleased.set(token, id);
|
|
11
23
|
if (unreleased.size > MAX_TRACKED_UNRELEASED)
|
|
@@ -14,20 +26,30 @@ function trackUnreleased(token, id) {
|
|
|
14
26
|
return;
|
|
15
27
|
exitHookInstalled = true;
|
|
16
28
|
process.on("exit", () => {
|
|
17
|
-
for (const
|
|
18
|
-
console.warn(
|
|
19
|
-
`idle timeout. Call kill() to release it (or declare it with \`await using\`).`);
|
|
20
|
-
}
|
|
29
|
+
for (const line of exitReminderMessages())
|
|
30
|
+
console.warn(line);
|
|
21
31
|
unreleased.clear();
|
|
22
32
|
});
|
|
23
33
|
}
|
|
24
34
|
const MIN_REST_TIMEOUT_MS = 1000;
|
|
25
35
|
export const PTY_EARLY_BUFFER_MAX = 1024 * 1024;
|
|
36
|
+
const GIT_CREDENTIAL_HELPER = '!f() { test "$1" = get && printf "username=%s\\npassword=%s\\n" "$SOLARI_GIT_USERNAME" "$SOLARI_GIT_PASSWORD"; }; f';
|
|
37
|
+
function gitSubcommand(args) {
|
|
38
|
+
let i = 0;
|
|
39
|
+
while (args[i] === "-c")
|
|
40
|
+
i += 2;
|
|
41
|
+
return args[i] ?? "";
|
|
42
|
+
}
|
|
43
|
+
function rejectOptionLike(what, v) {
|
|
44
|
+
if (v.startsWith("-"))
|
|
45
|
+
throw new Error(`git ${what} must not start with "-": ${JSON.stringify(v)}`);
|
|
46
|
+
}
|
|
26
47
|
export class SessionHandle {
|
|
27
48
|
id;
|
|
28
49
|
controlUrl;
|
|
29
50
|
expiresAt;
|
|
30
51
|
channel;
|
|
52
|
+
sharedContext = false;
|
|
31
53
|
hooks;
|
|
32
54
|
killed = false;
|
|
33
55
|
releaseToken = {};
|
|
@@ -58,7 +80,8 @@ export class SessionHandle {
|
|
|
58
80
|
return this.channel.reconnect();
|
|
59
81
|
}
|
|
60
82
|
close(opts = {}) {
|
|
61
|
-
|
|
83
|
+
if (!opts.quiet)
|
|
84
|
+
unreleased.delete(this.releaseToken);
|
|
62
85
|
if (!this.killed && !opts.quiet && typeof console !== "undefined") {
|
|
63
86
|
console.warn(`[solari] close() on session ${shortId(this.id)} closed the local channel only — ` +
|
|
64
87
|
`the remote machine is still running and will bill until its idle ` +
|
|
@@ -120,17 +143,26 @@ export class SessionHandle {
|
|
|
120
143
|
opts.onStderr?.(c.data);
|
|
121
144
|
});
|
|
122
145
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const data = f.base64 ? new TextDecoder().decode(base64ToBytes(f.base64)) : "";
|
|
126
|
-
const chunk = { stream, data };
|
|
146
|
+
const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() };
|
|
147
|
+
const emit = (chunk) => {
|
|
127
148
|
if (dataCbs.length === 0)
|
|
128
149
|
buffered.push(chunk);
|
|
129
150
|
else
|
|
130
151
|
for (const cb of dataCbs)
|
|
131
152
|
cb(chunk);
|
|
153
|
+
};
|
|
154
|
+
this.channel.onFrame("cmd.data", cmdId, (f) => {
|
|
155
|
+
const stream = f.stream ?? "stdout";
|
|
156
|
+
const dec = decoders[stream] ?? decoders.stdout;
|
|
157
|
+
const data = f.base64 ? dec.decode(base64ToBytes(f.base64), { stream: true }) : "";
|
|
158
|
+
emit({ stream, data });
|
|
132
159
|
});
|
|
133
160
|
this.channel.onFrame("cmd.exit", cmdId, (f) => {
|
|
161
|
+
for (const stream of ["stdout", "stderr"]) {
|
|
162
|
+
const rest = decoders[stream].decode();
|
|
163
|
+
if (rest)
|
|
164
|
+
emit({ stream, data: rest });
|
|
165
|
+
}
|
|
134
166
|
this.channel.offFrame("cmd.data", cmdId);
|
|
135
167
|
this.channel.offFrame("cmd.exit", cmdId);
|
|
136
168
|
this.channel.offStreamClose(cmdId);
|
|
@@ -172,23 +204,21 @@ export class SessionHandle {
|
|
|
172
204
|
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
173
205
|
throw new SolariError(`Solari: timeoutMs must be a non-negative number of milliseconds, got ${String(timeoutMs)}`);
|
|
174
206
|
}
|
|
175
|
-
|
|
207
|
+
const restEligible = !!this.hooks.exec &&
|
|
176
208
|
!opts.background &&
|
|
177
209
|
!opts.onStdout &&
|
|
178
210
|
!opts.onStderr &&
|
|
179
211
|
!opts.env &&
|
|
180
212
|
!opts.user &&
|
|
181
213
|
timeoutMs >= MIN_REST_TIMEOUT_MS &&
|
|
182
|
-
timeoutMs <= DEFAULT_COMMAND_TIMEOUT_MS
|
|
183
|
-
|
|
214
|
+
timeoutMs <= DEFAULT_COMMAND_TIMEOUT_MS;
|
|
215
|
+
const viaRest = async () => {
|
|
216
|
+
const r = await this.hooks.exec(this.id, { cmd, args: opts.args, cwd: opts.cwd, timeoutMs });
|
|
217
|
+
return { ...r, timedOut: r.timedOut === true };
|
|
218
|
+
};
|
|
219
|
+
if (restEligible && (!this.channel.connected || this.sharedContext)) {
|
|
184
220
|
try {
|
|
185
|
-
|
|
186
|
-
cmd,
|
|
187
|
-
args: opts.args,
|
|
188
|
-
cwd: opts.cwd,
|
|
189
|
-
timeoutMs,
|
|
190
|
-
});
|
|
191
|
-
return { ...r, timedOut: r.timedOut === true };
|
|
221
|
+
return await viaRest();
|
|
192
222
|
}
|
|
193
223
|
catch (err) {
|
|
194
224
|
if (!isRouteUnavailable(err))
|
|
@@ -196,7 +226,24 @@ export class SessionHandle {
|
|
|
196
226
|
}
|
|
197
227
|
}
|
|
198
228
|
const acc = { stdout: "", stderr: "" };
|
|
199
|
-
|
|
229
|
+
let handle;
|
|
230
|
+
try {
|
|
231
|
+
handle = await this.startCommand(cmd, { ...opts, timeoutMs: undefined }, acc);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (!isSharedRefusal(err))
|
|
235
|
+
throw err;
|
|
236
|
+
this.sharedContext = true;
|
|
237
|
+
if (restEligible) {
|
|
238
|
+
try {
|
|
239
|
+
return await viaRest();
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
throw normalizeExecError(e, timeoutMs);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
200
247
|
if (opts.background) {
|
|
201
248
|
return { exitCode: 0, stdout: "", stderr: "", timedOut: false };
|
|
202
249
|
}
|
|
@@ -363,9 +410,7 @@ export class SessionHandle {
|
|
|
363
410
|
return this.hooks.uploadUrl(this.id, path);
|
|
364
411
|
}
|
|
365
412
|
volumes = {
|
|
366
|
-
mount: (
|
|
367
|
-
.call("volume.mount", { volumeId: volId, path })
|
|
368
|
-
.then(() => undefined),
|
|
413
|
+
mount: (_volId, _path) => Promise.reject(new SolariError("volumes.mount() is not supported on a running sandbox: pass volumes: [{ volumeId, path }] when you create it")),
|
|
369
414
|
};
|
|
370
415
|
git = {
|
|
371
416
|
clone: (url, opts = {}) => this.gitClone(url, opts),
|
|
@@ -378,42 +423,61 @@ export class SessionHandle {
|
|
|
378
423
|
branches: (cwd) => this.gitBranches(cwd),
|
|
379
424
|
log: (opts = {}) => this.gitLog(opts),
|
|
380
425
|
};
|
|
381
|
-
async runGit(args, cwd) {
|
|
382
|
-
return this.runCommand("git", { args, ...(cwd ? { cwd } : {}) });
|
|
426
|
+
async runGit(args, cwd, env) {
|
|
427
|
+
return this.runCommand("git", { args, ...(cwd ? { cwd } : {}), ...(env ? { env } : {}) });
|
|
383
428
|
}
|
|
384
|
-
async mustGit(args, cwd) {
|
|
385
|
-
const r = await this.runGit(args, cwd);
|
|
429
|
+
async mustGit(args, cwd, env, secret) {
|
|
430
|
+
const r = await this.runGit(args, cwd, env);
|
|
386
431
|
if (r.exitCode !== 0) {
|
|
387
|
-
|
|
388
|
-
|
|
432
|
+
let detail = (r.stderr || r.stdout || "").trim();
|
|
433
|
+
if (secret)
|
|
434
|
+
detail = detail.split(secret).join("***");
|
|
435
|
+
throw new Error(`git ${gitSubcommand(args)} failed (exit ${r.exitCode}): ${detail}`);
|
|
389
436
|
}
|
|
390
437
|
return r.stdout;
|
|
391
438
|
}
|
|
392
|
-
|
|
439
|
+
gitAuth(remoteUrl, username, password) {
|
|
393
440
|
if (!username && !password)
|
|
394
|
-
return
|
|
441
|
+
return undefined;
|
|
442
|
+
for (const v of [username, password]) {
|
|
443
|
+
if (v !== undefined && /[\r\n\0]/.test(v))
|
|
444
|
+
throw new Error("git credentials must not contain line breaks or NUL");
|
|
445
|
+
}
|
|
446
|
+
let origin;
|
|
395
447
|
try {
|
|
396
|
-
const u = new URL(
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
u.password = encodeURIComponent(password);
|
|
401
|
-
return u.toString();
|
|
448
|
+
const u = new URL(remoteUrl);
|
|
449
|
+
if (u.protocol !== "https:" && u.protocol !== "http:")
|
|
450
|
+
return undefined;
|
|
451
|
+
origin = `${u.protocol}//${u.host}`;
|
|
402
452
|
}
|
|
403
453
|
catch {
|
|
404
|
-
return
|
|
454
|
+
return undefined;
|
|
405
455
|
}
|
|
456
|
+
if (/[=\s]/.test(origin))
|
|
457
|
+
return undefined;
|
|
458
|
+
return {
|
|
459
|
+
cfg: [
|
|
460
|
+
"-c", "credential.helper=",
|
|
461
|
+
"-c", `credential.${origin}.helper=${GIT_CREDENTIAL_HELPER}`,
|
|
462
|
+
],
|
|
463
|
+
env: {
|
|
464
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
465
|
+
SOLARI_GIT_USERNAME: username ?? "",
|
|
466
|
+
SOLARI_GIT_PASSWORD: password ?? "",
|
|
467
|
+
},
|
|
468
|
+
};
|
|
406
469
|
}
|
|
407
470
|
async gitClone(url, opts) {
|
|
408
|
-
const
|
|
471
|
+
const auth = this.gitAuth(url, opts.username, opts.password);
|
|
472
|
+
const args = [...(auth?.cfg ?? []), "clone"];
|
|
409
473
|
if (opts.depth && opts.depth > 0)
|
|
410
474
|
args.push("--depth", String(opts.depth));
|
|
411
475
|
if (opts.branch)
|
|
412
476
|
args.push("--branch", opts.branch);
|
|
413
|
-
args.push(
|
|
477
|
+
args.push("--", url);
|
|
414
478
|
if (opts.path)
|
|
415
479
|
args.push(opts.path);
|
|
416
|
-
await this.mustGit(args, opts.cwd);
|
|
480
|
+
await this.mustGit(args, opts.cwd, auth?.env, opts.password);
|
|
417
481
|
}
|
|
418
482
|
async gitStatus(cwd) {
|
|
419
483
|
const out = await this.mustGit(["status", "--porcelain=v1", "--branch"], cwd);
|
|
@@ -488,25 +552,25 @@ export class SessionHandle {
|
|
|
488
552
|
}
|
|
489
553
|
async gitPushPull(op, opts) {
|
|
490
554
|
const remote = opts.remote || "origin";
|
|
491
|
-
|
|
555
|
+
rejectOptionLike("remote", remote);
|
|
556
|
+
if (opts.branch)
|
|
557
|
+
rejectOptionLike("branch", opts.branch);
|
|
558
|
+
let auth;
|
|
492
559
|
if (opts.username || opts.password) {
|
|
493
|
-
const plain = (await this.
|
|
494
|
-
|
|
495
|
-
const authed = this.authUrl(plain, opts.username, opts.password);
|
|
496
|
-
if (authed !== plain)
|
|
497
|
-
cfg.push("-c", `url.${authed}.insteadOf=${plain}`);
|
|
498
|
-
}
|
|
560
|
+
const plain = (await this.mustGit(["remote", "get-url", "--", remote], opts.cwd)).trim();
|
|
561
|
+
auth = this.gitAuth(plain, opts.username, opts.password);
|
|
499
562
|
}
|
|
500
|
-
const args = [...cfg, op, remote];
|
|
563
|
+
const args = [...(auth?.cfg ?? []), op, "--", remote];
|
|
501
564
|
if (opts.branch)
|
|
502
565
|
args.push(opts.branch);
|
|
503
|
-
await this.mustGit(args, opts.cwd);
|
|
566
|
+
await this.mustGit(args, opts.cwd, auth?.env, opts.password);
|
|
504
567
|
}
|
|
505
568
|
async gitCheckout(ref, opts) {
|
|
569
|
+
rejectOptionLike("ref", ref);
|
|
506
570
|
const args = ["checkout"];
|
|
507
571
|
if (opts.create)
|
|
508
572
|
args.push("-b");
|
|
509
|
-
args.push(ref);
|
|
573
|
+
args.push(ref, "--");
|
|
510
574
|
await this.mustGit(args, opts.cwd);
|
|
511
575
|
}
|
|
512
576
|
async gitBranches(cwd) {
|
|
@@ -569,6 +633,7 @@ export class SessionHandle {
|
|
|
569
633
|
throw new Error("pause requires a client-created handle");
|
|
570
634
|
}
|
|
571
635
|
await this.hooks.pause(this.id);
|
|
636
|
+
markSessionReleased(this.id);
|
|
572
637
|
this.close({ quiet: true });
|
|
573
638
|
}
|
|
574
639
|
async resume() {
|
|
@@ -576,6 +641,7 @@ export class SessionHandle {
|
|
|
576
641
|
throw new Error("resume requires a client-created handle");
|
|
577
642
|
}
|
|
578
643
|
const r = await this.hooks.resume(this.id);
|
|
644
|
+
trackUnreleased(this.releaseToken, this.id);
|
|
579
645
|
this.channel.setControlUrl(r.controlUrl);
|
|
580
646
|
await this.channel.reconnect();
|
|
581
647
|
}
|
|
@@ -618,6 +684,7 @@ export class SessionHandle {
|
|
|
618
684
|
if (this.hooks.kill)
|
|
619
685
|
await this.hooks.kill(this.id);
|
|
620
686
|
this.killed = true;
|
|
687
|
+
markSessionReleased(this.id);
|
|
621
688
|
this.close();
|
|
622
689
|
}
|
|
623
690
|
}
|
|
@@ -652,6 +719,11 @@ function explainShellCommandFailure(cmd, err) {
|
|
|
652
719
|
}
|
|
653
720
|
return new Error(msg + hint);
|
|
654
721
|
}
|
|
722
|
+
function isSharedRefusal(err) {
|
|
723
|
+
if (err instanceof ActionError && err.code === "NotAvailableOnSharedIsolation")
|
|
724
|
+
return true;
|
|
725
|
+
return err instanceof Error && /NotAvailableOnSharedIsolation|not available on a shared/.test(err.message);
|
|
726
|
+
}
|
|
655
727
|
function isRouteUnavailable(err) {
|
|
656
728
|
return (err instanceof GatewayError &&
|
|
657
729
|
(err.status === 404 || err.status === 405 || err.status === 501));
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DEFAULT_COMMAND_TIMEOUT_MS, SessionHandle } from "./handle.js";
|
|
1
|
+
export { DEFAULT_COMMAND_TIMEOUT_MS, SessionHandle, markSessionReleased, unreleasedSessionIds, exitReminderMessages } from "./handle.js";
|
|
2
2
|
export { shortId } from "./ids.js";
|
|
3
3
|
export { formatIdlePolicy } from "./idle-policy.js";
|
|
4
4
|
export type { SessionConfig, SessionHooks } from "./handle.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DEFAULT_COMMAND_TIMEOUT_MS, SessionHandle } from "./handle.js";
|
|
1
|
+
export { DEFAULT_COMMAND_TIMEOUT_MS, SessionHandle, markSessionReleased, unreleasedSessionIds, exitReminderMessages } from "./handle.js";
|
|
2
2
|
export { shortId } from "./ids.js";
|
|
3
3
|
export { formatIdlePolicy } from "./idle-policy.js";
|
|
4
4
|
export { Desktop } from "./desktop.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -632,6 +632,10 @@ export interface MouseResult {
|
|
|
632
632
|
export interface ScrollOptions {
|
|
633
633
|
button?: MouseButton;
|
|
634
634
|
humanize?: boolean;
|
|
635
|
+
/** Which way the wheel turns. Default "down". */
|
|
636
|
+
direction?: "up" | "down";
|
|
637
|
+
/** Wheel clicks, a whole number from 1 to 50. Default 3. */
|
|
638
|
+
amount?: number;
|
|
635
639
|
}
|
|
636
640
|
export interface ClickOptions {
|
|
637
641
|
button?: MouseButton;
|
|
@@ -686,8 +690,12 @@ export interface ProcessListParams {
|
|
|
686
690
|
}
|
|
687
691
|
export interface ProcessInfo {
|
|
688
692
|
pid: number;
|
|
693
|
+
/** The process name (the guest agent's `comm`). */
|
|
689
694
|
name: string;
|
|
695
|
+
/** The full command line (the guest agent's `cmdline`). */
|
|
690
696
|
cmd?: string;
|
|
697
|
+
/** Process state letter as the kernel reports it (`R`, `S`, `Z`, ...). */
|
|
698
|
+
state?: string;
|
|
691
699
|
}
|
|
692
700
|
export interface ProcessListResult {
|
|
693
701
|
processes: ProcessInfo[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solarisdk/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Shared runtime for the Solari SDK \u2014 transport, session handles, images, templates, and types used by @solarisdk/desktop and @solarisdk/sandbox.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|