@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 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
- return this.rpcStream("exec", { cmd, args: opts.args ?? [], cwd: opts.cwd, timeoutMs: opts.timeoutMs, stream: true }, (c) => {
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
- onChunk({ stream: c.stream, text: new TextDecoder().decode(bytes), bytes });
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 = {}) => this.mouseCall(x, y, "scroll", opts.button, opts.humanize),
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
- * Mount a volume into the guest at `path`. Thin pass-through to the guest
170
- * `volume.mount` RPC; the gateway volume routes may be a 501 stub today.
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: (volId: string, path: string) => Promise<void>;
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
- /** Splice basic-auth creds into an https remote URL (for private remotes). */
204
- private authUrl;
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 id of unreleased.values()) {
18
- console.warn(`[solari] this process is exiting but session ${shortId(id)} is still running and will bill until its ` +
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
- unreleased.delete(this.releaseToken);
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
- this.channel.onFrame("cmd.data", cmdId, (f) => {
124
- const stream = f.stream ?? "stdout";
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
- if (this.hooks.exec &&
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
- !this.channel.connected) {
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
- const r = await this.hooks.exec(this.id, {
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
- const handle = await this.startCommand(cmd, { ...opts, timeoutMs: undefined }, acc);
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: (volId, path) => this.channel
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
- const detail = (r.stderr || r.stdout || "").trim();
388
- throw new Error(`git ${args[0]} failed (exit ${r.exitCode}): ${detail}`);
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
- authUrl(url, username, password) {
439
+ gitAuth(remoteUrl, username, password) {
393
440
  if (!username && !password)
394
- return url;
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(url);
397
- if (username)
398
- u.username = encodeURIComponent(username);
399
- if (password)
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 url;
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 args = ["clone"];
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(this.authUrl(url, opts.username, opts.password));
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
- const cfg = [];
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.runGit(["remote", "get-url", remote], opts.cwd)).stdout.trim();
494
- if (plain) {
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.2",
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",