@gjsify/devtools 0.52.0 → 0.54.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.
package/README.md CHANGED
@@ -149,14 +149,61 @@ Three of those need their reason stated, because the obvious use is the wrong on
149
149
 
150
150
  `opts.extend` adds app-specific methods (§ below); `opts.instance` / `GJSIFY_DEVTOOLS_INSTANCE` scope a multi-instance app.
151
151
 
152
- ### Calling one by hand
152
+ ### Calling one by hand — or from `gjsify devtools`
153
+
154
+ `gdbus` reaches the plane, and it is the right tool for reading a status string. It cannot do the
155
+ two things a script actually needs: `gdbus` cannot write BINARY (so a screenshot comes back as an
156
+ `ay` variant nobody can save), and it speaks only the session bus (which macOS and Windows do not
157
+ have — see above). So both halves live in this package instead of in every consumer's own script:
153
158
 
154
159
  ```bash
155
- gdbus call --session --dest <app-id> --object-path <path>/devtools \
156
- --method org.gjsify.Devtools.Screenshot ""
160
+ gjsify devtools shot /tmp/app.png --bus-name org.example.App # PNG on disk + its real size
161
+ gjsify devtools find Adw.StatusPage:error --bus-name org.example.App # → a widget path
162
+ gjsify devtools activate /toplevel:0/child:3 --bus-name org.example.App
163
+ gjsify devtools help # every operation and flag
157
164
  ```
158
165
 
159
- That returns an `ay` variant. `gdbus` itself cannot save binary, so unpack it from a tiny GJS caller.
166
+ Set `gjsify.devtools.busNameBase` in `package.json` once and the flag stops being needed. Progress
167
+ goes to **stderr**, the result to **stdout**, so `$(gjsify devtools find …)` is the widget path.
168
+ **Exit codes are the contract**: `0` done, `1` the app said no (no match, refused key, no image),
169
+ `2` the request itself was wrong (unknown operation, no `--bus-name`).
170
+
171
+ `gjsify devtools` is the scriptable peer of [`gjsify debug`](../devtools-mcp) (MCP over stdio, for
172
+ an AI agent) — same transport, same client, different front end. `--address` reaches the peer
173
+ socket on a bus-less host, and `--timeout <s>` waits for `GetStatus` to answer rather than sleeping:
174
+ a cold start measured 94 s before the first call was served.
175
+
176
+ ### Getting a screenshot onto disk — `captureShot`
177
+
178
+ `gdbus call … Screenshot` returns an `ay` variant, and unpacking one into a file was a 60-line GJS
179
+ script per consumer. `captureShot` is that script, once:
180
+
181
+ ```ts
182
+ import { captureShot } from '@gjsify/devtools/shot';
183
+
184
+ const shot = await captureShot(
185
+ async (scope) => client.control(instance, 'Screenshot', tuple(scope), '(ay)'),
186
+ '/tmp/app.png',
187
+ 'window',
188
+ );
189
+ shot.width; // 1100 — read out of the PNG, NOT the size asked for
190
+ ```
191
+
192
+ The `./shot` entry point is deliberately GTK-free: the client side of a screenshot needs Gio and
193
+ GLib only, so a headless client does not drag this package's app-side GTK adapter in with it.
194
+
195
+ Two invariants ride along, each paid for with a measured incident:
196
+
197
+ - **Empty bytes are a FAILURE, not a picture.** `Screenshot` answers `ay[0]` when there is no active
198
+ window or the window was never realised — a deliberate contract, see `CaptureBlocker`. Writing
199
+ that yields a 0-byte `.png`, a success line and exit 0: a rig that reports success while handing
200
+ on a file nothing can open. `captureShot` retries, then throws `CaptureShotError('empty-answer')`
201
+ **without writing the file**.
202
+ - **The size comes from the PNG, never from the request.** `ResizeWindow` answers with the size it
203
+ was ASKED for, whether or not the window honoured it, and `default-width` reads back that same
204
+ asked-for value. Measured in a consumer: 1280 requested, 1100 in the file, four checks green.
205
+ `pngSize()` reads the IHDR header instead, which is the one number about a screenshot that cannot
206
+ be faked — so `resize` cannot report its own failure and `shot` can.
160
207
 
