@gjsify/devtools 0.36.0 → 0.37.0

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.
@@ -3,24 +3,20 @@ import Gtk from '@girs/gtk-4.0';
3
3
  import type { InstallDevtoolsOptions } from './extension.js';
4
4
  import { type DevtoolsPeerServer } from './peer-transport.js';
5
5
  /**
6
- * The permanent `org.gjsify.Devtools` DBus interface for a GTK app — lets
7
- * external tooling (the MCP bridge, `gdbus`, scripts) inspect, screenshot,
8
- * and drive the running app. Generic methods are implemented here; app
9
- * extensions contribute extra methods that are attached (pause-guarded) and
10
- * merged into the same interface. Resolves the live window lazily per call so
11
- * it survives window recreation.
6
+ * The permanent `org.gjsify.Devtools` DBus interface for a GTK app, through which
7
+ * external tooling (the MCP bridge, `gdbus`, scripts) inspects, screenshots and drives
8
+ * it. Generic methods live here; app extensions contribute pause-guarded methods merged
9
+ * into the same interface. The live window is resolved lazily per call, so the service
10
+ * survives window recreation.
12
11
  */
13
12
  export declare class DevtoolsService {
14
13
  private readonly _app;
15
14
  private readonly _options;
16
15
  /**
17
- * One `Gio.DBusExportedObject` PER CONNECTION. The session bus is a single
18
- * shared pipe, but the peer transport (`peer-transport.ts`) hands out one
19
- * `Gio.DBusConnection` per client with no shared object registry — a
20
- * single-slot field would export the interface to the FIRST peer only, and
21
- * every later client would see `UnknownMethod` for all 26 methods. Keyed by
22
- * connection, so `export()` stays idempotent PER connection while serving
23
- * any number of them.
16
+ * One `Gio.DBusExportedObject` PER CONNECTION. The session bus is a single shared
17
+ * pipe, but the peer transport hands out one `Gio.DBusConnection` per client with no
18
+ * shared object registry, so a single-slot field would export to the FIRST peer only
19
+ * and every later client would see `UnknownMethod` for every method.
24
20
  */
25
21
  private readonly _exported;
26
22
  private _peer;
@@ -40,9 +36,9 @@ export declare class DevtoolsService {
40
36
  */
41
37
  unexport(connection?: Gio.DBusConnection): void;
42
38
  /**
43
- * Record the peer server hosting this service — plus the file its address was
44
- * published to, if any — so `unexport()` closes the socket AND retracts the
45
- * claim, and callers can read both back rather than scraping the app's stderr.
39
+ * Record the peer server hosting this service and the file its address went to, so
40
+ * `unexport()` closes the socket AND retracts the claim, and callers can read both
41
+ * back instead of scraping the app's stderr.
46
42
  */
47
43
  attachPeerServer(peer: DevtoolsPeerServer, addressFile?: string | null): void;
48
44
  /** The peer address clients must dial, or `null` when this service is on a bus. */
@@ -54,29 +50,23 @@ export declare class DevtoolsService {
54
50
  /**
55
51
  * `Screenshot(scope) -> ay` — PNG bytes of the active window via the GSK renderer.
56
52
  *
57
- * Implemented with the `<Name>Async` manual-reply convention (the method
58
- * gets the raw `invocation` and calls `return_value()` itself) rather than a
59
- * plain `async Screenshot(): Promise<Uint8Array>` ON PURPOSE. On gjs 1.86.0 a
60
- * Promise-returning exported DBus method mis-marshals its resolved value and
61
- * the caller sees `org.gnome.gjs.JSError.ValueError: Service implementation
62
- * returned an incorrect value type` — the `_handleDBusReply` continuation in
63
- * the Gio override fails for EVERY resolved type (a bare `s`, a pre-packed
64
- * `GLib.Variant`, an `ay`), not just byte arrays; only Screenshot surfaced it
65
- * because it is the sole async method in the interface. Replying manually
66
- * through the invocation is unaffected. The wire method stays `Screenshot`
67
- * (the interface XML is unchanged) — `wrapJSObject` falls back to
68
- * `<Name>Async` when the bare name is absent. Tracked upstream in
69
- * status/upstream-patch-candidates.md; collapse back to a plain `async
70
- * Screenshot` once the gjs fix ships.
53
+ * Uses the `<Name>Async` manual-reply convention: the method takes the raw
54
+ * `invocation` and calls `return_value()` itself, and `wrapJSObject` finds it because
55
+ * it falls back to `<Name>Async` when the bare name is absent, so the wire method
56
+ * stays `Screenshot`.
57
+ *
58
+ * The shape exists because gjs 1.86.0 mis-marshalled the resolved value of ANY
59
+ * Promise-returning exported method (`org.gnome.gjs.JSError.ValueError: Service
60
+ * implementation returned an incorrect value type`), while a manual reply was
61
+ * unaffected. That no longer reproduces on gjs 1.88.1 — a plain `async Screenshot()`
62
+ * returning `ay` marshals correctly — so this can collapse back into one.
71
63
  */
72
64
  ScreenshotAsync(_params: unknown[], invocation: Gio.DBusMethodInvocation): Promise<void>;
73
65
  /**
74
- * Capture the active window to PNG, warming up across frames when needed. A
75
- * just-launched or mid-layout window yields a zero-size GSK frame, so the
76
- * first `captureWidgetPng` can be empty; presenting it and retrying across a
77
- * few main-loop iterations turns that into a real screenshot. The empty-bytes
78
- * contract is preserved as the genuine-failure signal (window truly never
79
- * realises / is occluded).
66
+ * Capture the active window to PNG, warming up across frames: a just-launched or
67
+ * mid-layout window yields a zero-size GSK frame, so the first `captureWidgetPng` can
68
+ * be empty. Empty bytes remain the genuine-failure signal, for a window that never
69
+ * realises at all.
80
70
  */
81
71
  private _captureActiveWindowPng;
82
72
  /** `ListActions() -> s` — JSON of the `app.*` + `win.*` actions. */
@@ -1,11 +1,10 @@
1
1
  import type Gio from '@girs/gio-2.0';
2
2
  import type { MethodKind } from '@gjsify/devtools-protocol';
3
3
  /**
4
- * App-specific extension of the devtools control plane. The host contributes
5
- * extra DBus methods (e.g. the map-editor's PaintTile) without forking the
6
- * generic core: the `<method>` XML is merged into `org.gjsify.Devtools`, the
7
- * handlers are attached as concrete methods (positional DBus args), and each
8
- * carries a pause classification the guard enforces.
4
+ * App-specific extension of the devtools control plane: extra DBus methods (the
5
+ * map-editor's PaintTile, say) without forking the generic core. The `<method>` XML is
6
+ * merged into `org.gjsify.Devtools`, the handlers attach as concrete methods taking
7
+ * positional DBus args, and each carries a pause classification the guard enforces.
9
8
  */
10
9
  export interface DevtoolsExtension {
11
10
  /** `<method>…</method>` XML fragments merged into the interface. */
@@ -18,8 +18,7 @@ export type DevtoolsTransportChoice =
18
18
  kind: 'unregistered';
19
19
  };
20
20
  /**
21
- * The transport PRECEDENCE, as one pure function so it is machine-checked
22
- * rather than merely described:
21
+ * The transport PRECEDENCE, as one pure function so a spec can check it:
23
22
  *
24
23
  * | requested address | app has bus connection | session bus answers | choice |
25
24
  * |---|---|---|---|
@@ -28,16 +27,11 @@ export type DevtoolsTransportChoice =
28
27
  * | unset | no | no | `peer`, auto-picked — this machine has no usable bus |
29
28
  * | unset | no | yes | `unregistered` — the bus is fine, the CALL SITE is early |
30
29
  *
31
- * The third row is the point of the whole transport, and it AUTO-PICKS rather
32
- * than failing: `GJSIFY_DEVTOOLS=1` is an explicit request for a control plane,
33
- * and answering it with "no control plane, one line on stderr" is exactly the
34
- * silent absence being removed. It cannot regress Linux — a host whose bus
35
- * answers never reaches that row.
36
- *
37
- * The fourth row deliberately does NOT fall back. The bus works, so the honest
38
- * diagnosis is that `installDevtools` ran before the application registered
39
- * (call it from `startup`); standing a socket up there would paper over a
40
- * call-site bug and hide it behind a working-looking address.
30
+ * Row three AUTO-PICKS rather than failing: `GJSIFY_DEVTOOLS=1` is an explicit request
31
+ * for a control plane, so refusing it with one stderr line is the silent absence this
32
+ * transport exists to remove. Row four deliberately does NOT fall back — the bus works,
33
+ * so the honest diagnosis is a call site that ran before registration, and a socket
34
+ * there would hide that behind a working-looking address.
41
35
  */
42
36
  export declare function chooseDevtoolsTransport(input: {
43
37
  requestedAddress?: string | null;
@@ -45,11 +39,9 @@ export declare function chooseDevtoolsTransport(input: {
45
39
  sessionBusUsable: boolean;
46
40
  }): DevtoolsTransportChoice;
47
41
  /**
48
- * The diagnostic for "no connection to export on". It must NAME THE RIGHT
49
- * CAUSE: the old message blamed the call site unconditionally ("call
50
- * installDevtools() from the startup handler"), which on macOS sent every reader
51
- * hunting a bug in their own app when the machine simply has no session bus.
52
- * Three distinguishable states, three answers.
42
+ * The diagnostic for "no connection to export on", which must NAME THE RIGHT CAUSE:
43
+ * blaming the call site unconditionally sent every macOS reader hunting a bug in their
44
+ * own app when the machine simply has no session bus. Three states, three answers.
53
45
  */
54
46
  export declare function describeMissingConnection(sessionBusAddress: string | null, busAnswers: boolean): string;
55
47
  /**
@@ -59,15 +51,14 @@ export declare function describeMissingConnection(sessionBusAddress: string | nu
59
51
  * handler — the session-bus connection + object path are only available after
60
52
  * the application has registered.
61
53
  *
62
- * Needs NO session bus (macOS, Windows): with none, it listens on a socket of
63
- * its own and publishes the address. {@link chooseDevtoolsTransport} documents
64
- * the precedence; `options.address` / `GJSIFY_DEVTOOLS_ADDRESS` pins it.
54
+ * Needs NO session bus (macOS, Windows): with none it listens on a socket of its own
55
+ * and publishes the address — precedence in {@link chooseDevtoolsTransport},
56
+ * `options.address` / `GJSIFY_DEVTOOLS_ADDRESS` pins it.
65
57
  *
66
- * TOTAL BY CONTRACT: it never throws. Every failure — an address already in use,
67
- * a stale socket that cannot be reclaimed, a malformed address — is reported on
68
- * stderr with the address and the way out, and returns `null` like the disabled
69
- * path. Callers wire it into `startup`/`activate` handlers whose remaining
70
- * statements (`present()`, `onStartup()`) GJS would silently skip on a throw.
58
+ * TOTAL BY CONTRACT: it never throws. Every failure is reported on stderr with the
59
+ * address and the way out, and returns `null` like the disabled path, because callers
60
+ * wire it into `startup`/`activate` handlers whose remaining statements GJS would
61
+ * silently skip on a throw.
71
62
  *
72
63
  * @example
73
64
  * ```ts
@@ -11,20 +11,16 @@ export interface DevtoolsPeerServer {
11
11
  stop(): void;
12
12
  }
13
13
  /**
14
- * A peer transport that could not be stood up. `message` is already the
15
- * operator-facing sentence — `installDevtools` logs it verbatim rather than
16
- * paraphrasing a localised GIO error (on a German desktop the bare
17
- * `Gio.DBusServer` failure reads `Fehler beim Binden an Adresse
18
- * (GUnixSocketAddress): Die Adresse wird bereits verwendet`, which names neither
19
- * the address nor a way out).
14
+ * A peer transport that could not be stood up. `message` is already the operator-facing
15
+ * sentence, which `installDevtools` logs verbatim: the bare `Gio.DBusServer` failure is
16
+ * LOCALISED and names neither the address nor a way out.
20
17
  */
21
18
  export declare class DevtoolsPeerServerError extends Error {
22
19
  readonly address: string;
23
20
  readonly reason: 'address-in-use' | 'occupied-by-other-file' | 'listen-failed';
24
21
  /**
25
- * The GIO failure underneath, when there was one. A field rather than the
26
- * `Error(message, { cause })` option because this package compiles to
27
- * `lib: es2020`, which predates it.
22
+ * The GIO failure underneath, as a field rather than `Error(message, { cause })`:
23
+ * this package compiles to `lib: es2020`, which predates that option.
28
24
  */
29
25
  readonly cause?: unknown;
30
26
  constructor(message: string, address: string, reason: 'address-in-use' | 'occupied-by-other-file' | 'listen-failed', cause?: unknown);
@@ -33,17 +29,14 @@ export declare class DevtoolsPeerServerError extends Error {
33
29
  * Listen on `address` (auto-picked when omitted) and export `service` at
34
30
  * `objectPath` on every incoming peer connection.
35
31
  *
36
- * Each peer connection is its own `Gio.DBusConnection` with no shared name
37
- * registry, so the interface has to be exported per connection — which is why
38
- * `DevtoolsService.export()` tracks exports per connection rather than once. A
39
- * second client would otherwise attach to a connection with nothing on it and
40
- * see `UnknownMethod` for every call, the exact silent-absence failure this
41
- * whole transport exists to remove.
32
+ * Each peer connection is its own `Gio.DBusConnection` with no shared name registry, so
33
+ * the interface must be exported PER CONNECTION — hence `DevtoolsService.export()`
34
+ * tracking exports per connection. A second client would otherwise attach to a
35
+ * connection with nothing on it and see `UnknownMethod` for every call.
42
36
  *
43
- * Throws a {@link DevtoolsPeerServerError} when the address cannot be listened on
44
- * — an explicitly requested address that does not work is a configuration error
45
- * the operator must see, and the message says what to do about it. `installDevtools`
46
- * catches it: an opt-in diagnostic must never abort an app's lifecycle.
37
+ * Throws a {@link DevtoolsPeerServerError} when the address cannot be listened on, with
38
+ * a message saying what to do about it; `installDevtools` catches it, because an opt-in
39
+ * diagnostic must never abort an app's lifecycle.
47
40
  */
48
41
  export declare function startDevtoolsPeerServer(service: DevtoolsService, objectPath: string, address?: string): DevtoolsPeerServer;
49
42
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/devtools",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "In-app DBus devtools control plane for GJS/GTK apps — inspect, screenshot, and drive a running app over org.gjsify.Devtools (gdbus or the MCP bridge)",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -43,11 +43,11 @@
43
43
  "@girs/gobject-2.0": "^4.1.0",
44
44
  "@girs/graphene-1.0": "^4.1.0",
45
45
  "@girs/gtk-4.0": "^4.1.0",
46
- "@gjsify/devtools-protocol": "^0.36.0"
46
+ "@gjsify/devtools-protocol": "^0.37.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@gjsify/cli": "^0.36.0",
50
- "@gjsify/unit": "^0.36.0",
49
+ "@gjsify/cli": "^0.37.0",
50
+ "@gjsify/unit": "^0.37.0",
51
51
  "@types/node": "^25.9.2",
52
52
  "typescript": "^6.0.3"
53
53
  },