161
208
  ## App-specific methods — a `DevtoolsExtension`
162
209
 
@@ -175,6 +222,31 @@ const myExtension: DevtoolsExtension = {
175
222
  installDevtools(this, { extend: [myExtension] });
176
223
  ```
177
224
 
225
+ ## The Adwaita markup trap
226
+
227
+ A widget property that Pango parses as markup turns a bare `&` or `<` into a **parse error**, and
228
+ the label renders **blank**. Nothing throws, nothing fails, and the only symptom is a screenshot
229
+ somebody looks at. German UI text is full of `&` ("Kosten & Förderung"), so it is a recurring class.
230
+
231
+ ```ts
232
+ import { hasRawPangoMarkup, isPangoMarkupSink } from '@gjsify/devtools';
233
+
234
+ isPangoMarkupSink('AdwActionRow', 'title'); // true — `use-markup` is @default true
235
+ hasRawPangoMarkup('Kosten & Förderung'); // true — the label would render empty
236
+ hasRawPangoMarkup('Kosten &amp; Förderung'); // false — an entity is markup, and valid
237
+ hasRawPangoMarkup('<b>Kosten</b> & Ertrag'); // false — markup you wrote on purpose
238
+ ```
239
+
240
+ The interesting half is `PANGO_MARKUP_SINKS`, the list of properties that parse markup at all,
241
+ because the traps cut both ways. `Gtk.Label:label` and `AdwWindowTitle:title` are plain text —
242
+ reporting `&` there teaches an audience to switch the check off. And **`AdwAlertDialog:heading` and
243
+ `:body` are plain text by default**: `heading-use-markup` / `body-use-markup` are both
244
+ `@default false` in `@girs/adw-1`, so a sink list that includes them is wrong, however plausible it
245
+ looks. Every row class inherits markup titles from `AdwPreferencesRow:use-markup` (`@default true`,
246
+ "subclasses may also use it for other labels, such as subtitle") — which is where the trap in most
247
+ apps comes from. Runtime values need `GLib.markup_escape_text()` at the point of use; this checks
248
+ literals, it does not claim a rendered string is clean.
249
+
178
250
  ## Exports
179
251
 
180
252
  - `installDevtools(app, options)` → returns a `DevtoolsService`, or `null` when the env gate is off **or the transport could not come up**. It never throws: every failure is one stderr line naming the address and the way out. `uninstallDevtools(service)` — opt-in lifecycle; it stops the socket *and* retracts the published address.
@@ -183,6 +255,8 @@ installDevtools(this, { extend: [myExtension] });
183
255
  - `startDevtoolsPeerServer(service, objectPath, address?)` → `DevtoolsPeerServer` — the bus-less transport by hand; it throws `DevtoolsPeerServerError` (with `address` + `reason`) when the address is unusable, which is what `installDevtools` catches. `chooseDevtoolsTransport(input)` — the precedence table above as a pure function; `writeDevtoolsAddressFile` / `removeDevtoolsAddressFile` — publish/retract a peer address.
184
256
  - `captureWidgetPng` (GSK screenshot), `buildVariant` / `variantKindFor` / `VariantKind` (GVariant), `activateAction` / `changeActionState` / `describeActions` (GAction bridge).
185
257
  - widget-tree helpers (`dumpTree`, `getWidgetProperty`, `listToplevels`, `resolveWidgetPath`, …), `dumpCss` / `swapCss` / `removeCss`, `dumpGSettings`, `buildDevtoolsIfaceXml`.
258
+ - **client-side, from the GTK-free `./shot` entry point**: `captureShot(source, path, scope?, options?)` → `ShotResult` (with `width`/`height` read from the PNG) or `CaptureShotError`; `pngSize(bytes)` → `{ width, height }` or `null` for bytes that are not a PNG.
259
+ - markup-trap helpers (root entry point): `hasRawPangoMarkup(text)`, `rawPangoMarkupIn(text)`, `isPangoMarkupSink(class, property)`, `PANGO_MARKUP_SINKS`.
186
260
  - the re-exported `@gjsify/devtools-protocol` contract.
187
261
 
188
262
  > **GJS gotcha:** the long-lived `Gio.DBusExportedObject` is reachable only through a self-cycle, which SpiderMonkey's GC can collect mid-run. `installDevtools` roots the service in a module-level set to defeat this — keep that rooting if you wire the service by hand.
package/lib/esm/index.js CHANGED
@@ -1 +1 @@
1
- import{buildVariant as e,variantKindFor as t}from"./gvariant.js";import{activateAction as n,changeActionState as r,describeActions as i}from"./actions.js";import{dumpCss as a,removeCss as o,swapCss as s}from"./css.js";import{buildDevtoolsIfaceXml as c}from"./devtools-iface.js";import{DevtoolsPeerServerError as l,removeDevtoolsAddressFile as u,startDevtoolsPeerServer as d,writeDevtoolsAddressFile as f}from"./peer-transport.js";import{captureWidget as p,captureWidgetPng as m}from"./screenshot.js";import{dumpGSettings as h}from"./gsettings.js";import{buildWidgetPath as g,dumpTree as _,getWidgetProperty as v,listToplevels as y,parseWidgetPath as b,pathOfWidget as x,resolveWidgetPath as S,widgetIsA as C,widgetType as w}from"./widget-tree.js";import{DevtoolsService as T}from"./devtools-service.js";import{chooseDevtoolsTransport as E,installDevtools as D,uninstallDevtools as O}from"./install.js";export*from"@gjsify/devtools-protocol";export{l as DevtoolsPeerServerError,T as DevtoolsService,n as activateAction,c as buildDevtoolsIfaceXml,e as buildVariant,g as buildWidgetPath,p as captureWidget,m as captureWidgetPng,r as changeActionState,E as chooseDevtoolsTransport,i as describeActions,a as dumpCss,h as dumpGSettings,_ as dumpTree,v as getWidgetProperty,D as installDevtools,y as listToplevels,b as parseWidgetPath,x as pathOfWidget,o as removeCss,u as removeDevtoolsAddressFile,S as resolveWidgetPath,d as startDevtoolsPeerServer,s as swapCss,O as uninstallDevtools,t as variantKindFor,C as widgetIsA,w as widgetType,f as writeDevtoolsAddressFile};
1
+ import{buildVariant as e,variantKindFor as t}from"./gvariant.js";import{activateAction as n,changeActionState as r,describeActions as i}from"./actions.js";import{dumpCss as a,removeCss as o,swapCss as s}from"./css.js";import{buildDevtoolsIfaceXml as c}from"./devtools-iface.js";import{DevtoolsPeerServerError as l,removeDevtoolsAddressFile as u,startDevtoolsPeerServer as d,writeDevtoolsAddressFile as f}from"./peer-transport.js";import{captureWidget as p,captureWidgetPng as m}from"./screenshot.js";import{dumpGSettings as h}from"./gsettings.js";import{buildWidgetPath as g,dumpTree as _,getWidgetProperty as v,listToplevels as y,parseWidgetPath as b,pathOfWidget as x,resolveWidgetPath as S,widgetIsA as C,widgetType as w}from"./widget-tree.js";import{DevtoolsService as T}from"./devtools-service.js";import{chooseDevtoolsTransport as E,installDevtools as D,uninstallDevtools as O}from"./install.js";import{CaptureShotError as k,captureShot as A,pngSize as j}from"./shot.js";import{PANGO_MARKUP_SINKS as M,hasRawPangoMarkup as N,isPangoMarkupSink as P,rawPangoMarkupIn as F}from"./pango.js";export*from"@gjsify/devtools-protocol";export{k as CaptureShotError,l as DevtoolsPeerServerError,T as DevtoolsService,M as PANGO_MARKUP_SINKS,n as activateAction,c as buildDevtoolsIfaceXml,e as buildVariant,g as buildWidgetPath,A as captureShot,p as captureWidget,m as captureWidgetPng,r as changeActionState,E as chooseDevtoolsTransport,i as describeActions,a as dumpCss,h as dumpGSettings,_ as dumpTree,v as getWidgetProperty,N as hasRawPangoMarkup,D as installDevtools,P as isPangoMarkupSink,y as listToplevels,b as parseWidgetPath,x as pathOfWidget,j as pngSize,F as rawPangoMarkupIn,o as removeCss,u as removeDevtoolsAddressFile,S as resolveWidgetPath,d as startDevtoolsPeerServer,s as swapCss,O as uninstallDevtools,t as variantKindFor,C as widgetIsA,w as widgetType,f as writeDevtoolsAddressFile};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";const e={AdwAboutDialog:[`comments`,`license`],AdwAboutWindow:[`comments`,`license`],AdwActionRow:[`title`,`subtitle`],AdwBanner:[`title`],AdwComboRow:[`title`,`subtitle`],AdwEntryRow:[`title`,`subtitle`],AdwExpanderRow:[`title`,`subtitle`],AdwPreferencesGroup:[`title`,`description`],AdwPreferencesPage:[`title`,`description`],AdwSpinRow:[`title`,`subtitle`],AdwStatusPage:[`title`,`description`],AdwSwitchRow:[`title`,`subtitle`],AdwToast:[`title`]},t=`&(?![a-zA-Z][a-zA-Z0-9]*;|#\\d+;|#x[0-9a-fA-F]+;)|<(?![a-zA-Z/!?])`,n=RegExp(t),r=RegExp(t,`g`);function rawPangoMarkupIn(e){let t=new Set;for(let n of e.matchAll(r))t.add(n[0]);return[...t]}function hasRawPangoMarkup(e){return e.length<2?!1:n.test(e)}function isPangoMarkupSink(t,n){let r=e[t];return r?r.includes(n):!1}export{e as PANGO_MARKUP_SINKS,hasRawPangoMarkup,isPangoMarkupSink,rawPangoMarkupIn};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import e from"gi://GLib?version=2.0";import t from"gi://Gio?version=2.0";const n=[137,80,78,71,13,10,26,10];var CaptureShotError=class extends Error{constructor(e,t,n,r){super(n),this.scope=e,this.reason=t,this.cause=r,this.name=`CaptureShotError`}};function pngSize(e){if(e.length<24)return null;for(let t=0;t<n.length;t++)if(e[t]!==n[t])return null;let t=new DataView(e.buffer,e.byteOffset,e.byteLength);return{width:t.getUint32(16),height:t.getUint32(20)}}function delay(t){return new Promise(n=>{e.timeout_add(e.PRIORITY_DEFAULT,t,()=>(n(),e.SOURCE_REMOVE))})}function writeBytes(e,n){t.File.new_for_path(e).replace_contents(n,null,!1,t.FileCreateFlags.REPLACE_DESTINATION,null)}async function captureShot(e,t,n=`window`,r={}){let{retries:i=8,retryDelayMs:a=250,settleMs:o=0}=r;o>0&&await delay(o);let s=new Uint8Array;for(let t=0;t<=i&&(s=await e(n),!(s.length>0));t++)t<i&&await delay(a);if(s.length===0)throw new CaptureShotError(n,`empty-answer`,`no image for scope "${n}" after ${i+1} attempts — the window is absent or was never realised`);let c=pngSize(s);if(!c)throw new CaptureShotError(n,`unreadable-png`,`scope "${n}" answered ${s.length} bytes that are not a PNG — refusing to write them out as an image`);try{writeBytes(t,new Uint8Array(s))}catch(e){throw new CaptureShotError(n,`write-failed`,`cannot write ${t}`,e)}return{path:t,byteLength:s.length,width:c.width,height:c.height}}export{CaptureShotError,captureShot,pngSize};
@@ -5,11 +5,14 @@ export type { DevtoolsExtension, InstallDevtoolsOptions } from './extension.js';
5
5
  export { DevtoolsPeerServerError, removeDevtoolsAddressFile, startDevtoolsPeerServer, writeDevtoolsAddressFile, } from './peer-transport.js';
6
6
  export type { DevtoolsPeerServer } from './peer-transport.js';
7
7
  export { captureWidget, captureWidgetPng, type CaptureBlocker, type CaptureResult } from './screenshot.js';
8
+ export { CaptureShotError, captureShot, pngSize } from './shot.js';
9
+ export type { CaptureShotOptions, PngSize, PngSource, ShotFailure, ShotResult } from './shot.js';
8
10
  export { buildVariant, variantKindFor } from './gvariant.js';
9
11
  export type { VariantKind } from './gvariant.js';
10
12
  export { activateAction, changeActionState, describeActions } from './actions.js';
11
13
  export { buildDevtoolsIfaceXml } from './devtools-iface.js';
12
14
  export { buildWidgetPath, dumpTree, getWidgetProperty, listToplevels, parseWidgetPath, pathOfWidget, resolveWidgetPath, widgetIsA, widgetType, } from './widget-tree.js';
13
15
  export { dumpCss, removeCss, swapCss } from './css.js';
16
+ export { hasRawPangoMarkup, isPangoMarkupSink, PANGO_MARKUP_SINKS, rawPangoMarkupIn } from './pango.js';
14
17
  export { dumpGSettings } from './gsettings.js';
15
18
  export * from '@gjsify/devtools-protocol';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Adw properties whose value Pango parses as markup, as `ClassName` → the properties it applies to.
3
+ *
4
+ * The `use-markup` / `*-use-markup` properties are the documented switches, and their DEFAULTS are
5
+ * what decide membership. Read out of `@girs/adw-1`:
6
+ *
7
+ * · `AdwPreferencesRow:use-markup` — `@default true`, "whether to use Pango markup for the title
8
+ * label … subclasses may also use it for other labels, such as subtitle". So every
9
+ * `AdwPreferencesRow` subclass has markup `title` AND `subtitle` unless it opts out.
10
+ * · `AdwBanner:use-markup` and `AdwToast:use-markup` — both `@default true`, for the title.
11
+ * · `AdwAlertDialog:heading-use-markup` and `:body-use-markup` — both **`@default false`**, so
12
+ * `heading` and `body` are PLAIN TEXT by default and marking them raw would be a false finding.
13
+ * This is where a sink list copied from a consumer's own script goes wrong.
14
+ * · `AdwMessageDialog` (deprecated since 1.6) is absent for the same practical reason: its heading
15
+ * and body went through markup, but it has no toggle to read a default off and no reason to be
16
+ * written in new code.
17
+ * · `AdwPreferencesGroup`, `AdwPreferencesPage` and `AdwStatusPage` carry no toggle and no GIR note;
18
+ * their titles and descriptions are markup in libadwaita's implementation. Included, and noted
19
+ * here as the one group not backed by GIR documentation, so a future correction has a home.
20
+ * · `AdwAboutDialog`/`AdwAboutWindow` — the GIR documents `comments` ("can also contain links and
21
+ * Pango markup") and `license` ("can contain Pango markup and links") in prose. `debug_info` is
22
+ * in the same class and explicitly cannot, which is why it is not here.
23
+ *
24
+ * `Gtk.Label:label` and `AdwWindowTitle:title` are plain text and are deliberately absent: a rule
25
+ * that reports `&` in those teaches its audience to switch it off.
26
+ */
27
+ export declare const PANGO_MARKUP_SINKS: Readonly<Record<string, readonly string[]>>;
28
+ /** The raw markup characters in `text`, deduplicated; empty when the string parses as markup. */
29
+ export declare function rawPangoMarkupIn(text: string): string[];
30
+ /**
31
+ * Would `text` render BLANK as an Adw markup property?
32
+ *
33
+ * The single-character cases are answered first because they are the ones that read as deliberate:
34
+ * an author who wrote `"&"` or `"<"` meant them, and flagging it teaches nothing.
35
+ */
36
+ export declare function hasRawPangoMarkup(text: string): boolean;
37
+ /** Is `property` of `className` (e.g. `'AdwActionRow'`, `'title'`) a markup sink? */
38
+ export declare function isPangoMarkupSink(className: string, property: string): boolean;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,60 @@
1
+ /** Fetches PNG bytes for a scope (`''`/`window` for the active window, a widget path for one widget). */
2
+ export type PngSource = (scope: string) => Promise<Uint8Array>;
3
+ /** The pixel dimensions read out of a PNG's own IHDR header. */
4
+ export interface PngSize {
5
+ width: number;
6
+ height: number;
7
+ }
8
+ /** A written screenshot: where it is, how big the file is, and how big the PICTURE is. */
9
+ export interface ShotResult {
10
+ path: string;
11
+ /** Bytes on disk. Never 0 — a capture that produced none throws instead. */
12
+ byteLength: number;
13
+ /** From the PNG header, so it is what the file actually contains. */
14
+ width: number;
15
+ height: number;
16
+ }
17
+ /** Tunables for {@link captureShot}; the defaults are the measured ones from a consumer rig. */
18
+ export interface CaptureShotOptions {
19
+ /**
20
+ * How many extra attempts after an empty answer. The window is often mid-layout, so one
21
+ * empty reply is normal right after `present()` — 8 × 250 ms was enough for every cold
22
+ * start measured, and 0 is the right answer for a caller that drives its own settling.
23
+ */
24
+ retries?: number;
25
+ /** Gap between attempts, milliseconds. */
26
+ retryDelayMs?: number;
27
+ /** Pause before the FIRST capture — the counterpart of a rig's `--settle`. */
28
+ settleMs?: number;
29
+ }
30
+ /** Why {@link captureShot} gave up. Named, because "it wrote nothing" is not a diagnosis. */
31
+ export type ShotFailure = 'empty-answer' | 'unreadable-png' | 'write-failed';
32
+ /** The capture that did not happen. Thrown so a caller cannot mistake it for a picture. */
33
+ export declare class CaptureShotError extends Error {
34
+ readonly scope: string;
35
+ readonly reason: ShotFailure;
36
+ readonly cause?: unknown | undefined;
37
+ constructor(scope: string, reason: ShotFailure, message: string, cause?: unknown | undefined);
38
+ }
39
+ /**
40
+ * The pixel size of a PNG, read from its IHDR chunk — the width and height are the first two
41
+ * big-endian `uint32`s after the 8-byte signature and the chunk header.
42
+ *
43
+ * Returns `null` for anything that is not a PNG carrying an IHDR, rather than guessing: a
44
+ * caller printing a size is reporting what the file contains, and a plausible number derived
45
+ * from bytes that are not a header is worse than no number. Zero width or height cannot occur
46
+ * in a valid PNG (both are `>= 1`), so `null` is not reachable for a well-formed file.
47
+ */
48
+ export declare function pngSize(bytes: Uint8Array): PngSize | null;
49
+ /**
50
+ * Capture a screenshot through `source` and save it to `path`.
51
+ *
52
+ * Retries while the app answers empty bytes (see the file header), then throws
53
+ * {@link CaptureShotError} instead of writing a 0-byte file. The returned size is the PNG's
54
+ * own, not the size that was asked for.
55
+ *
56
+ * @throws {CaptureShotError} `empty-answer` after the retries are spent, `unreadable-png` when
57
+ * the bytes are not a PNG (a control plane that answered with something else is a different
58
+ * bug and must not be written out as an image), `write-failed` when the path is not writable.
59
+ */
60
+ export declare function captureShot(source: PngSource, path: string, scope?: string, options?: CaptureShotOptions): Promise<ShotResult>;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/devtools",
3
- "version": "0.52.0",
3
+ "version": "0.54.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",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "types": "./lib/types/index.d.ts",
11
11
  "default": "./lib/esm/index.js"
12
+ },
13
+ "./shot": {
14
+ "types": "./lib/types/shot.d.ts",
15
+ "default": "./lib/esm/shot.js"
12
16
  }
13
17
  },
14
18
  "files": [
@@ -45,12 +49,12 @@
45
49
  "@girs/gobject-2.0": "5.4.0",
46
50
  "@girs/graphene-1.0": "5.4.0",
47
51
  "@girs/gtk-4.0": "5.4.0",
48
- "@gjsify/devtools-protocol": "^0.52.0"
52
+ "@gjsify/devtools-protocol": "^0.54.0"
49
53
  },
50
54
  "devDependencies": {
51
- "@gjsify/cli": "^0.52.0",
55
+ "@gjsify/cli": "^0.54.0",
52
56
  "@gjsify/node-gi": "file:../../node-gi/node-gi",
53
- "@gjsify/unit": "^0.52.0",
57
+ "@gjsify/unit": "^0.54.0",
54
58
  "@types/node": "^25.9.2",
55
59
  "typescript": "^6.0.3"
56
60
  },